ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MATLAB PCA人脸识别系统:从特征脸原理到完整代码实现

MATLAB PCA人脸识别系统:从特征脸原理到完整代码实现 之前用 PCA 做人脸识别时最常见的困境不是看不懂 PCA 公式而是拿到一套代码后跑不起来数据集怎么放、读图脚本怎么写、训练和测试怎么划分、最后怎么把准确率算出来。本文整理了一套完整的 MATLAB PCA 人脸识别系统从原理到代码、从特征脸可视化到准确率评估尽量把每一步都说清楚。新手可以参考它理解完整流程有基础的也可以直接复用其中的训练与识别函数再按自己的数据集做调整。1. 背景与核心概念1.1 人脸识别要解决什么问题人脸识别的本质是一个分类问题给定一张未知身份的人脸图像从已经登记的人脸库中找到最接近的那一个人。不同于单纯的图像分类人脸识别通常有两个典型任务人脸验证Face Verification判断两张人脸是否属于同一个人。人脸识别Face Identification从包含多人的库中找出目标人的身份。PCA 人脸识别系统解决的是第二种任务它把每一张人脸图像看成一个高维向量利用主成分分析将高维向量映射到低维特征空间再通过距离度量判断身份。这个思路在 20 世纪 90 年代被 Turk 和 Pentland 提出也就是经典的 Eigenface特征脸方法直到今天仍然非常适合用来理解人脸识别的核心流程。1.2 为什么用 PCA 做人脸识别一张普通的人脸图像比如 112×92 像素展开成向量后有 10304 维。如果直接在这个高维空间里做距离计算不仅计算量巨大而且图像中大量像素之间存在相关性还会带来大量冗余信息。PCA 做的事情就是找到一组新的正交基使得数据在这些基上的投影方差最大。换句话说PCA 能够用很少的几个“主成分”去表示人脸的大部分信息。这些人脸主成分在图像空间里看起来仍然是一张“脸”所以被称为特征脸Eigenface。使用 PCA 人脸识别的优势很明显降维后计算速度快。能提取出人脸的主要变化模式。实现简单不需要复杂的深度学习框架。对小型人脸库和教学演示非常友好。1.3 本文适合哪些读者本文适合以下几类读者正在做 MATLAB 课程设计或毕业设计需要实现人脸识别系统。刚学完 PCA 但不清楚如何落地到图像任务。手里有一套源码但跑不通想理解每个文件的作用。希望快速搭建一个人脸识别演示系统后续再扩展改进。2. 环境准备与版本说明2.1 MATLAB 版本要求本文示例代码主要依赖 MATLAB 基础功能和图像处理工具箱Image Processing Toolbox因此理论上 R2016b 以上版本都可以运行。如果使用旧版本需要注意部分函数可能有差异。我建议使用 R2020b 或更新版本原因如下新版对矩阵计算的性能更好。与fullfile、dir等文件操作的兼容性更稳定。后续如果要扩展深度学习方案新版支持更好。你可以通过ver命令在 MATLAB 中查看当前版本ver如果没有安装图像处理工具箱可先在命令行里执行license(test, image_toolbox)返回1表示工具箱可用。2.2 数据集准备本文以经典的 ORLOlivetti Research Laboratory人脸库为例该数据集包含 40 个人每人 10 张灰度图像图像尺寸为 112×92。每张图像在光照、表情和面部细节上有少量变化非常适合小型 PCA 人脸识别实验。如果你没有下载好的数据集也可以用自己的照片构造数据。目录结构建议如下data/ s1/ 1.pgm 2.pgm ... s2/ 1.pgm 2.pgm ...也就是说每个人一个文件夹文件夹名以s开头内部放该人的所有人脸图像。读取代码会对.pgm、.jpg、.png做兼容处理。2.3 示例项目结构为了让代码结构清晰建议创建以下文件face_recognition/ main_pca_face.m % 主脚本 readFaceData.m % 数据读取函数 pcaTrain.m % PCA 训练函数 faceRecognize.m % 人脸识别函数这么做的好处是主脚本只负责整体流程具体功能由函数完成后期调整数据集或算法时不用大改主脚本。3. 核心原理拆解3.1 高维图像向量化在 MATLAB 中读取一张人脸灰度图后得到的是一个二维矩阵。例如 112×92 的矩阵用冒号展开后就得到一个 10304×1 的列向量img imread(face.pgm); imgVec img(:);假设训练集有 200 张图像把每一张图像向量化后按列拼接就得到一个 10304×200 的数据矩阵 X。接下来所有 PCA 计算都基于这个矩阵。3.2 平均脸与中心化PCA 的第一步是计算所有训练样本的平均脸meanFace mean(X, 2);然后让每个样本减去平均脸消除数据的整体偏移XCenter X - repmat(meanFace, 1, size(X, 2));中心化后的数据每一列表示该样本相对平均脸的“差异”。平均脸本身也是一个非常直观的可视化结果在实验里可以先显示出来看看数据集中人脸的整体样貌。3.3 协方差矩阵与特征值分解PCA 的核心是计算数据的协方差矩阵并求它的特征值和特征向量。如果数据维度为 d样本数为 m直接计算 d×d 的协方差矩阵 X * X 代价很大因为 d 可能是上万。好在这里的 m 往往远小于 d。利用矩阵分解的性质可以先计算 m×m 的小矩阵L XCenter * XCenter;对 L 做特征值分解[eigVecs, eigVals] eig(L);得到的特征向量是 m 维的想还原成原始 d 维空间中的特征脸只需左乘 XCentereigenfaces XCenter * eigVecs;这种“用小矩阵特征向量间接求大矩阵特征向量”的技巧是 PCA 在人脸识别中的关键优化避免了对 10304×10304 的大矩阵做直接分解。3.4 特征脸与降维投影特征脸是训练集中人脸变化的主要模式。选择前 K 个特征值对应的特征向量就构成了一组降维基底selectedVecs eigVecs(:, 1:K); eigenfaces XCenter * selectedVecs; % 归一化便于可视化与距离计算 for i 1:K eigenfaces(:, i) eigenfaces(:, i) / norm(eigenfaces(:, i)); end之后每个训练样本都可以投影到特征脸空间得到一个 K 维权重向量weightTrain eigenfaces * XCenter;识别时把待测图像也做同样的投影然后与所有训练样本的权重向量比较距离距离最近的那个训练样本的类别就是识别结果。4. 完整实战案例4.1 创建项目结构在你的工作目录下创建face_recognition文件夹把接下来要写的文件放在里面。数据集的目录结构按第 2.2 节准备。4.2 数据读取函数文件路径readFaceData.m该函数负责读取人脸库并划分训练集和测试集。选择每个人的前trainNum张为训练样本后testNum张为测试样本。function data readFaceData(dataDir, trainNum, testNum) % 读取人脸数据并划分训练集和测试集 % 输入 % dataDir - 数据根目录其中每个子文件夹代表一个人文件夹名以 s 开头 % trainNum - 每个人取前 trainNum 张作为训练样本 % testNum - 每个人取后 testNum 张作为测试样本 % 输出 % data - 结构体包含训练数据、训练标签、测试数据、测试标签 personDirs dir(fullfile(dataDir, s*)); numPersons length(personDirs); if numPersons 0 error(未找到以 s 开头的人脸子文件夹请检查数据集目录结构); end trainData []; trainLabels []; testData []; testLabels []; for i 1:numPersons folder fullfile(dataDir, personDirs(i).name); files dir(fullfile(folder, *.pgm)); if isempty(files) files dir(fullfile(folder, *.jpg)); end if isempty(files) files dir(fullfile(folder, *.png)); end if isempty(files) warning(文件夹 %s 中没有找到 pgm/jpg/png 图像, folder); continue; end totalFiles min(length(files), trainNum testNum); for j 1:totalFiles imgPath fullfile(folder, files(j).name); img imread(imgPath); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, [112 92]); imgVec img(:); if j trainNum trainData [trainData, imgVec]; trainLabels [trainLabels, i]; elseif j trainNum testNum testData [testData, imgVec]; testLabels [testLabels, i]; end end end data struct(trainData, trainData, trainLabels, trainLabels, ... testData, testData, testLabels, testLabels); end这段代码有几个值得注意的点使用fullfile拼接路径避免不同操作系统路径分隔符的问题。对彩色图像执行rgb2gray保证统一为灰度图。统一缩放到 112×92避免不同来源图像尺寸不一致导致向量长度不同。4.3 PCA 训练函数文件路径pcaTrain.m该函数实现 PCA 训练过程输出模型结构体包括特征脸、平均脸、训练样本投影权重和标签。function model pcaTrain(trainData, trainLabels, numComponents) % PCA 人脸识别训练函数 % 输入 % trainData - 训练数据矩阵每一列是一张人脸图像向量 % trainLabels - 训练标签向量与训练数据列一一对应 % numComponents - 保留特征脸数量 % 输出 % model - 结构体包含特征脸、平均脸、投影权重、标签等信息 X double(trainData); [dim, numSamples] size(X); if numComponents numSamples numComponents numSamples; warning(主成分数量超过训练样本数自动调整为 %d, numSamples); end meanFace mean(X, 2); XCenter X - repmat(meanFace, 1, numSamples); % 计算小矩阵 L XCenter * XCenter L XCenter * XCenter; [eigVecs, eigVals] eig(L); eigVals diag(eigVals); % 按特征值从大到小排序 [~, sortIdx] sort(eigVals, descend); eigVecs eigVecs(:, sortIdx); % 取前 numComponents 个特征向量映射回原空间得到特征脸 selectedVecs eigVecs(:, 1:numComponents); eigenfaces XCenter * selectedVecs; % 特征脸归一化 for i 1:numComponents normVal norm(eigenfaces(:, i)); if normVal 1e-12 eigenfaces(:, i) eigenfaces(:, i) / normVal; end end % 训练样本投影到特征脸空间 weightTrain eigenfaces * XCenter; model struct(eigenfaces, eigenfaces, meanFace, meanFace, ... weightTrain, weightTrain, trainLabels, trainLabels, ... numComponents, numComponents); end这里对特征值做了降序排序这是 PCA 里非常重要的一步。因为eig函数返回的特征值顺序并不保证是降序如果不排序直接取前 K 个得到的主成分贡献度可能并不是最大的 K 个。4.4 人脸识别函数文件路径faceRecognize.m该函数接收模型和待测图像向量返回预测标签。function predictLabel faceRecognize(model, testImgVec) % 基于欧氏距离的人脸识别 % 输入 % model - pcaTrain 返回的模型结构体 % testImgVec - 待测人脸图像向量 % 输出 % predictLabel - 预测的类别标签 testImgVec double(testImgVec(:)); centeredTest testImgVec - model.meanFace; weightTest model.eigenfaces * centeredTest; trainWeights model.weightTrain; % 计算与所有训练样本的欧氏距离 diffMat trainWeights - repmat(weightTest, 1, size(trainWeights, 2)); distList sqrt(sum(diffMat .^ 2, 1)); [~, minIdx] min(distList); predictLabel model.trainLabels(minIdx); end如果你想使用余弦相似度替代欧氏距离只需要把距离计算改为cosSim (trainWeights * weightTest) / (norm(weightTest) * sqrt(sum(trainWeights .^ 2, 1))); [~, maxIdx] max(cosSim); predictLabel model.trainLabels(maxIdx);欧氏距离更直观余弦相似度对光照变化稍鲁棒一些但具体效果仍需以实验为准。4.5 主脚本文件路径main_pca_face.m主脚本负责把整个流程串起来并输出准确率结果。clc; clear; close all; %% 1. 数据准备 dataDir D:/datasets/orl_faces; % 请修改为你的数据集路径 trainNum 5; % 每人取 5 张训练 testNum 5; % 每人取 5 张测试 data readFaceData(dataDir, trainNum, testNum); fprintf(训练样本总数: %d\n, size(data.trainData, 2)); fprintf(测试样本总数: %d\n, size(data.testData, 2)); %% 2. 显示平均脸 meanImg mean(data.trainData, 2); figure; imshow(reshape(meanImg, 112, 92), []); title(平均脸); %% 3. PCA 训练 numComponents 40; model pcaTrain(data.trainData, data.trainLabels, numComponents); %% 4. 显示特征脸 figure; numShow min(16, model.numComponents); for i 1:numShow subplot(4, 4, i); eigenImg reshape(model.eigenfaces(:, i), 112, 92); imshow(eigenImg, []); title(sprintf(特征脸 %d, i)); end %% 5. 测试识别 testCount size(data.testData, 2); correctCount 0; for i 1:testCount predLabel faceRecognize(model, data.testData(:, i)); if predLabel data.testLabels(i) correctCount correctCount 1; end end accuracy correctCount / testCount * 100; fprintf(识别准确率: %.2f%%\n, accuracy);运行后程序会依次显示平均脸、前 16 个特征脸并在命令行输出准确率。图 1 中平均脸是所有训练样本的平均效果它体现了该数据集的整体人脸形态。图 2 中的特征脸则代表不同的人脸变化方向例如光照方向变化、面部结构变化等。特征脸越多能表示的细节越丰富但也可能引入噪声。4.6 主成分数量对准确率的影响特征脸数量 K 是 PCA 人脸识别最重要的参数。K 太小降维后丢失的信息过多无法区分不同人脸K 太大又会引入噪声而且计算量增加。下面这段脚本可以观察 K 值变化对准确率的影响kList [5, 10, 20, 30, 40, 50, 60, 80, 100]; accList zeros(size(kList)); for idx 1:length(kList) k kList(idx); modelK pcaTrain(data.trainData, data.trainLabels, k); correctK 0; for i 1:testCount predK faceRecognize(modelK, data.testData(:, i)); if predK data.testLabels(i) correctK correctK 1; end end accList(idx) correctK / testCount * 100; fprintf(K%3d, 准确率%.2f%%\n, k, accList(idx)); end figure; plot(kList, accList, o-, LineWidth, 1.5); xlabel(主成分数量 K); ylabel(识别准确率 (%)); title(主成分数量对识别准确率的影响); grid on;在 ORL 数据集的常见实验条件下当训练样本为每人 5 张时K 在 20 到 50 之间通常能得到较高的准确率。不过具体数值受数据集划分、图像预处理方式影响较大不能一概而论。5. 常见问题与排查思路5.1 特征值出现负数或复数现象对 L 矩阵做eig分解后部分特征值为负或带微小虚部。原因人脸图像转换为double后范围可能不一致或者矩阵本身存在舍入误差导致数值上出现微小非对称。解决思路% 只取实数部分并过滤非正特征值 eigVals real(diag(eigVals)); eigVecs real(eigVecs);同时确保训练数据转换为 double 后取值范围一致比如统一除以 255 归一化到 [0,1]。5.2 图像尺寸不一致导致矩阵维度错误现象拼接训练数据矩阵时报错提示矩阵维度不一致。原因数据集中部分图像尺寸不同img(:)得到的列向量长度不同。解决思路在读取阶段统一imresize到固定尺寸。readFaceData中已经做了这一步如果你自己替换数据集也要保证所有图像经过同样处理。5.3 训练样本太少识别率偏低现象准确率明显低于预期特征脸可视化效果模糊。原因PCA 属于统计方法当每个类别的训练样本太少时无法准确估计类内变化模式。解决思路每个类别至少准备 4 到 5 张训练图像。同一人的图像尽量包含表情、角度、光照变化。增加训练样本数量或用数据扩展方式生成更多样本。5.4 程序运行内存不足现象在构造XCenter或eigenfaces时提示内存不足。原因图像维度 d 很大而训练样本数 m 也很大时直接存储 d×m 的矩阵会占用较多内存。解决思路降低固定图像尺寸比如从 112×92 缩小到 56×46。使用single类型存储数据减少内存占用。分批次读取和投影避免一次性加载全部数据。5.5 特征脸数量超过样本数导致警告现象运行 PCA 训练时提示主成分数量超过训练样本数。原因PCA 最多只能得到 min(d, m) 个有效主成分当 K 大于训练样本数时没有意义。解决思路在代码中限制 K 不超过训练样本数。pcaTrain中已经加入了自动调整逻辑如果自定义实现也要加上边界判断。6. 最佳实践与工程建议6.1 图像预处理优先于算法调参在 PCA 人脸识别系统里预处理的影响往往比主成分数量还大。建议按下面顺序做预处理转为灰度图。统一尺寸并转成列向量。直方图均衡化减弱光照变化影响。归一化像素值到 [0,1] 或均值为 0、方差为 1。直方图均衡化示例img histeq(img);在readFaceData中添加这一行相当于用很小的成本提高模型对光照变化的鲁棒性。6.2 特征脸数量选择策略不要盲目选择固定 K 值建议通过累计贡献率确定eigVals sort(real(eigVals), descend); totalEnergy sum(eigVals); cumEnergy cumsum(eigVals) / totalEnergy; K find(cumEnergy 0.95, 1); fprintf(达到 95%% 累计贡献率需要 %d 个主成分\n, K);一般取累计贡献率达到 90% 到 95% 的 K 值即可。这个策略比你手动尝试多个 K 值更科学也能在实际项目中形成可解释的参数选择依据。6.3 距离度量与匹配策略匹配阶段除了欧氏距离还可以使用最近邻Nearest Neighbor距离最小的训练样本。阈值判断如果最小距离大于某个阈值判定为“陌生人”而不是强行分配标签。K 近邻KNN取 K 个近邻投票确定类别。在工程系统中阈值判断非常重要因为真实场景中人脸库通常不会覆盖所有可能的人这时就需要一个“未注册人员”的拒绝策略。6.4 训练数据与测试数据划分划分数据时务必保证测试图像不参与训练。人的肉眼有时看不出来区别但代码里很容易不小心把同一张图既放训练集又放测试集这样得到的准确率虚高不具备真实参考价值。建议按照人脸图像文件名顺序划分并在代码里打印出训练和测试样本数量便于检查。6.5 批量识别与性能优化如果测试集很大逐张调用faceRecognize会比较慢。可以把测试集所有图像一次性投影到特征脸空间然后批量计算距离矩阵testMat double(data.testData); centeredTest testMat - repmat(model.meanFace, 1, size(testMat, 2)); weightTestBatch model.eigenfaces * centeredTest; distMatrix zeros(size(data.testData, 2), size(model.weightTrain, 2)); for i 1:size(weightTestBatch, 2) diffMat model.weightTrain - repmat(weightTestBatch(:, i), 1, size(model.weightTrain, 2)); distMatrix(i, :) sqrt(sum(diffMat .^ 2, 1)); end [~, idxMin] min(distMatrix, [], 2); predLabels model.trainLabels(idxMin);这种批量计算方式比逐张循环快得多也更容易利用 MATLAB 的矩阵运算特性。7. 总结与学习路线7.1 本文要点回顾到这里一个完整的 MATLAB PCA 人脸识别系统已经搭建完成。你可以回顾一下整个流程中的关键环节图像数据的读取与预处理。PCA 训练的数学处理与小矩阵技巧。特征脸的可视化与理解。测试图像的投影与最近邻识别。主成分数量和准确率的关系。对于课程设计来说掌握到这一步已经足够交出一份完整的人脸识别系统。但如果想深入提升还有几个方向可以继续研究使用 LDA线性判别分析代替 PCA实现 Fisherface 方法。使用核 PCAKernel PCA处理非线性人脸变化。在实际数据集上对比欧氏距离、余弦距离和曼哈顿距离的表现。加入人脸检测模块先在完整图像中定位人脸区域再做识别。尝试用深度学习特征替代原始像素特征再做 PCA 降维。7.2 项目落地的风险提示如果你打算把这个系统用于实际项目而不只是课设演示请特别注意以下几点PCA 方法对光照、姿态和遮挡比较敏感适合受控场景。识别人数不宜过多通常几十人的小型库效果较好。系统需要明确区分“识别成功”和“未注册人员”不能强行返回最近邻结果。人脸数据涉及个人隐私采集和使用时必须符合相关法律法规并在测试环境中使用授权数据。7.3 下一步建议建议你先把本文提供的完整源码在自己的数据集上跑一遍然后逐步修改固定的图像尺寸、训练样本数量和主成分个数观察识别率如何变化。这个过程能让你真正理解 PCA 人脸识别系统的每个参数对结果的影响。如果后续想扩展成实时人脸识别系统可以先加入摄像头采集图像、人脸检测、图像裁剪缩放等模块再复用本文的 PCA 训练与识别函数。本文完整源码如果需要直接运行版本或针对你自己的数据集做定制修改欢迎在评论区留言沟通。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进