ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MATLAB人脸识别实战:从PCA特征脸到参数调优全流程

MATLAB人脸识别实战:从PCA特征脸到参数调优全流程 简介这是一份系统讲解MATLAB人脸识别算法原理与实现的专题文档面向算法仿真学习者与图像处理研究者以YCbCr颜色空间肤色模型为核心解决人脸区域分割与检测定位问题。文档从肤色聚类原理切入完整呈现读图、色彩空间转换、二值化、形态学去噪、白色区域筛选及findeye眼睛特征确认的全流程并给出skin函数、findeye函数与主程序的详细代码便于读者理解算法细节并快速复现。全文以单份docx格式提供文件大小约385KB体积精炼内容紧凑适合下载后边读边练。目前已有201人学习使用认可度良好。需要提醒的是实际部署时需根据光照与肤色差异调整模型参数文档中也给出了相应建议对入门与进阶均有参考价值。1. MATLAB 实现人脸识别算法最难的不是分类器“老生谈算法”这个名字容易让人以为又是一篇 PCA 公式推导加一段现成代码但真按标题去复现时最先卡住你的往往不是特征提取而是从图像文件夹到特征矩阵这一小段路路径怎么拼、灰度怎么归一化、训练集测试集怎么切、特征脸怎么画出来验证“算法确实学到了东西”。这套流程我断断续续做过三四个版本从 PCA、LDA 到深度学习嵌入都试过最稳定的入门主线仍然是 PCA 特征脸。这篇按“选型 → 实现 → 预处理 → 调参验证”的顺序把一个人脸识别算法在 MATLAB 里的完整落地路径讲清楚代码可以直接抄下来改路径运行也顺手把那些教程里不写明的参数边界说透。2. 人脸识别算法选型PCA、LDA、LBP 在 MATLAB 里的适用边界2.1 为什么先讲 PCA 特征脸92×112 的图片只有几十个自由度人脸识别算法在 MATLAB 里的“经典三件套”是 PCA、LDA 和 LBP。先说 PCA 特征脸。一张 92×112 的灰度图按列拉直后是 10304 维向量但人脸图像根本不会占满整个高维空间——光照、姿态、表情的变化集中在少数几个方向上。PCA 做的事情就是找到这些方向。原始论文里的做法是把每张图减去均值脸然后对协方差矩阵做特征分解特征向量就是“特征脸”把新图像投影到前 k 个特征脸上用投影系数做分类。数学上写设训练集按列排成矩阵 X10304×N均值脸为 m中心化后 Xc X - m·1^T协方差矩阵 C Xc·Xc^T / (N-1)。C 的特征向量就是特征脸。但直接在 10304×10304 的矩阵上求特征向量非常浪费内存。常见做法是用 SVD 技巧对 Xc 做经济奇异值分解[U, S, V] svd(Xc, econ)U 的列就是协方差矩阵的特征向量特征值等于 S 对角元的平方除以 N-1。U 只有 N 列对应 N-1 个非零特征值计算量小得多。需要强调的是这里的“降维数 k”不是模型自动决定的。每个人脸库的秩不同光照变化大的库需要的 k 更大。一般观察特征值衰减曲线取累积能量超过 90% 的 k按经验值看 30 到 80 之间居多。不要一上来就定 k100后面我会给扫描代码。2.2 LDA、LBP 和 CNN什么情况下值得换PCA 是纯无监督降维它不关心类别标签所以保留的方向不一定是“最利于分类”的方向。LDAFisherface改进了这一点它最大化类间散布、最小化类内散布也就是说投影后同一人的照片更聚拢、不同人分得更开。缺点是人脸库类别数不能太少且当样本维度远大于样本数时类内散布矩阵必然奇异需要先用 PCA 降到 N-c 维再算 LDA这一步不提的话代码一跑就报错。MATLAB 里 LDA 的核心是解广义特征值问题常见写法是先做 PCA 降维再算类内、类间散布矩阵% Xpca: PCA降维后的投影系数, labelTrain: 训练标签 Sw zeros(size(Xpca,1)); Sb zeros(size(Xpca,1)); mu mean(Xpca, 2); for c unique(labelTrain) Xc Xpca(:, labelTrain c); mc mean(Xc, 2); Sw Sw (Xc - mc) * (Xc - mc); Sb Sb size(Xc,2) * (mc - mu) * (mc - mu); end [V, D] eig(pinv(Sw) * Sb); % 用伪逆而不是直接相除 [~, idx] sort(diag(D), descend); % 按特征值降序取列 W_lda V(:, idx(1:c-1));代码说明Sw 是类内散布矩阵Sb 是类间散布矩阵c 是人数pinv(Sw) * Sb的特征值越大代表该方向类间分离性越好取前 c-1 列作为投影矩阵。注意 eig 返回的特征向量不保证按特征值排好序必须手动排序这一点经常被跳过。LBP 走的是另一条路不学全局子空间而是提取局部纹理直方图。MATLAB 的 Computer Vision Toolbox 直接提供了extractLBPFeatures内部实现 Uniform LBP默认输出 59 维直方图把图像分块后拼接直方图距离度量用卡方距离效果较好。LBP 对光照变化比 PCA 更稳但对表情和姿态更敏感且没有投影系数那种“可解释性”。如果认真对比在 ORL 这种每人只有 10 张的库上LBP 识别率通常比 PCA 高几个点但特征维度和调参项也更多。至于深度学习路线CNN 特征嵌入MATLAB 从 R2018a 起支持trainNetwork也能用预训练模型提特征做迁移学习但人脸数据少于几万张时容易过拟合。常见现象是把 LFW 上训练好的模型拿来提特征用余弦距离做验证效果很好自己从头训练小 CNN识别率反而打不过 LBP。这几种算法之间的权衡我用下面这张表总结过多次算法前提条件对光照鲁棒性对姿态鲁棒性MATLAB 实现难度小样本表现PCA 特征脸要求人脸对齐弱弱低30 行内尚可LDA Fisherface类别数≥2先 PCA 降维中弱中需处理 Sw 奇异较好LBP 直方图分块参数要调较强弱中工具箱封装好较好CNN 微调需要大训练集强较强高需 GPU 更佳差除非迁移如果训练样本只够每人数张、项目周期也紧PCA 仍然是最稳妥的起点。它让你把“数据管好”这件事练熟后续换 LDA 或 LBP 都只是换特征提取那一段。网上不少 MATLAB 教程会在 PCA 之后直接接一个 BP 神经网络做分类常见说法是 BP 能拟合非线性分类边界但 400 张照片喂给 BP 很容易演变成过拟合表演而 BP 神经网络拟合曲线那套调参经验隐层数、学习率、激活函数平移到这里并不适用我更推荐先用最近邻跑通基线再谈更复杂的分类器。2.3 一句话选型建议最终选型取决于你手上的数据量。每人 25 张、纯正脸、背景稳定PCA 足够代码最简单。每人数张但光照差异大LBP 或 LDA。每人有几十张以上且包含姿态变化考虑先用预训练 CNN 提特征再做最近邻或线性 SVM。本文后续的完整实现仍以 PCA 为主线因为它的检查手段最直观——特征脸可以直接画出来看特征脸长得像人脸才说明数据预处理和矩阵计算是对的。3. 用 MATLAB 实现 PCA 特征脸从 ORL 数据集到识别率的完整代码3.1 读入 ORL 数据集路径、灰度与列向量化这一步是最容易被教程略过、却最常出错的环节。ORLATT人脸库是公开的经典小样本库40 个人每人 10 张 112×92 灰度 PGM 图像目录结构是orl_faces/s1/1.pgm到s40/10.pgm。读数据时要把每张图拉成列向量最终拼成一个大矩阵同时记录每列对应的标签folder orl_faces; % 改成你的实际路径 numPersons 40; imgsPerPerson 10; X []; labels []; for p 1:numPersons for k 1:imgsPerPerson name fullfile(folder, sprintf(s%d, p), sprintf(%d.pgm, k)); img imread(name); % imread 支持 pgm img im2double(img); % uint8(0~255) - double(0~1) X [X, img(:)]; % 112*9210304 维列向量 labels [labels; p]; end end代码说明img(:)把二维矩阵按列拉成向量这是 MATLAB 常用的矢量化操作im2double是必须的否则后面算均值和协方差时会因为 uint8 溢出出诡异结果。fullfile负责跨平台拼接路径比直接写s p安全。如果你自己的图片是 jpg/png把pgm换成对应扩展名即可或者用imageDatastore递归读取效果相同但少一层循环控制。读完后建议立刻打印一下size(X)看到10304×400才说明数据组织正确。经常有人在这里得到400×10304后面一堆维度错误全是因为行和列的方向反了。整个流程里我坚持“每列一个样本”的约定因为 MATLAB 矩阵运算默认沿列方向执行后续mean(X, 2)求均值脸、svd(Xc, econ)拿特征脸都不用转置。3.2 用 SVD 求特征脸为什么不用自带 pca 函数硬算数据组织好之后直接进入 PCA 核心。MATLAB 有现成的pca函数一行就能出主成分方向[coeff, score, latent] pca(X); % X 是 400×10304每行一个样本 W coeff(:, 1:k); % 取前 k 个主成分但我不建议只在代码里依赖它一是面试和课程答辩总要讲清楚原理二是pca函数内部自己做了中心化很多人用的时候忘记原始数据要不要减均值概念容易混。自己用 SVD 写一遍逻辑更透明meanFace mean(X, 2); % 均值脸每行是每个像素的均值 Xc X - meanFace; % 中心化广播到每一列 [U, S, V] svd(Xc, econ); % 经济SVDU: 10304×400 eigVals diag(S).^2 / (size(Xc, 2) - 1); k 50; % 降维数先给保守值 W U(:, 1:k); % 特征脸按列排列 projTrainAll W * Xc; % 400 张训练图全部投影到 k 维代码说明svd(..., econ)对高维小样本数据非常关键它会返回一个 10304×400 的 U 而不是 10304×10304 的满矩阵否则内存直接爆炸。特征值由奇异值平方再除 N-1 得到对应协方差矩阵的特征值。这里W的每一列就是一张特征脸W * Xc得到的是每张图在特征脸基下的坐标形状是k×400。凡是教程里“先对全体数据算 PCA 再划分训练测试”的做法都是有问题的正确流程是先划分、再用训练集算 W测试集只做投影和分类否则就是数据泄漏识别率虚高得离谱。顺带列一下这段代码的几个关键参数k是降维数太小丢信息太多太大等于没降Xc的中心化基于训练集的均值脸测试集也必须减同一个均值脸不能各算各的否则人脸不在同一个坐标系里分类必然崩。3.3 最近邻分类与识别率评估拿到投影系数后分类就变得很朴素了。我们把每个类别的训练照片投影到特征空间测试照片也投影过去然后找距离最近的训练样本把它的标签当作预测结果。这种最近邻分类器复杂度为 O(测试数 × 训练数 × 维度)在 400 张图上毫无压力。这里用分层随机划分保证每人的 10 张照片均匀分进训练集和测试集而不是整体随机抽——否则可能出现某人照片全在测试集、训练集里根本没这人得分会虚低。rng(42); % 固定随机种子结果可复现 trainMask false(400, 1); testMask false(400, 1); for p 1:numPersons idx find(labels p); % 第p个人的10张图的下标 perm idx(randperm(10)); trainMask(perm(1:5)) true; % 每人取5张训练 testMask(perm(6:10)) true; % 剩下5张测试 end XcTrain Xc(:, trainMask); XcTest Xc(:, testMask); labelTrain labels(trainMask); labelTest labels(testMask); projTrain W * XcTrain; projTest W * XcTest; D pdist2(projTest, projTrain, cosine); % 测试集×训练集距离矩阵 [~, minIdx] min(D, [], 2); pred labelTrain(minIdx); accuracy mean(pred labelTest) * 100; fprintf(PCA 特征脸识别率%.2f%%\n, accuracy);参数说明距离度量我推荐cosine而不是默认的欧氏距离euclidean。原因是投影系数向量的模长受光照强度影响较大而方向更接近“这个人长什么样”的本质。如果你用欧氏距离可以试试对每个投影向量做 L2 归一化效果和余弦距离基本等价。pdist2需要 Statistics and Machine Learning Toolbox如果没有这个工具箱就写两层 for 循环计算余弦相似度400 的量级不会慢。这套代码跑完ORL 上一般能到 90% 到 97%取决于划分和 k 的选择。如果第一次跑出来低于 85%先别急着调模型大概率是特征归一化或随机划分出了问题去画特征脸检查。3.4 把特征脸和重构人脸画出来只盯着一个数字没法判断程序是不是“真的学会了”。我每次写完这段流程都会强制自己至少画两类图前几个特征脸和一张图的原始版本/重构版本对比。特征脸长得像人脸才能确认 SVD 方向正确重构误差小才能确认 k 取得够大。figure; for i 1:9 subplot(3, 3, i); imshow(reshape(W(:, i), 112, 92), []); title(sprintf(特征脸 %d, i)); end % 用前50个特征重构第一张测试图 recon W * projTest(:, 1) meanFace; figure; subplot(1, 2, 1); imshow(reshape(XcTest(:, 1) meanFace, 112, 92), []); title(原始人脸); subplot(1, 2, 2); imshow(reshape(recon, 112, 92), []); title(50特征重构);说明reshape(W(:, i), 112, 92)把特征向量还原成图像尺寸第二个参数[]表示让 imshow 自动按数据类型缩放灰度范围。MATLAB 画图这里有个常见坑imagesc会自动拉伸灰度而imshow配合[]才是对浮点图像的标准做法。重构时把投影系数乘回特征脸方向、再加回均值脸这是 PCA 的可逆性带来的验证手段。一个实用技巧是画出累积能量曲线判断 k 的合理区间energy cumsum(eigVals) / sum(eigVals); figure; plot(energy); xlabel(特征值数量); ylabel(累积能量); % 找能量达到90%的最小k k90 find(energy 0.9, 1); fprintf(能量达到90%%需要k%d\n, k90);eigVals是按奇异值降序排列的cumsum累加后除以总和就是前若干个特征值贡献的能量占比。这段曲线是本节最有价值的输出如果前 50 个特征值就占了 95% 能量说明 10304 维图像背后真正的自由度并不高如果到 200 还不到 80%说明你的数据集里人脸对齐或光照归一化做得不好加了太多“噪声自由度”。4. 人脸图像预处理与调参从实验室样例到摄像头实时识别4.1 用 vision.CascadeObjectDetector 完成人脸检测与对齐离线识别跑通后面临的第一个现实问题就是训练用的 ORL 图像都是已经裁剪好、眼睛大致对齐的正脸而你手里的图片可能是一张全家福或摄像头抓拍。常见做法是先用级联检测器定位人脸再裁剪。MATLAB 里这一套封装得很好几行代码就能接进识别管线detector vision.CascadeObjectDetector(FrontalFaceCART); img imread(test.jpg); bboxes detector(img); % 返回 n×4 的[x y w h] if isempty(bboxes) error(没有检测到人脸); end bbox bboxes(1, :); % 只取检测分数最高的一张 face imcrop(img, bbox); face imresize(face, [112 92]); % 缩放与训练图同尺寸 if size(face, 3) 3 % 彩色转灰度 face rgb2gray(face); end face im2double(face);这段的逻辑是检测 → 裁剪 → 缩放 → 灰度化 → 双精度化。FrontalFaceCART是基于 Viola-Jones 的检测器对正脸效果好侧脸会漏检换ProfileFace可以提升侧脸召回但误检也多。检测框坐标是[x, y, width, height]imcrop和imresize在这里负责把人脸统一到训练尺寸这一步是“对齐”的最简版本。如果想更精细可以用vision.PointsTracker检测双眼位置做旋转校正把两只眼睛的角度拉平这一步通常能带来 23 个百分点的识别率提升。要提醒的是检测框裁出来的图人脸边缘会有背景区域光照条件和训练集也不一致。所以真实场景的识别率一开始往往比 ORL 上低十个百分点以上这是正常的检测带来的对齐误差比分类器本身的误差影响更大。4.2 光照归一化histeq 与自适应的两种写法处理光照不均时MATLAB 图像处理里最常用的一行是histeq它能把直方图拉伸到接近均匀分布抵消部分光照影响。但它的问题是会对整张图做全局变换如果半边脸在阴影里效果就有限。我一般会组合两步先直方图均衡再做逐像素的均值方差归一化。% 写法1全局直方图均衡简单直接 faceEq histeq(face); % 写法2自适应直方图均衡适合光照剧烈变化 faceEq2 adapthisteq(face, NumTiles, [8 8], ClipLimit, 0.02); % 写法3标准化到零均值单位方差最好放在最后 faceNorm (faceEq - mean(faceEq(:))) / std(faceEq(:));参数说明NumTiles把图像切成的块数越大局部对比度增强越强但也容易放大噪声ClipLimit是直方图裁剪上限取值范围一般在 0.010.05太大会出现过曝效果。在 ORL 上这些预处理提升不大因为库本身光照均匀但换到自拍照片或摄像头画面光照归一化常常直接影响最后几层分类器比调 k 值更有效。预处理的选择本质上是对数据分布做的假设和人脸识别算法本身耦合得很深所以要在管线层面调试而不是单独调某个函数。4.3 必调的三个参数和推荐取值范围整套 PCA 人脸识别管线里影响识别率最大的三个参数是降维数 k、距离度量、每人的训练样本数。第一个控制特征空间容量第二个控制相似度定义第三个控制数据充足度。三者的优先级是训练样本数 降维数 距离度量。参数推荐范围调试方法常见误区降维数 k3080能量90%扫描 k画识别率曲线直接设成 100认为越多越好距离度量cosine 优先对比 euclidean / cosine / correlation不做归一化直接比欧氏距离每类训练样本数57 张ORL 上每次固定随机种子递增样本数训练测试不按人分层划分参数搜索本质上是优化问题把 k 当超参丢给 MATLAB 优化工具箱或者自己写网格扫描都可以但数据量小的时候手写扫描反而更直观ks [10 20 30 50 80 100]; for kk ks Wt U(:, 1:kk); projTr Wt * XcTrain; projTe Wt * XcTest; D pdist2(projTe, projTr, cosine); [~, mi] min(D, [], 2); acc mean(labelTrain(mi) labelTest) * 100; fprintf(k%3d, 识别率%.2f%%\n, kk, acc); end训练样本数的影响最直观每人 3 张训练ORL 上大概 85%每人 5 张95% 左右每人 9 张98% 以上。如果你的项目数据量连每人 3 张都保证不了任何经典算法都很难有好的表现这时该考虑数据增强左右翻转、小角度旋转、加噪声而不是换模型。5. 识别率上不去时先检查这四个环节5.1 先查特征脸长什么样不要盯着识别率空想模型表现不好第一步不是调参而是看特征脸。在同一个 MATLAB 会话里运行诊断脚本画出前 20 个特征脸和原始人脸的重构对比。如果特征脸出现明显的棋盘格或噪点说明训练图没有对齐如果前几张特征脸看起来几乎一样说明光照预处理过强把个体差异抹掉了。另一个常用指标是特征值能量曲线前 10 个特征值占掉 80% 以上能量说明数据模式集中降维有效如果能量分散多半是原始图片缩放时破坏了宽高比或者混入了非人脸区域。5.2 用交叉验证和混淆矩阵定位问题来源单次随机划分的识别率波动很大我习惯做 5 次重复随机划分、每次重新划分训练测试、最后汇报均值和标准差避免“这次跑得不错”的假象。accList zeros(5, 1); for r 1:5 % 重新执行“3.3 节”的划分代码rng(r) 保证可复现 accList(r) runPCAEvaluation(r); end fprintf(平均识别率: %.2f%% ± %.2f%%\n, mean(accList), std(accList)); % 对最后一次结果画混淆矩阵 cm confusionchart(labelTest, pred); cm.RowSummary row-normalized;confusionchart是 R2018b 之后内置的混淆矩阵可视化行归一化之后能直接看出哪些人被经常认混。如果混淆集中在某几个人身上多半是他们的面部特征太像跟算法本身关系不大如果把错误分散到所有人说明 k 或预处理有问题。5.3 从识别转向验证设置相似度阈值最后一个环节是从“这是哪个人”的识别问题转向“这两个人是不是同一个人”的验证问题。常见做法是计算测试样本与训练样本的距离分布同一人的距离正样本对和不同人的距离负样本对应该有明显分离在这个分布之间取一个阈值。如果两个分布重叠严重说明特征空间的判别力不够回到 5.2 节看混淆矩阵找原因。这里的技巧是把阈值选择当作一个一维搜索用perfcurve画出 ROC 曲线选择等错误率EER对应的阈值。EER 越低说明特征表示越适合做验证。这个指标也比“识别率”更能反映算法在实际闸机、签到场景下的可靠性适合作为项目验收依据。本文还有配套的精品资源点击获取
返回列表