ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于MATLAB的PCA人脸识别系统从零实现与原理详解

2026/9/2 2:25:15 拓冰建站 浏览量
基于MATLAB的PCA人脸识别系统从零实现与原理详解 做图像处理课设或者毕业设计的同学很多人被人脸识别这个题目卡住过。不是不懂 PCA 原理而是看了不少理论讲解之后打开 MATLAB 依然不知道从哪行代码开始。更麻烦的是网上能跑通的源码虽然多但大多绑定着固定数据集、固定图片尺寸稍微换一个文件夹就报错即使把代码跑通了也说不清楚识别率为什么高、为什么低更不知道怎么写实验结论。先说一个核心判断PCA 本身并不是“人脸识别算法”它只是把人脸图像从高维像素空间压缩到低维特征空间的降维工具。真正完成“识别”动作的是分类器最常见也最直观的是最近邻分类器。很多初学者把 PCA 当成端到端的识别方法这是对人脸识别系统最常见的误解也是代码写不对、准确率上不去的真正原因。读完这篇文章你可以做到三件事第一把一套完整的 PCA 人脸识别程序在 MATLAB 中从零跑通第二看懂均值脸、特征脸、主成分数量、训练集/测试集划分这些关键概念第三在遇到报错或者准确率不理想时知道按什么顺序排查问题而不是盲目重装环境或者乱调参数。1. PCA 人脸识别系统这篇文章真正要解决的问题先明确一点什么人适合读这篇文章。如果你正在做“基于 PCA 的人脸识别系统”“人脸识别课程设计”“MATLAB 图像处理大作业”这类题目那这篇文章就是为你准备的。你需要的不是一两段 PCA 公式而是一套能运行、能改、能解释的系统。文章后面给的代码不是孤立片段而是一个完整的工作流数据集读取、图像预处理、PCA 训练、特征投影、最近邻分类、准确率评估。这套流程在 ORL 人脸库上通常能跑到 90% 以上的识别率。它的工程意义在于用普通 CPU 就能完成训练不需要 GPU不需要深度学习框架代码量也只有几百行。这对于理解模式识别的完整链路非常有帮助。另一个要解决的问题是“源码看懂了但不属于自己”的困境。不少同学从网上下载一份源码改个路径跑通后便不再动它。一旦老师追问“你的特征值是怎么求的”“为什么取前 30 个主成分”就回答不上来。这篇文章会把每段关键代码和算法步骤对应起来让系统真正成为你能解释、能改进的工程实践。当然这篇文章不解决工业级人脸识别问题。PCA 特征脸对光照、表情、遮挡和姿态变化比较敏感在高精度场景下远不如深度学习方法。这一点在选题和写论文时要有清醒认知后面也会展开说明。2. PCA 主成分分析与特征脸概念、原理与适用场景2.1 什么是 PCA 主成分分析PCAPrincipal Component Analysis主成分分析是一种无监督的线性降维方法。它的目标非常朴素把高维数据压缩成少数几个新维度同时尽量保留原始数据中的差异信息。把人脸识别放进来理解。一张 ORL 库中的灰度图是 112×92 像素按列展开后就是一个 10304 维的向量。如果直接把这样的高维向量拿去做分类不仅计算量大而且很多维度上的信息是冗余的。PCA 做的事情就是找到一组新的正交方向让数据在这些方向上的方差尽量大再把每张人脸图像投影到这些方向上。用通俗的话解释PCA 会找到“人脸图像之间最不一样的地方”。不同的人脸差异可能来自五官轮廓、肤色深浅、是否戴眼镜、光照方向等。PCA 把最主要的差异方向提取出来每一张人脸图都可以用“它在这些差异方向上的投影值”来表示。这个投影值就是降维后的特征。2.2 什么是特征脸PCA 得到的主成分向量如果还原成图像尺寸来看会得到一张类似人脸的灰度图这就是特征脸Eigenface。名字听起来有点玄但其实不复杂。训练集里所有脸的平均值叫均值脸代表所有人的共性PCA 提取出的特征向量代表相对于均值脸最显著的差异模式。把它们可视化之后会看到像“某种光影变化”“某种轮廓偏移”的图案。每张输入人脸都可以用这些特征脸的线性组合近似重构出来。特征脸的正式流程最早由 Turk 和 Pentland 在 1991 年提出这种方法也因此被称为 Eigenface 方法。它以数学简单、实现容易、可解释性强成为人脸识别入门教材中最经典的案例。即便今天深度学习已经统治人脸识别领域PCA 特征脸依然是理解特征提取与降维概念的最佳起点。2.3 小样本技巧为什么不对大矩阵做特征分解PCA 的核心数学操作是对协方差矩阵做特征值分解。这里有一个非常关键的工程问题。假设训练集有 n 张图每张图展成 d 维向量。常规做法是构造 d×d 的协方差矩阵然后对它做特征值分解。但在人脸识别里d 是图像像素数动辄上万n 往往只有几百。直接求 10304×10304 矩阵的特征向量内存占用非常大速度也很慢。解决办法是利用线性代数中的一个结论先对 n×n 的小矩阵做特征值分解再把得到的特征向量映射回原始空间。这个操作在 Eigenface 论文中就有论述也是许多 MATLAB 源码里让新手最困惑的一段。理解它就掌握了 PCA 训练的最后一个难点。2.4 PCA 与 LDA、深度学习的对比对比维度PCA 特征脸LDA线性判别分析深度学习CNN模型规模轻量CPU 秒级训练轻量需标签信息重量级通常需要 GPU数据量需求每类少量样本即可每类样本较多需要大量标注数据是否使用标签无监督只用图像本身有监督利用类别标签有监督端到端学习可解释性特征脸可直观可视化判别方向可解释特征抽象解释较难对光照、遮挡鲁棒性一般一般更好适合场景教学、课设、小型库小样本分类任务工业级识别、大规模库本节小结PCA 解决的是“如何用更少的维度表示一张人脸”而不是“如何判断这张脸是谁”。识别工作由分类器完成。把降维和分类分开理解是看懂整个系统的一把钥匙。3. 环境准备MATLAB 版本、工具箱与 ORL 人脸数据集3.1 MATLAB 环境要求本文示例代码不依赖深度学习工具箱也不需要额外的第三方包。只要安装 MATLAB R2018 及以上版本即可运行主要使用 Image Processing Toolbox 中的imread、imresize、imshow等函数。如果你的 MATLAB 版本较旧某些文件读取 API 可能略有差异但对本文示例影响不大。代码中会尽量采用较通用的函数写法。3.2 数据集选择ORL 人脸库ORL 人脸库现在通常称为 ATT 人脸库是学术领域最常用的公开人脸数据集之一。它包含 40 个人每人 10 张灰度图像共 400 张图片原始尺寸为 112×92格式为.pgm。这个数据集的优点是规模适中、版权要求宽松、非常适合教学和实验。下载解压后目录结构应该类似下面这样ORL/ ├── s1/ │ ├── 1.pgm │ ├── 2.pgm │ └── ... ├── s2/ │ ├── 1.pgm │ ├── 2.pgm │ └── ... ├── s3/ └── ...如果你没有找到现成的 ORL 库也可以用自己的照片建立一个小型数据集。方法是把每个人的多张人脸照片裁剪、缩放为相同尺寸命名存放到单独的文件夹中。这类自建数据集对代码验证同样有效。需要提醒的是人脸数据属于敏感个人信息。实验时请优先使用公开的学术数据集不要随意采集或传播未授权的人脸照片。3.3 数据集正确的存放方式在运行代码之前把刚才的 ORL 文件夹放在与.m脚本相同的路径下或者把脚本中的dataDir改为数据集所在绝对路径。例如dataDir D:/MATLAB_Projects/ORL;这样做的原因是后续代码需要通过dir(fullfile(dataDir, s*))来遍历目录路径写错时程序不会报“文件不存在”而是得到空目录导致训练矩阵为空后面所有步骤都会失败。所以第一步的正确性很重要。4. 图像预处理与数据读取从 PGM 文件到矩阵4.1 为什么需要预处理人脸识别的输入是图像而 PCA 模型的输入是矩阵所以需要先把每张图片转换成一个列向量。但这中间还有几个细节问题第一数据集中的图像尺寸必须一致。PCA 对输入向量维度非常敏感训练时用的 112×92测试时如果来了一个不同尺寸的图片投影矩阵的维度就会不匹配程序直接报错。第二数据格式必须统一。imread读进来的是uint8类型取值范围 0 到 255。如果直接用uint8做均值或协方差计算会出现溢出或精度丢失导致结果完全错误。一般在计算前要先转成double类型。第三如果数据集包含彩色图像需要先转灰度。ORL 库本身就是灰度图不需要这步但代码里保留判断逻辑会让程序更通用。4.2 数据读取函数代码下面这个函数用于遍历数据集目录把所有图像读取为相同尺寸的列向量并返回对应的类别标签。% 文件路径load_face_data.m function [X, labels] load_face_data(dataDir, imgSize) % dataDir : 数据集根目录例如 ORL % imgSize : 统一图像尺寸例如 [112, 92] % X : 图像矩阵每一列是一张图像展开后的向量 % labels : 每列对应的类别编号 classDirs dir(fullfile(dataDir, s*)); numClass length(classDirs); imgH imgSize(1); imgW imgSize(2); X []; labels []; for i 1:numClass files dir(fullfile(classDirs(i).folder, classDirs(i).name, *.pgm)); for j 1:length(files) img imread(fullfile(files(j).folder, files(j).name)); img imresize(img, [imgH, imgW]); % 统一尺寸 if size(img, 3) 3 img rgb2gray(img); % 彩色图转灰度 end X(:, end1) double(img(:)); % 展成列向量并转 double labels(end1) i; end end end这里的核心逻辑有两点。一是img(:)把二维图像矩阵按列展开成列向量也就是把 112×92 的图像变成 10304×1 的向量。这样一张图就成了特征空间中的一个点。二是double(img(:))的类型转换。ORL 库的灰度图读进来是uint8不转换的话后面的mean和矩阵乘法会出现数据溢出的风险。虽然 MATLAB 有时会自动转换某些运算类型但显式转换永远是更可控的习惯。4.3 验证读取结果调用这个函数后可以快速验证数据是否读取正确[X, labels] load_face_data(ORL, [112, 92]); disp(size(X)); % 期望输出10304 400 disp(unique(labels)); % 期望输出1 到 40如果size(X)输出的行数不是 10304说明某些图像没有成功缩放成 112×92。如果列数不是 400说明数据集的图片数量和你预期不一致。此时应优先检查数据集目录结构和文件格式而不是继续往下跑。5. PCA 训练核心代码均值脸、协方差矩阵与特征空间5.1 训练流程梳理PCA 训练通常可以分为五步计算所有训练样本的均值脸本质是对每一行像素求平均。用原图像减去均值脸得到中心化数据保留每个人的“个性化差异”。对中心化数据的协方差矩阵做特征值分解。选择最大的前 k 个特征值对应的特征向量并做单位归一化。用这些特征向量组成投影矩阵把训练样本投影到低维空间。下面代码把这几步封装成一个函数。它可以直接调用上一章的load_face_data.m所以训练和读取是解耦的方便后续更换数据集。% 文件路径pca_train.m function [eigenfaces, meanFace, projectTrain, labels] pca_train(dataDir, imgSize, numComponents) % 读取全部图像 [X, labels] load_face_data(dataDir, imgSize); numImgs size(X, 2); % 1. 均值脸 meanFace mean(X, 2); % 2. 中心化 Xc X - meanFace; % 3. 小样本技巧对 n×n 矩阵做特征值分解 % 直接求 Xc * Xc 是 d×d 矩阵d 为图像维数计算代价很大 % 先求 Xc * Xc 的 n×n 矩阵特征向量再映射回原始空间 L Xc * Xc; [V, D] eig(L); [~, idx] sort(diag(D), descend); V V(:, idx); % 特征向量映射回原始空间并归一化 eigenfaces Xc * V; for i 1:size(eigenfaces, 2) eigenfaces(:, i) eigenfaces(:, i) / norm(eigenfaces(:, i)); end % 4. 只保留前 numComponents 个主成分 numComponents min(numComponents, numImgs - 1); eigenfaces eigenfaces(:, 1:numComponents); % 5. 训练样本投影 projectTrain eigenfaces * Xc; end这段代码里最难理解的是第 3 步。直接对Xc * Xc做特征值分解得到的矩阵是 10304×10304在普通电脑上非常吃力而Xc * Xc只有 400×400特征值分解秒级完成。两者的特征向量存在对应关系如果V是Xc * Xc的特征向量矩阵那么Xc * V的列向量就是原始协方差矩阵的特征向量方向。这是 PCA 在人脸识别领域最经典的小样本优化技巧。理解这一点再看网上很多源码就不会觉得那一步“莫名其妙”了。5.2 特征脸可视化训练完成后把前几个特征向量还原成图像可以直观地看到特征脸figure; numShow min(16, size(eigenfaces, 2)); for i 1:numShow subplot(4, 4, i); imshow(reshape(eigenfaces(:, i), imgSize), []); title(sprintf(特征脸 %d, i), FontSize, 10); end如果训练成功你会看到一组类似人脸轮廓但带有明显光照变化和结构差异的灰度图。这些图并不是某个具体的人而是 PCA 从所有训练人脸中总结出的“差异模式”。如果看到的是杂乱噪声先检查预处理环节是否漏掉了中心化或类型转换。6. 最近邻人脸识别测试图像分类的完整流程6.1 为什么用最近邻分类器PCA 降维之后每张人脸图变成一个 k 维向量。分类问题变成给定一个测试向量判断它和哪个训练样本最相似。最近邻Nearest Neighbor是最直观的选择。它把测试向量与所有训练样本投影向量逐一计算欧氏距离取距离最小的那一个训练样本的类别作为识别结果。这个分类器不需要训练参数也没有调参负担非常适合作为 PCA 人脸识别系统的默认分类器。如果你想把系统做得更高级可以用 SVM、决策树等分类器替代最近邻但那些分类器会引入自己的训练参数和调优成本。在入门阶段先跑通最近邻再逐步替换是更稳妥的路线。6.2 单张图片识别函数% 文件路径pca_recognize.m function [label, distMin] pca_recognize(testImg, eigenfaces, meanFace, projectTrain, labels, imgSize) % testImg : 输入的测试图像三维数组或二维灰度图均可 % eigenfaces : PCA 训练得到的特征脸矩阵 % meanFace : 均值脸 % projectTrain : 训练样本在特征空间的投影 % labels : 训练样本类别标签 % imgSize : 和训练时一致的图像尺寸 % 预处理测试图像 img imresize(testImg, imgSize); if size(img, 3) 3 img rgb2gray(img); end testVec double(img(:)); % 中心化并投影 testVec testVec - meanFace; projectTest eigenfaces * testVec; % 欧氏距离最近邻 dist sum((projectTrain - projectTest).^2, 1); [distMin, idxMin] min(dist); label labels(idxMin); end注意这个函数内部完整复现了测试图像的预处理流程。很多初学者在训练时设置了图像尺寸测试时却忘了缩放导致“矩阵维度不一致”的报错。把imresize和rgb2gray放在识别函数内部可以避免这类问题。6.3 主程序与完整运行流程% 文件路径main_pca_face.m clear; clc; close all; dataDir ORL; imgSize [112, 92]; numComponents 30; % 1. 训练 PCA 模型 [eigenfaces, meanFace, projectTrain, labels] pca_train(dataDir, imgSize, numComponents); % 2. 保存模型方便下次直接加载识别 save(face_pca_model.mat, eigenfaces, meanFace, projectTrain, labels, imgSize); % 3. 读取一张测试图像 testImg imread(ORL/s1/10.pgm); [label, distMin] pca_recognize(testImg, eigenfaces, meanFace, projectTrain, labels, imgSize); fprintf(识别结果类别 s%d最小距离 %.4f\n, label, distMin);运行这段主程序如果测试图片来自s1正常输出应该是识别结果类别 s1最小距离 0.0000最小距离为 0 是正常的因为这张图本身就在训练集里。它只说明代码流程正确不能说明系统泛化能力。真正评估系统识别能力时必须让测试图片不参与训练这是下一章的重点。6.4 运行顺序说明把上文所有文件放进同一个工程目录project/ ├── ORL/ ├── load_face_data.m ├── pca_train.m ├── pca_recognize.m └── main_pca_face.m直接运行main_pca_face.m即可。如果找不到函数文件检查当前路径是否已切换到工程目录可以使用cd命令切换或者在 MATLAB 编辑器中右键工程目录设置为当前文件夹。7. 运行结果与识别率评估参数扫描与可视化7.1 评估识别率的正确姿势很多人在课设里犯的一个严重错误是把所有图像用于训练再用同一批图像测试得到 100% 的准确率就以为自己完成了人脸识别系统。这种做法没有任何评估意义因为模型已经“见过”这些图像了。正确的做法是划分训练集和测试集。以 ORL 库为例每个人有 10 张图。可以取前 7 张用于训练后 3 张用于测试。这样训练样本是 40×7280 张测试样本是 40×3120 张。下面的评估函数完整实现了这个划分并在内部独立完成训练和测试。% 文件路径evaluate_accuracy.m function acc evaluate_accuracy(dataDir, imgSize, trainPerClass, numComponents) classDirs dir(fullfile(dataDir, s*)); numClass length(classDirs); trainX []; trainLabels []; testX []; testLabels []; % 按类别划分训练集和测试集 for i 1:numClass files dir(fullfile(classDirs(i).folder, classDirs(i).name, *.pgm)); if length(files) trainPerClass 1 error(每类样本数不足无法按 trainPerClass 划分训练集和测试集); end % 训练集每类前 trainPerClass 张 for j 1:trainPerClass img imread(fullfile(files(j).folder, files(j).name)); img imresize(img, imgSize); if size(img, 3) 3 img rgb2gray(img); end trainX(:, end1) double(img(:)); %#okAGROW trainLabels(end1) i; %#okAGROW end % 测试集每类剩余图片 for j trainPerClass1:length(files) img imread(fullfile(files(j).folder, files(j).name)); img imresize(img, imgSize); if size(img, 3) 3 img rgb2gray(img); end testX(:, end1) double(img(:)); %#okAGROW testLabels(end1) i; %#okAGROW end end % PCA 训练 numComponents min(numComponents, size(trainX, 2) - 1); meanFace mean(trainX, 2); Xc trainX - meanFace; L Xc * Xc; [V, D] eig(L); [~, idx] sort(diag(D), descend); V V(:, idx); eigenfaces Xc * V; for i 1:size(eigenfaces, 2) eigenfaces(:, i) eigenfaces(:, i) / norm(eigenfaces(:, i)); end eigenfaces eigenfaces(:, 1:numComponents); % 投影训练集和测试集 projectTrain eigenfaces * Xc; projectTest eigenfaces * (testX - meanFace); % 最近邻分类 correct 0; for i 1:size(projectTest, 2) dist sum((projectTrain - projectTest(:, i)).^2, 1); [~, idxMin] min(dist); if trainLabels(idxMin) testLabels(i) correct correct 1; end end acc correct / size(projectTest, 2); end调用示例acc evaluate_accuracy(ORL, [112, 92], 7, 30); fprintf(识别准确率%.2f%%\n, acc * 100);7.2 主成分数量扫描主成分数量numComponents是 PCA 人脸识别最重要的超参数。取太少会丢掉区分人脸的关键信息取太多可能引入噪声和冗余维度识别率不升反降。建议做一个从 1 到 50 的扫描实验画出准确率曲线这样既能选出合适的