基于Matlab的PCA人脸识别系统:原理、实现与源码解析
2026/9/21 18:51:36 网站建设 项目流程

基于 Matlab 的 PCA 人脸识别系统,是很多图像处理类课程和毕业设计里最先接触的方案。它用主成分分析把每一张人脸图片压缩成一组低维特征,再通过最近邻匹配去判断测试人脸属于训练库里的哪一个人,整个过程不依赖深度学习框架,也不需要 GPU,普通教室电脑就能跑。我最近拿到一套这类项目的完整源码,编号是源码37期,正好可以把环境、数据、训练、识别、批量导出这些环节完整拆开讲。如果你是正在做 Matlab 作业、准备毕设,或者打算把 PCA 作为论文对比方法,这篇内容值得先看完再动手。

人脸识别的核心并不神秘:把输入图片变成一个向量,然后和数据库里的已知向量比较。比较的准确性,取决于向量怎么构造、用什么距离、参数怎么选。下面会按实际落地顺序,把这套系统的代码逻辑、运行条件和常见坑点逐个说清楚。

1. 这套源码解决的三个核心问题

1.1 人脸识别任务到底在识别什么

人脸识别不是让电脑看“脸长什么样”,而是把输入图像变成向量,再和数据库里的已知向量比较。比较结果是否可靠,取决于两个问题:怎么把图像变成有区分度的向量,以及怎么比较两个向量是否属于同一个人。PCA 解决前一个问题,最近邻解决后一个问题。

在 Matlab 的 PCA 人脸识别系统里,输入通常是一组灰度图像训练集。假设每张图像是 64×64 像素,展开后就是一个 4096 维的向量。如果直接在原始向量空间里比较,会有两个麻烦:维度太高导致计算量大,像素之间存在大量冗余,光照、背景变化都会干扰距离计算。PCA 做的事情,就是把这 4096 维投影到一个低维子空间,通常是几十维,同时尽量保留样本之间的差异。

所以理解这套源码,要先建立一条主线:先准备训练矩阵,再计算投影矩阵,然后把所有训练样本投影,最后对测试样本做同样的投影并计算距离。整个代码并没有多高深,就是把这条主线按顺序实现了。

1.2 为什么选择 PCA 而不是深度学习

很多同学在选课程题目的时候都会问,人脸识别现在不都用深度学习吗,为什么还要用 PCA。原因是场景不同。深度学习需要大量数据、较长训练时间,并且对可解释性要求不高时才划算。课程作业、本科毕业设计和算法对比实验里,数据通常只有几类几十张,硬件也没有 GPU,这时候 PCA 更有优势。

PCA 不需要训练网络,只需要一次特征分解。它的每一步都可以用矩阵运算表达,老师问起来也容易讲清楚。如果论文需要做“传统方法 vs 深度学习”的对比实验,PCA 往往是那个经典的基线方法。另外,Matlab 里做矩阵操作非常接近数学表达,不像 Python 那样需要额外处理数据类型和库版本,所以用 Matlab 展示 PCA 流程会更直观。

1.3 谁适合使用这套源码

如果你是下面这几类情况,这套源码可以当作一个很好的起点。

  • 正在做数字图像处理、模式识别课程作业,需要一个能运行、能解释的人脸识别系统。
  • 正在准备毕业设计,想在已有代码上扩展,比如增加特征提取方法、改变分类器。
  • 写论文时需要一个经典算法作为基线,和 SVM、卷积神经网络做对比。
  • 需要做二次开发,把单张图片识别改成批量识别,或加入 GUI 界面。

如果你对矩阵运算不熟,也不用担心,下面会把每个变量的维度和含义都标出来。只要按步骤操作,跑通一套基础版本是没问题的。

2. 运行环境与数据准备

2.1 环境要求

源码是在 Matlab 里完成的,所以第一件事是确认本机安装的 Matlab 版本。常见版本比如 R2021b、R2022b 都能运行这套逻辑,关键不是版本号,而是确认安装的工具箱。至少需要 Image Processing Toolbox,里面提供 imread、imresize、rgb2gray 等函数。如果希望直接使用 pca 函数,还需要 Statistics and Machine Learning Toolbox。

原始材料没有给出明确的版本要求,所以落地时第一件事是在命令行运行:

ver

查看是否有对应的工具箱。如果发现缺少工具箱,最简单的做法是不使用 pca 函数,改用 Matlab 自带的 eig 函数做特征分解,代码量只多几行,后面会给出写法。

硬件方面,PCA 人脸识别的常见实验集是 ORL 或 Yale,图片数量在几十到几百张,尺寸也小。普通 8GB 内存的电脑就够用。要注意的是,如果图像尺寸设置成 512×512,训练集又有几百张,特征矩阵会一下变成几十万维,内存占用会明显上升,这个心理预期要有。

2.2 数据集的目录结构

代码要跑通,最关键的不是算法本身,而是数据集结构是否清晰。建议采用如下目录:

FaceDataset/ s01/ 1.pgm 2.pgm 3.pgm s02/ 1.pgm 2.pgm 3.pgm ... TestImages/ test_s01_1.pgm test_s02_1.pgm

这样做的原因有两个。第一,Matlab 可以用 dir 递归读取子文件夹,按照人员编号做标签,不需要手动维护一个标签表。第二,训练集和测试集分开之后,避免把同一张图片既用于训练又用于测试,否则识别率会虚高。

如果是自建人脸数据,尽量让人脸在图片中处于居中位置,背景简单一点。不要指望 PCA 自动修正人头旋转和大角度侧脸,它没有这个能力。图片拍摄时最好在光照相近的环境下采集,至少在训练集和测试集里保持大致一致。

2.3 所有图像必须统一尺寸并转成灰度

这是源码跑通前最容易忽略的一步。PCA 要求每一张图片展开成向量后长度一致,否则训练矩阵都无法构造。比如训练集第一张图是 64×64,第二张图是 100×80,展开后向量维度分别是 4096 和 8000,Matlab 在拼接矩阵时会直接报错。

所以读入每一张图后,必须先做三件事:如果图像是 RGB 三通道,先转成灰度;统一成相同尺寸;把 uint8 类型转成 double 再计算。这里有一个常见误区:有人希望保留 RGB 彩色信息,直接把每个通道也展开。这样并不是不行,但维度会膨胀很多,而且得到的特征就不是经典意义上的“特征脸”了。第一版建议先用灰度图跑通。

对应代码可以写成:

function imgVec = loadImageAsVector(imgPath, rows, cols) img = imread(imgPath); if size(img, 3) == 3 img = rgb2gray(img); end img = imresize(img, [rows, cols]); imgVec = double(img(:))'; end

如果你从不同文件夹读取 png、jpg、pgm 混合格式,优先统一转成 double 矩阵,而不是在保存时互转格式。格式转换过程本身可能引入压缩损失,但影响一般不大,真正的风险是尺寸不一致。

3. PCA 实现的核心流程拆解

3.1 构造训练矩阵并去除均值

把每一张训练图像展开为一个行向量,假设训练样本数是 N,每个向量的长度是 D,那么训练矩阵 X 的大小就是 N×D。在 ORL 数据集中,N 往往取 200 到 400,D 取 4096 或更小。

之后计算所有训练向量的均值,得到一个 1×D 的平均脸。去均值的过程就是让所有样本围绕原点分布,这是 PCA 的关键步骤。有人会把这一步省略,直接对原始矩阵计算协方差,结果仍然能跑,但含义会变,而且第一个主成分会被均值主导,导致识别效果很差。

X = double(trainMatrix); % N x D meanFace = mean(X, 1); Xc = X - meanFace; % 中心化

调试时可以把平均脸显示出来:

figure; imshow(uint8(reshape(meanFace, [rows, cols])));

如果平均脸看起来像一张清晰的人脸轮廓,说明数据读取和向量化逻辑基本正确。如果平均脸看起来杂乱,大概率是图片读取顺序或尺寸转换出了问题。

3.2 用小矩阵计算特征脸

PCA 的中心是求解协方差矩阵的特征向量。协方差矩阵的数学形式是 Xc' * Xc,大小为 D×D。当 D 很大时,比如 4096×4096,直接求特征分解依然能算,但更高效的方法是借助一个事实:只需要保留前 k 个主成分,而样本数 N 远小于 D 时,可以转换为求解较小的 N×N 矩阵。

具体做法是令一个小矩阵:

covSmall = Xc * Xc'; % N x N [V, D] = eig(covSmall);

这里的特征向量 V 是 N×N,并不是像素空间里的特征脸。要还原成像素空间的特征脸,需要继续乘上转置的中心矩阵:

eigenfaces = Xc' * V; % D x N,每一列是一个特征脸

这个技巧在 Matlab 里写起来并不复杂,但要理解为什么。它减少了内存占用,也避免了对超大协方差矩阵直接做特征分解时可能出现的不稳定。如果样本数比像素数大,比如几千张图片,这时直接用 pca 函数会更省事。

得到特征脸后,还需要按特征值从大到小排列,再取前 k 列。因为 eig 函数返回的特征值不保证有序。

eigvals = diag(D); [~, idx] = sort(eigvals, 'descend'); eigenfaces = eigenfaces(:, idx); W = eigenfaces(:, 1:k);

3.3 训练投影与测试投影

有了投影矩阵 W,维度是 D×k,接下来把训练集投影到低维空间。由于 W 的每一列理论上可以归一化,投影就是:

trainProj = Xc * W; % N x k

测试图像的处理路径和训练时完全一致:先转灰度、统一尺寸、展开成向量,然后减去平均脸,再乘以 W:

testVec = loadImageAsVector(testPath, rows, cols); testVec = testVec - meanFace; testProj = testVec * W; % 1 x k

此时可以用欧氏距离计算与所有训练样本的距离:

distances = sqrt(sum((trainProj - testProj).^2, 2)); [~, idx] = min(distances);

idx 对应的训练样本标签,就是系统判断出的身份。

这里要强调的是,投影矩阵 W 只依赖训练集计算一次。测试阶段不要重新计算 W,否则会造成信息泄漏,让结果失真,也违背真实部署场景。

3.4 完整流程的伪代码结构

如果要把这个流程封装成一个主脚本,推荐下面这种结构:

% 1. 读取训练图像,构建 trainMatrix % 2. 计算 meanFace、中心化 Xc % 3. 计算小矩阵特征分解,得到 W % 4. 计算 trainProj % 5. 遍历测试文件夹,对每张图调用相同预处理 % 6. 计算距离,最近邻分类 % 7. 输出识别结果和准确率

这种结构比把全部代码塞进一个文件更容易调试,也方便后续改成批量处理或 GUI。后续章节会逐步展开。

4. 关键参数与判断标准

4.1 主成分数量 k 怎么选

k 是这套源码里最重要的参数。k 太小,会丢失有用信息;k 太大,会引入噪声,并且计算量变大。常见的经验选择有两种。

第一种是累计贡献率法。计算每个主成分对应的特征值占总特征值的比例,然后取前 k 个,使累计贡献率达到 95% 或 99%。Matlab 里可以用 cumsum 快速画图:

explained = cumsum(eigvals) / sum(eigvals); plot(explained);

第二种是实验法。把训练集内部再划分出一部分验证集,扫描 k 从 5 到 50,画出识别率曲线,选择曲线最高且稳定的区间。对课程作业来说,这种方法更容易和导师解释。不要直接选择最大 k,因为最后几个主成分多是噪声。

在实际项目里,k 的合理范围往往和样本数有关。训练样本总数为 N 时,k 最大只能取到 N-1,因为去均值后数据的秩最多是 N-1。如果你发现无论如何调 k,识别率都上不去,先检查训练样本是不是太少。

4.2 距离阈值和未知人脸判定

很多项目只做“类内判别”,也就是测试图像一定来自训练集中的人。但实际场景里可能存在陌生人,例如门禁系统要拒绝未知用户。这时需要设置距离阈值:如果最小距离小于阈值,判定为已知人脸;否则判定为未知。

阈值可以这样估计:先把训练数据用留一法或交叉验证计算每个样本的最小距离,统计这些距离的均值和标准差,然后设定为均值加上 2 倍标准差。这是经验方法,不是唯一标准。更稳妥的做法是额外采集一些负样本,在负样本上计算距离,再根据误拒率和误纳率选择阈值。源码第一版可以先写死一个阈值,后面再根据数据调整。

4.3 距离度量的影响

最常用的距离是欧氏距离,因为它与 PCA 的降维特性比较贴合。但还有两个变体值得比较。

距离方法对幅值变化是否敏感适用场景实现难度
欧氏距离敏感光照稳定的经典实验
余弦相似度不敏感图片亮度整体变化较大
马氏距离不敏感特征各维度方差差异大较高

如果某人脸图像整体变暗,像素值整体缩放,欧氏距离可能变大,但余弦相似度变化更小。在 Matlab 里实现余弦相似度并不复杂,把投影向量归一化后点乘即可:

sim = (testProj / norm(testProj)) * (trainProj' ./ vecnorm(trainProj'));

建议不要一次性把所有距离度量都塞进代码。先跑欧氏距离版本,确认系统能工作,再做对比实验,记录不同距离下的准确率。

5. 批量识别、结果可视化和论文输出

5.1 从单张图片识别改成批量识别

训练完成之后,如果测试集是一个文件夹,可以用 dir 一次性读取测试图片。需要留意,dir 返回的结果包括 . 和 ..,要过滤掉非文件项。

testFolder = 'TestImages'; files = dir(fullfile(testFolder, '*.pgm')); for i = 1:length(files) testPath = fullfile(testFolder, files(i).name); [~, name, ext] = fileparts(files(i).name); predLabel = recognize(testPath); fprintf('%s -> %s\n', [name ext], predLabel); end

批量场景里最重要的是输出命名规范。如果测试图片文件名本身带有真实标签,比如 test_s01_1.pgm,就可以直接从文件名解析出真实标签,统计整体识别率。如果文件名没有规则,可以单独准备一个 csv 文件,记录真实标签和数据顺序。

批量跑的时候还要考虑失败重试。如果某张图读取失败,不要让整个脚本中断报错。可以用 try-catch 把异常记录到日志里:

try predLabel = recognize(testPath); catch ME fprintf('错误: %s -> %s\n', testPath, ME.message); continue; end

这样批量任务能持续运行到最后,排查时也只需要看失败列表。

5.2 把识别结果展示成图

为了让结果更适合放进报告,可以生成一个合成图,原图放在左边,预测标签写在图片上方,同时显示最小距离。一般做法是遍历测试图像后,用 subplot 排列:

figure; for i = 1:cols subplot(2, cols, i); imshow(testImages{i}); title(sprintf('预测: %s', predLabels{i})); end

保存图片时建议使用 exportgraphics 或 print,避免直接截屏造成分辨率不够。论文里更常用的是表格:每一行是测试样本、真实标签、预测标签、距离、是否正确,最后再汇总识别率。

5.3 论文里怎么描述 PCA 流程

写论文时,PCA 人脸识别通常按这几段写:问题定义,算法流程,实验设置,实验结果和对比。算法流程部分最好画一张流程图,把训练和测试两条分支画清楚:训练分支是“预处理 -> 灰度化 -> 向量化 -> 中心化 -> 特征分解 -> 投影”,测试分支是“预处理 -> 向量化 -> 中心化 -> 投影 -> 距离计算”。

这也解释了为什么在前面要把训练函数和测试函数拆开,因为论文里的逻辑和代码逻辑应当一致。实验部分要写明数据集从哪里来、每类使用多少张训练、测试多少张、图像尺寸、PCA 保留主成分数、分类器。不要只写“识别率达到了 90%”,要写明这是在什么条件下得到的,否则自己复现时都不知道该复现哪个参数。

6. 常见报错与排查顺序

6.1 图像尺寸不一致导致的 Reshape 错误

最常见的报错是使用数量不兼容的尺寸执行 reshape。遇到这种问题,先不要怀疑算法,而是检查两张图像的宽高。可以在读取循环里输出每个文件的大小:

info = imfinfo(fullfile(folder, files(i).name)); fprintf('%s: %d x %d\n', files(i).name, info.Width, info.Height);

如果发现目录里藏有非图片文件,比如临时文件或缩略图缓存,也会导致读取崩溃。所以 dir 过滤条件要严格,比如只读取 *.pgm 或 *.jpg,并且最好在代码开头统一检查所有文件是否都是有效图片。

6.2 内存不足或特征矩阵不可逆

当 D 太大或者 N 太大时,构造大矩阵会出现内存压力。如果报错提示内存不足,第一步是降低图像分辨率,比如从 112×92 降到 56×46。第二步是改用小矩阵方式或 SVD。在 Matlab 中,svd 通常比显式构造协方差矩阵更稳定,对中心化矩阵直接做 SVD 即可:

[U, S, V] = svd(Xc, 'econ'); eigenfaces = V(:, 1:k);

这个方案也避免了直接求 Xc' * Xc 出现奇异矩阵警告。如果仍然出现奇异值接近 0 的警告,说明训练样本数量太少,或不同样本过于相似,这时需要增加训练样本,而不是强行提高 k 值。

6.3 识别率低时先排查什么

识别率低是一个现象,原因通常不在最后一步的距离计算,而在前面的数据预处理。按照以下顺序逐个检查。

  • 检查训练集和测试集是否来自同一图像采集条件。如果训练是正常人脸,测试是强光或逆光,PCA 会明显失效。
  • 检查人脸裁剪区域是否一致。如果有些人脸在图中的位置偏差很大,需要先做人脸对齐,或者提高裁剪精度。
  • 检查数据划分。测试图像是否在训练集中出现过,如果出现过,识别率虚高;如果测试和训练图像拍摄差异过大,识别率又偏低。
  • 检查训练样本数。每类最好至少 3 到 5 张,如果每类只有 1 张训练图,再去识别同一个人的另一个姿态,很难得到稳定结果。

在这些都没问题的情况下,再考虑增加主成分数量或调整距离度量。

6.4 程序卡住或运行太慢

如果程序运行很长时间,先看是哪一步。读取图片很快,特征分解在样本少时也很快,通常慢的是循环里重复读取图片或重复计算投影矩阵。建议把所有预处理结果保存成 .mat 文件,第二次运行直接加载:

save('features.mat', 'W', 'meanFace', 'trainProj', 'labels');

这样能大幅减少重复处理时间。另一个常见瓶颈是使用双层循环计算每张图的距离,在样本较少时影响不大,但样本很多时建议矩阵化。

7. 边界、扩展和源码定制建议

7.1 这套方案的适用范围和边界

PCA 人脸识别在经典数据集上能取得不错效果,但它绝不是万能的。它对光照、姿态、遮挡和表情变化非常敏感。如果直接把训练数据从正面人脸换成侧脸,或者戴上口罩,识别率会明显下降。因此,在报告或项目说明里要写清楚系统的适用条件:正面人脸、光照稳定、基本无遮挡。

如果项目需求是固定摄像头、固定位置的小型门禁系统,使用 PCA 是可以接受的。如果需求是复杂环境下的大规模人脸识别,那应该考虑深度学习方案。这并不矛盾,文章里最好明确说明,免得被人误以为 PCA 是万金油。

7.2 从 PCA 扩展到 LDA、LBP 或 SVM

如果要做二次开发,或者论文里需要对比实验,可以从以下几个方向扩展。

  • PCA + LDA:先用 PCA 降维,再在降维空间做 Fisher 线性判别,经典改进方法。
  • LBP 直方图:提取局部纹理特征,对光照变化更鲁棒。
  • HOG + SVM:强调局部梯度信息,适合做人脸检测和分类。
  • 深度学习:使用预训练 CNN 提取特征,再接全连接层或 SVM,适合大数据量场景。

从源码角度看,你只需要保留“读取图像、预处理、划分训练测试集、计算识别率、输出结果”这些通用框架,把中间的特征提取模块和分类模块替换成新算法,就能快速做出多个对比实验。这个方法比每次把整个项目复制一遍再改要可靠,因为框架保持一致,实验结果才可对比。

7.3 拿到源码后第一件事不要改算法

最后给一个建议。拿到任何这类源码,第一件事不要急着改算法,而是先看数据读取和标签生成逻辑。关掉 GUI,去掉所有显示代码,在命令行跑一次训练和单张测试,确认结果可复现。然后把原始代码备份一份,再开始改参数。这样即使改乱了,也能回到原始状态。

二次开发时,给自己做一个简单的修改日志。比如今天把图像尺寸从 64×64 改成 96×96,识别率从 88% 变成 90%;明天把 k 从 30 改成 50,识别率又掉到 87%。没有日志,很快会忘记什么参数对应什么结果。论文或项目说明里如果能有这样的调参过程,反而是加分项。

我自己更建议先把单任务跑稳,再去考虑批量、接口和 GUI。很多人一上来就想要一个带界面的系统,但界面只是壳,里面的人脸预处理、PCA 投影和距离判断如果没理清楚,界面做得再漂亮也无助于识别。先把核心链路跑通,后续包装会很快。

这个方案真正落地时,最该盯住的不是功能列表,而是输入数据格式、资源占用和失败重试。数据格式决定了能不能训练,资源占用决定了能不能跑真实数据,失败重试决定了批量任务能不能持续运行。只要这三件事处理干净,后续扩展就很少会踩坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询