
简介基于MATLAB的PCA算法人脸识别项目包含完整源码、GUI界面与说明文档面向高校课程设计、期末大作业以及模式识别入门实践覆盖人脸图像读取、PCA降维、特征提取与分类识别等关键环节运行环境依赖清晰下载后无需修改即可复现结果适合快速交付或在此基础上扩展。压缩包共16个文件包含11张人脸样本图像、4个MATLAB脚本以及1份说明文档整体仅269KB脚本分别承担数据加载、PCA计算与识别匹配等职责说明文档则梳理实现流程、参数配置与结果分析便于对照学习或用于答辩讲解。项目已有426人学习/下载属于高完成度的小型实验工程。借助GUI界面可以直观查看识别效果结合文档还能理解特征脸原理与降维思想既可作为期末设计的高分方案也能为后续深入研究人脸识别算法提供可运行的起点。1. PCA人脸识别项目为什么值得用MATLAB重写一遍打开那个以 zip 结尾的 MATLAB 人脸识别项目你可能第一反应是找说明文档但真正值得先看的其实是主程序里对 PCA 的调用方式。PCA主成分分析人脸识别在深度学习流行前几乎是课程设计和毕业设计的默认起点它用几十行 MATLAB 代码就能在 ORL 数据集上跑出接近 90% 的识别率。把 GUI 界面加上之后整个流程从“命令行里跑通”变成“可以演示、可以调参数、可以继续扩展”的作品。下面这条链路从数学原理讲到源码实现再讲到 GUI 排错最后给出验证识别率的方法让你拿到这套源码时不只是会运行还知道每一步在做什么。2. PCA主成分分析特征脸与降维的数学原理2.1 一张人脸图怎么变成一列向量很多人拿到人脸识别代码后第一步会卡在数据矩阵的组织方式。常见的人脸库如 ORL、Yale里的每张图都是灰度图先用imread读入再用imresize统一到 64×64 像素。MATLAB 按列展开这张图得到 4096 维的列向量。把 M 张训练图拼在一起就得到一个 4096×M 的矩阵。这里有个容易混淆的点如果按行拼后续 mean 和协方差计算的方向全部要反过来。我一般规定好“每列一个样本”后面所有代码都遵守这个约定。还应该注意彩色图有三个通道需要先rgb2gray否则展开后的维度是 3×4096PCA 结果会被颜色信息干扰而人脸识别里颜色通常不是主要判别依据。2.2 协方差矩阵的特征值为什么能代表人脸特征把所有训练向量减去平均脸得到去均值矩阵 X。平均脸就是所有样本在像素级取平均值后的结果它看起来像一张模糊的人脸轮廓。自协方差矩阵 C (1/M) X Xᵀ 的第 (i,j) 个元素表示第 i 个像素与第 j 个像素在训练集上共同变化的程度。PCA 要做的事情是找出一个单位正交基使得样本投影到这些基向量上的方差依次递减。这里的关键是方差最大的方向保留了样本之间最大的差异而不同人的身份差异正是人脸识别里最重要的信息。最大几个特征值对应的特征向量就是前几个主成分。由于它们的形状也像人脸所以叫特征脸英文写作 eigenface。特征值衡量了对应方向上的能量特征值越大该方向上的变化越剧烈对人脸区分贡献也越大。2.3 特征脸与投影系数有了特征脸之后一张人脸图不再用 4096 个像素值描述而是用它在每个特征脸上的投影系数描述。投影就是内积对去均值后的图像向量 xc计算eigVecs * xc得到一个 k 维系数向量。训练集中的每一张图都会有一个系数向量测试图也做同样的投影然后找距离最近的训练图。距离可以用欧氏距离也可以用余弦相似度。这里要强调的是PCA 本身不是分类器它只负责把高维像素空间压缩到一个低维子空间。最终的分类决策需要最近邻、SVM 或其他分类器完成。很多人把这一整套流程笼统地叫“PCA 人脸识别”严格说应该是“PCA 特征提取 最近邻分类”。2.4 为什么用 SVD 而不是直接 eig如果直接对 4096×4096 的协方差矩阵做特征分解内存开销非常大。常见做法是改为对 XᵀXM×M 小矩阵做特征分解再由 V 恢复出 U。这个思路在数学上等价于对 X 做奇异值分解SVD。在 MATLAB 里可以这样写[U, S, V] svd(Xc, econ); eigVecs U(:, 1:k);S 是奇异值矩阵奇异值的平方与协方差矩阵的特征值成正比。用svd而不是直接对Xc * Xc做eig主要原因是 Xc 本身参与计算后特征向量不需要经过矩阵乘法放大数值稳定性更好。两者得到的特征向量方向可能相差一个正负号但不影响投影距离计算。下面这个表整理了两种方式的差异。对比项直接 eigSVD 方式计算对象C X Xᵀ / MX 本身或 XᵀX矩阵规模n×nn 为像素数m×mm 为训练样本数内存占用高低适用场景小图、样本多高维小样本的人脸数据集人脸识别数据集通常样本数只有几百而像素维度过万SVD 方式把大规模特征分解问题转成小规模矩阵计算几乎是这类项目唯一现实的选择。理解这一点后面看代码里的Xc * Xc就不会觉得奇怪了。3. MATLAB实现PCA人脸识别核心源码与关键参数3.1 准备数据集与图像预处理常见的数据集会按人的编号建文件夹比如faces/person1、faces/person2每个文件夹里放同一人的多张不同姿态、表情的灰度图。下面这段代码把这些图像读进来统一尺寸并转成 double 类型。% 读取按文件夹分类的人脸库faces/person1, faces/person2, ... dataDir faces; subdirs dir(dataDir); imgRows 64; imgCols 64; X []; labels []; for i 1:numel(subdirs) if ~subdirs(i).isdir || strcmp(subdirs(i).name, .) || strcmp(subdirs(i).name, ..) continue; end files dir(fullfile(dataDir, subdirs(i).name, *.pgm)); for j 1:numel(files) img imread(fullfile(dataDir, subdirs(i).name, files(j).name)); img imresize(img, [imgRows, imgCols]); img double(img(:)); X [X; img]; % 每行一张图 labels [labels; i]; % 类别编号 end end X X; % 转置后每列是一个样本逻辑说明先用dir定位每个子目录再逐个文件读取。img(:)将二维图像变成列向量转置后拼成行向量。最后整体转置让 X 的第 i 列代表第 i 张训练图和前面 2.1 的约定保持一致。labels 的取值就是文件夹序号不需要手写标注。参数说明imgRows和imgCols如果设置不一致后面中心化、投影都会出现维度错误。ORL 原始图是 92×112imresize到 64×64 是为了降低计算量。如果你用的是分辨率更高的人脸门禁机截图建议至少缩放到 112×112保留眼角和嘴角附近的细节。3.2 自己写 PCA 函数下面是一个不依赖内置pca函数的最小实现适合用来理解算法本身。function [eigVecs, trainCoef, meanFace] myPca(X, k) meanFace mean(X, 2); % 平均脸 Xc X - meanFace; % 去均值 L Xc * Xc; % 小矩阵大小是样本数×样本数 [V, D] eig(L); [~, idx] sort(diag(D), descend); V V(:, idx); eigVecs Xc * V; % 恢复高维空间中的特征向量 eigVecs eigVecs ./ vecnorm(eigVecs, 2, 1); % 单位化 eigVecs eigVecs(:, 1:k); % 只保留前 k 个特征脸 trainCoef eigVecs * Xc; % 训练集的投影系数 end这里要注意eig返回的特征向量默认按特征值升序排列必须用idx重新排序。vecnorm是 R2017b 之后才有的函数如果还在用更老的 MATLAB 版本可以用bsxfun(rdivide, eigVecs, sqrt(sum(eigVecs.^2, 1)))替代。单位化不能省否则特征向量的尺度会影响投影系数进而影响距离比较。3.3 最近邻分类与距离度量训练完成后测试阶段只需要把新图像投影到特征空间再与所有训练样本的系数比较距离。function pred recognize(img, eigVecs, trainCoef, meanFace, labels, imgRows, imgCols) img imresize(img, [imgRows, imgCols]); xc double(img(:)) - meanFace; coef eigVecs * xc; dist sum((trainCoef - coef).^2, 1); [~, best] min(dist); pred labels(best); end说明trainCoef是 k×Mcoef是 k×1相减后按列求和得到每个训练样本的欧氏距离平方。min返回的best是局部索引与 labels 中的位置一一对应。如果想把欧氏距离换成余弦相似度可以这样写cosSim trainCoef * coef ./ (vecnorm(trainCoef, 2, 1) * norm(coef));然后取最大值。欧氏距离对图像整体亮度变化更敏感余弦距离更关注方向所以在光照变化明显的场景下建议对比两种度量。3.4 关键参数维度 k 和图像分辨率参数选择直接影响识别率下面这张表是实践中的参考值。参数推荐范围说明训练样本数每人 5~10 张少于 5 张时 PCA 统计不够稳定图像分辨率32×32 ~ 112×11264×64 是性价比比较高的选择PCA 维度 k10 ~ 100通常保留 90%~95% 能量距离度量欧氏 / 余弦光照变化大时选余弦能量比例的判断方式是把特征值从大到小排序选择最小的 k让前 k 个特征值之和占所有特征值之和的 95%。在 MATLAB 里可以写成cumsum(diag(D), reverse) / sum(diag(D))第一个超过 0.95 的位置就是 k。这个 k 一般不会超过 60因为人脸的总体变化维度有限再往上加多半是在拟合噪声。4. GUI界面把PCA识别流程封装成可操作工具4.1 GUIDE 已经被 App Designer 取代很多网上下载的旧源码打开后会有.fig文件双击把手动拖动到布局编辑器这属于 GUIDE 时代。MATLAB 官方从 R2016a 开始主推 App DesignerR2020a 之后的版本已经不建议再用 GUIDE。新写的 GUI 我建议直接使用 App Designer它生成的代码是面向对象的 classdef虽然初期上手比 GUIDE 慢一点但回调管理更清晰也不容易出现 handles 更新丢失的问题。界面布局上只需要三个控件一个坐标轴用来显示测试图一个按钮触发训练一个按钮选择待识别图片并显示结果标签。4.2 App Designer 的最小回调结构在 App Designer 设计视图中拖好组件后组件名会作为属性出现在类里。下面是最小可运行的示意代码。classdef FaceRecognitionApp matlab.apps.AppBase properties (Access public) UIFigure Axes TrainButton TestButton ResultLabel model % 结构体保存特征脸、投影系数、均值脸 end methods (Access private) function onTrain(app, ~) [eigVecs, trainCoef, meanFace] myPca(X, 30); app.model struct(eigVecs, eigVecs, ... trainCoef, trainCoef, ... meanFace, meanFace); app.ResultLabel.Text 训练完成; end function onTest(app, ~) [file, path] uigetfile({*.jpg;*.png;*.pgm, 人脸图片}); if isequal(file, 0), return; end img imread(fullfile(path, file)); pred recognize(img, app.model.eigVecs, ... app.model.trainCoef, app.model.meanFace, ... labels, 64, 64); app.ResultLabel.Text [识别结果: 类别 , num2str(pred)]; end end end对应关系如下表控件回调函数作用TrainButtononTrain训练 PCA 模型TestButtononTest选择图片并识别Axes无显示测试图或特征脸ResultLabel无显示识别结果文本说明这里假设 X 和 labels 在外部已经加载实际项目中可以把训练数据加载也放进类里避免污染全局空间。回调函数写在 methods 里参数列表的第一个 app 自动由框架传入。注意属性名不能和回调函数名重复。4.3 在坐标轴中显示平均脸和特征脸function showFace(app, vec, rows, cols) imagesc(app.Axes, reshape(vec, rows, cols)); axis(app.Axes, image); colormap(app.Axes, gray); end把平均脸传到这个函数里会显示一张模糊的人脸轮廓把eigVecs的第 1 列传进去会显示第一特征脸。前几张特征脸通常对应光照方向和整体明暗从第 5 张开始才能看出明显的五官轮廓。这一步可以帮助你快速判断 PCA 训练是否成功如果特征脸看起来像纯噪声说明没有做去均值或者训练图像没有对齐。4.4 打包与文件路径问题如果要把这个 GUI 分享给其他同事常见做法是使用 MATLAB Compiler 的deploytool打包成独立程序。打包后的工作目录和开发时的 MATLAB 当前目录不一定相同依赖相对路径的数据文件会找不到。解决办法是在代码开头写projectRoot fileparts(mfilename(fullpath));然后所有路径都基于projectRoot拼接。另外图片文件夹要一并放进部署包不能只打包.m文件。更稳妥的做法是把训练集和模型参数全部存成.mat文件GUI 启动时load省去首次运行时的 PCA 训练时间。如果只是为了演示也可以把 16 个特征脸用montage拼在一张图里输出效果比连续翻页直观适合写进说明文档。5. 说明文档之外的排错PCA人脸识别常见崩溃点与优化手段5.1 矩阵维度不一致的第一排查顺序在 MATLAB 里跑 PCA 人脸识别最常遇到的就是矩阵维度不匹配。首先在训练前打印size(X)确认第二个维度是样本数比如 4096×200。如果打印出来是 200×4096后面的mean(X,2)就会给出 200×1 的均值和 4096 维的图像相减时直接报错。第二点检查读取图像时是否统一转了 doubleuint8 类型在做减法时可能溢出虽然不一定报维度错误但结果会很奇怪。第三点检查测试图片是否和训练图片用了同一个imresize尺寸尺寸不同会导致投影矩阵的列数不匹配。5.2 特征脸对光照敏感直方图均衡化是低成本解PCA 特征脸的性能在光照变化大的数据集上会明显下降。最直接的优化是在读入每张图后加一行img histeq(img);。直方图均衡化会重新分配像素灰度级让过曝和过暗的图片灰度分布更接近。代价是压缩对比度对暗光环境下的细节有一定损失。实际项目里我一般会把均衡化和不均衡化各跑一次用识别率决定是否保留。如果是彩色图应该先转灰度再均衡化否则三个通道分别均衡会导致严重的颜色失真。img rgb2gray(img); % 彩色转灰度 img imresize(img, [64 64]); img histeq(img); % 直方图均衡化另一个经常被忽略的问题是人脸对齐。PCA 假设所有人脸的眼睛位置大致相同ORL 数据集已经做过对齐但自己采集的照片往往存在偏移、旋转和缩放。如果训练集里人脸没有对齐特征脸会同时编码位置变化和身份变化识别率急剧下降。最简单的缓解方式是手动标定双眼坐标再用仿射变换把眼睛对齐到固定位置。这一步对识别率的提升通常比改变 k 更明显。5.3 用内置 pca 函数时的双中心化问题MATLAB 从 R2012b 开始提供pca函数很多新代码直接用[coeff, score, ~, ~, explained] pca(X, NumComponents, k)。但内置pca默认把数据中心化了也就是内部会执行 X - mean(X)。如果在外层又手动减掉均值就会得到双重中心化数据特征向量和投影系数都会偏差。检查方法看 X 的每一列均值是不是已经接近 0。如果已经接近 0调用pca时要设置Centered, false或者干脆把去均值代码删掉。另外要注意pca函数要求输入矩阵每列是一个观测值每行是一个变量这与前面第 3 章的约定相同。5.4 排错对照表现象可能原因处理方式维度不一致X 方向放反检查 size(X) 后转置特征脸像噪声没去均值或图片未对齐检查 meanFace对齐人脸识别率低于 70%k 选得过大或过小绘制 k-识别率曲线GUI 按钮无响应回调函数名不对查看命令窗口堆栈打包后找不到文件相对路径失效用 mfilename 定位路径排错时不要一次改多个参数每次只改一个再跑一遍交叉验证否则很难定位是谁导致的变化。6. 用交叉验证和重构误差验证PCA模型的泛化能力6.1 用 cvpartition 做 K 折交叉验证别只跑一次训练集/测试集就写进结论。把人脸库分成 5 折每一折轮流做测试集其余做训练集最后统计平均识别率这种做法的结论要可信得多。MATLAB 里cvpartition可以按类别比例划分保证每一折里每个类别出现的样本数大致相同。cv cvpartition(labels, KFold, 5); acc zeros(cv.NumTestSets, 1); for f 1:cv.NumTestSets trIdx cv.training(f); teIdx cv.test(f); [eigVecs, trainCoef, meanFace] myPca(X(:, trIdx), 30); correct 0; for t find(teIdx) pred recognize(X(:, t), eigVecs, trainCoef, meanFace, labels(trIdx), 64, 64); correct correct (pred labels(t)); end acc(f) correct / sum(teIdx); end mean(acc)注意特征空间只用训练集计算测试集在前面的投影过程中不参与任何训练步骤否则识别率会虚高。这是交叉验证里经常被忽略的细节也是论文里容易被审稿人追问的点。6.2 重构误差曲线确定 k 下界除了用识别率选 k还可以用重构误差从另一面验证。对某张测试图 x重构图像是eigVecs * (eigVecs * xc) meanFace。随着 k 增加重构误差必然下降但下降速度会明显变慢。这个拐点对应的 k 表示再增加维度只能重建细节和噪声对身份判别帮助不大。把误差曲线和识别率曲线放在同一张图里看可以让你对模型容量有直观感受。xc double(img(:)) - meanFace; rec eigVecs * (eigVecs * xc) meanFace; mse mean((double(img(:)) - rec).^2);注意 rec 已经加回 meanFace所以直接用原始图像像素值做差不需要像某些博客写的那样再减一次均值。6.3 将 CMC 曲线作为补充评估CMC 曲线回答的问题是正确身份出现在分类器前 N 个候选中。实现时对每个测试样本计算与所有训练样本的距离把类别号按距离升序排列找到正确标签所在的位次。把所有测试样本的位次统计成直方图再做累计即可。这个指标适合门禁、考勤等不允许误拒的场景比单一识别率信息更丰富。[~, order] sort(dist, ascend); r find(order teLabelIdx, 1); % teLabelIdx 是正确身份在训练标签中的索引 rankStat(r:end) rankStat(r:end) 1;把所有 rank 循环累加后除以测试样本数再画出来就是标准的 CMC 曲线。本文还有配套的精品资源点击获取