MATLAB平台下基于PCA算法的人脸识别图像考勤系统
基于MATLAB平台的PCA的人脸识别图像考勤系统 识别原理为:从一副生活照中寻找到人脸,并且分割人脸图象,利用PCA算法进行降维,和库里图片进行对比,人脸库和识别图像请自行添加
今早挤完电梯冲进公司打卡——指纹考勤机“滴”的一声提示“无法识别,请重试”,第三次重试差点摔工牌时,突然想起上周蹲实验室调的那个基于MATLAB的破刷脸考勤,虽然现在还只能放正脸光亮点戴眼镜框(隐形识别率差点意思后续慢慢调),但至少不用跟磨平的指纹死磕。
捣鼓这玩意儿的核心其实就是三步:抓脸切脸、降维瘦身、比脸找身份,全靠MATLAB自带的工具箱撑场子,省了不少手写底层算法的头发。
第一步:抓脸切脸——别拿后脑勺糊弄机器
首先得有个“脸库”对吧?我随便从ORL人脸库里扒了40个人,每人10张表情光线稍微有点小变化的正脸(分辨率112×92,懒癌晚期不想自己拍太多),丢在项目根目录的face_database文件夹里,每人建个自己的子文件夹,比如person01到person40,每张图命名成1.pgm到10.pgm——名字统一后续代码好遍历。
考勤的测试图我单独放test_face文件夹,也是正脸清晰的PGM或者JPG都行(PGM是灰度图更省事儿)。
抓脸我用的是MATLAB自带的Viola-Jones检测器——这个玩意儿是老古董但巨好用,不用自己调复杂的参数,自带训练好的人脸模型。先上一段抓脸代码:
% 读入待考勤的测试图,假设是今天摸鱼拍的摸鱼前.jpg摸鱼后的.jpg?选个严肃的吧test.jpg
test_img = imread('test_face/test.jpg');
% 要是彩色图先转成灰度,Viola-Jones对灰度图友好
if size(test_img,3) == 3
test_gray = rgb2gray(test_img);
else
test_gray = test_img;
end
% 检测器的参数:第一个是训练好的人脸模型,选这个最小误差的就行
face_detector = vision.CascadeObjectDetector;
% 检测人脸框,bbox是[left top width height]四个数
bbox = step(face_detector, test_gray);
% 可视化一下抓脸结果,万一抓的是鼻孔呢
result_img = insertObjectAnnotation(test_img, 'rectangle', bbox, '今天来了个谁?');
figure;imshow(result_img);
哎对了有时候检测可能会抓到多个框?比如我那天把工位的鼠标垫卡通人物也放进去了,就被当成“人脸”了。不过咱们场景是固定的打卡区域,只有一个人对着摄像头,加个简单的筛选就行——选面积最大的框:
if ~isempty(bbox)
% 计算每个框的面积
areas = bbox(:,3).*bbox(:,4);
% 找面积最大的索引
[~,max_idx] = max(areas);
bbox = bbox(max_idx,:);
end
抓到框之后就该切脸了,切完统一缩放到和脸库一样的112×92,不然后续降维没法对齐:
if ~isempty(bbox)
% 从灰度图里切,因为PCA降维用的是灰度
crop_face = imcrop(test_gray, bbox);
% 统一缩放
resize_face = imresize(crop_face, [112 92]);
% 保存或者直接用,先可视化缩切后的结果
figure;imshow(resize_face);title('切好的考勤脸');
else
error('连脸都没抓到!请把正脸对准摄像头!');
end
刚才那段代码的imresize其实也可以换成双线性插值什么的,但MATLAB默认的最近邻插值对付这个分辨率已经够了。
第二步:降维瘦身——别让1万多个像素点打架
脸库每张图是112×92=10304个像素点,如果直接拿10304维的向量去比脸,就像在一万层楼里找邻居——太麻烦太慢了,而且很多像素点是重复的(比如背景墙都是白色的,或者额头都是平的),没用的信息很多。
基于MATLAB平台的PCA的人脸识别图像考勤系统 识别原理为:从一副生活照中寻找到人脸,并且分割人脸图象,利用PCA算法进行降维,和库里图片进行对比,人脸库和识别图像请自行添加
这时候就该PCA(主成分分析)登场了——它的核心思想就是“把有用的信息拧成一股绳,没用的信息扔掉”。比如10304维的向量,PCA能给你降到50维甚至20维,保留90%以上的有用信息。
先理理PCA在人脸识别里的流程(别太复杂,大概意思到就行):
- 把脸库所有图拉成列向量,拼成一个“大脸矩阵”,每一列是一张脸
- 减去大脸矩阵的平均脸,得到“中心化脸矩阵”
- 计算中心化脸矩阵的协方差矩阵?不对不对——脸的数量(比如40×10=400张)远小于像素数(10304),直接算协方差矩阵会死人的(10304×10304的矩阵),所以算协方差矩阵的转置转置转置(重要的事情说三遍),也就是“中心化脸矩阵的转置 × 中心化脸矩阵”,得到400×400的小矩阵,算它的特征值和特征向量,然后再还原回去,得到大矩阵的特征向量(也就是所谓的“特征脸”)
- 选前k个最大的特征值对应的特征向量作为投影矩阵
- 把脸库所有的中心化脸都投影到投影矩阵上,得到“脸库特征向量集”
好,上代码(先假设我们已经把脸库遍历完拉成大脸矩阵了,遍历脸库的代码后面补):
% ------------------ 假设已经有这些变量: ------------------
% 1. all_faces: 大脸矩阵,10304行×400列,每列是拉成列向量的脸(double类型,不然数值会溢出)
% 2. labels: 标签向量,400行×1列,person01的10张图标1,person02标2,...,person40标40
% -----------------------------------------------------------
% 第一步:计算平均脸,再中心化
avg_face = mean(all_faces, 2); % 10304行×1列
centered_faces = all_faces - avg_face;
% 第二步:转置转置转置,算小矩阵的特征值特征向量,再还原
small_cov = centered_faces' * centered_faces; % 400×400
[eig_vecs_small, eig_vals] = eig(small_cov); % 特征值默认从小到大排
eig_vecs_small = fliplr(eig_vecs_small); % 左右翻转,变成从大到小
eig_vals = diag(eig_vals); % 提取对角线上的特征值
eig_vals = flipud(eig_vals); % 上下翻转,从大到小
% 还原成大矩阵的特征向量(特征脸)
eig_vecs = centered_faces * eig_vecs_small;
% 归一化特征脸,不然后续投影的数值会乱
eig_vecs = eig_vecs ./ vecnorm(eig_vecs);
% 第三步:选前k个特征脸,这里选能保留95%信息的k
cumulative_ratio = cumsum(eig_vals) / sum(eig_vals);
k = find(cumulative_ratio >= 0.95, 1, 'first');
fprintf('保留95%%信息的特征脸数量:%d\n', k);
projection_matrix = eig_vecs(:, 1:k); % 10304×k
% 第四步:把脸库所有中心化脸投影到投影矩阵上
face_database_features = projection_matrix' * centered_faces; % k×400
% 可视化一下前9个特征脸,看看长啥样(就是一堆抽象的脸)
figure;
for i = 1:9
subplot(3,3,i);
eigenface = reshape(eig_vecs(:,i), [112 92]);
imshow(eigenface, []); % []自动调整对比度,不然全是黑的
title(['特征脸', num2str(i)]);
end
sgtitle('前9个抽象的特征脸');
这段代码里的cumsum和find组合特别好用,不用自己手动数保留多少个特征脸,直接让程序算到95%就行,我试了ORL库大概保留40-50个,超级省事儿。
哦对了补一下遍历脸库的代码,不然前面的all_faces和labels从哪儿来:
% 初始化变量
all_faces = [];
labels = [];
% 脸库根目录
database_dir = 'face_database';
% 遍历所有person子文件夹
person_dirs = dir(database_dir);
person_dirs = person_dirs(~ismember({person_dirs.name}, {'.', '..'})); % 去掉.和..
for i = 1:length(person_dirs)
person_path = fullfile(database_dir, person_dirs(i).name);
image_files = dir(fullfile(person_path, '*.pgm')); % 只找pgm格式的
for j = 1:length(image_files)
image_path = fullfile(person_path, image_files(j).name);
img = imread(image_path);
% 转成double,拉成列向量
img_vec = double(reshape(img, [], 1));
all_faces = [all_faces, img_vec];
labels = [labels; i]; % person01标1,以此类推
end
end
遍历完就可以把这些变量保存成MAT文件,下次直接加载不用再遍历:
save('face_database_data.mat', 'all_faces', 'labels', 'avg_face', 'projection_matrix', 'face_database_features');
第三步:比脸找身份——测测今天摸鱼的是谁
刚才第一步我们已经把测试图切好缩好转成列向量了,接下来就是和脸库比脸:
% 加载之前保存的MAT文件
load('face_database_data.mat');
% ------------------ 假设已经有切好缩好的resize_face: ------------------
% 转成double,拉成列向量
test_vec = double(reshape(resize_face, [], 1));
% 中心化
centered_test = test_vec - avg_face;
% 投影到特征脸空间
test_feature = projection_matrix' * centered_test; % k×1
% 计算欧氏距离,找最近的脸
distances = vecnorm(face_database_features - test_feature, 2, 1); % 1×400,每个元素是和对应脸库图的距离
[min_dist, min_idx] = min(distances);
% 设置一个阈值,防止拿陌生人的脸过来(阈值自己调,ORL库我试的5000左右合适)
threshold = 5000;
if min_dist < threshold
person_id = labels(min_idx);
fprintf('考勤成功!是Person %02d,距离:%.2f\n', person_id, min_dist);
% 可以把名字对应上,比如建个字典
name_dict = {'张三','李四','王五',...}; % 40个名字,自己填
fprintf('真实姓名:%s\n', name_dict{person_id});
else
fprintf('识别失败!陌生人或者脸没放正,距离:%.2f\n', min_dist);
end
欧氏距离就是最普通的距离公式,PCA人脸识别里也有用余弦相似度的,但我试了ORL库欧氏距离已经够用了,而且好调阈值。
阈值怎么调呢?可以拿脸库的训练集和测试集分开,比如每人拿8张当训练,2张当测试,然后画ROC曲线找最优阈值——但懒癌晚期的我直接拿几张陌生人的脸和几张脸库的脸试了试,大概5000左右就行。
最后:一些小吐槽和小改进
这个系统其实挺简陋的,比如:
- 只能识别正脸,侧脸低头戴口罩全歇菜
- 光线影响很大,要是考勤机对着窗户,逆光根本抓不到脸
- 戴隐形眼镜有时候识别率会下降(可能是我扒的ORL库戴眼镜的样本少?)
- 没有集成摄像头,只能拿拍好的图片测试
小改进的话,比如:
- 抓脸的时候可以加个眼睛检测,确保是正脸
- 降维之后可以加个LDA(线性判别分析),提升类间距离
- 集成MATLAB的摄像头工具箱,实时抓脸实时识别
- 加个Excel导出功能,记录考勤时间
不过对于我这种不想跟指纹死磕的懒癌来说,现在这个版本已经够用了——下次挤完电梯,掏出手机拍张正脸,丢进去跑一遍,就能代替考勤机打卡(嘘,别让老板知道)。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)