MATLAB基于PCA的人脸识别:特征脸完整源码与工程实现指南
如果你正在做计算机视觉课的大作业或者为毕业设计做传统机器学习方案的预研很可能会遇到一个尴尬搜“MATLAB 人脸识别”要么是多年以前的论文式讲解特征值公式推导了一堆却给不出完整可运行源码要么是封装过度的商业代码调一个 API 输出 result核心步骤全被藏起来。真正能让你“看懂每一步、改得动参数、跑得通结果”的资料反而很少。PCA主成分分析人脸识别也叫特征脸Eigenface方法是早期人脸识别领域最有代表性的方案。它不依赖深度学习不需要 GPU也不需要海量标注数据。在 ORL 这类标准小型人脸库上用一段不长的 MATLAB 代码就能完成训练、投影、识别、特征脸可视化全过程。这篇是“源码 37 期”项目目标很直接给你一套完整可运行的 MATLAB PCA 人脸识别系统源码同时把背后的原理、工程细节、常见错误和调参思路讲透。读完之后你可以把它直接当作课程设计底稿也能改成自己的实验代码在 ORL、Yale 或自建人脸库上继续验证和扩展。更重要的是你能真正理解 PCA 在人脸识别里扮演什么角色而不是只会复制粘贴。1. 这篇文章真正要解决的问题先给出一个判断PCA 人脸识别适合作为图像识别入门的第一套完整方案但不适合作为生产级人脸识别系统的核心算法。为什么这么判断因为 PCA 方法在标准小数据集上表现稳定代码复杂度低数学原理清晰可视化效果好非常适合课程设计、论文基线、算法对比实验。但它的局限也很明显对光照、姿态、遮挡非常敏感一旦数据分布和训练集差异较大识别率会明显下降。生产环境现在主流是深度学习方案PCA 更多是教学和对比用途。这篇文章要解决的具体问题包括网上很多 PCA 人脸识别代码只讲“特征值分解”这个中间步骤却不说数据怎么组织、标签怎么对齐、测试集怎么划分。初学者不知道为什么要做“小样本技巧”直接算协方差矩阵会对内存和性能产生很大压力。很多代码把分类阶段封装成一个魔法函数读者不知道最近邻到底是怎么执行的。K 值保留主成分数量到底怎么选很多人试了几个数就放弃了缺乏系统评估方法。如果你正在做“MATLAB人脸识别系统”类的课程作业或者需要一个传统机器学习基线模型这篇文章可以帮你节省两天以上的时间。阅读之前建议你准备好 MATLAB 环境并下载好 ORL 等标准人脸数据集。2. PCA 人脸识别的核心概念与适用场景2.1 图像为什么要降维一张 112×92 的灰度人脸图展开成一个向量维度是 10304。训练集如果有 400 张图数据矩阵就是 400×10304。直接在这个原始维度上做分类会面临两个问题一是维度太高计算开销大二是图像像素之间存在大量冗余信息并不是所有像素点都对“区分不同人脸”有贡献。PCA 的核心思想是在高维空间中寻找一组新的正交方向使得数据沿着这些方向投影后方差最大。方差大的方向保留了样本间的主要差异方差小的方向通常对应噪声或无关细节。只保留前 K 个方向就能用远低于原始维度的特征向量表示一张人脸。2.2 什么是特征脸特征脸Eigenface是 PCA 在人脸识别中的经典应用。对训练集的中心化矩阵做特征分解后特征向量对应回图像尺寸看起来像一张张模糊的“脸部基元图”所以叫特征脸。一张新的人脸可以表示为“平均脸 若干特征脸的线性组合”。组合系数就是这张人脸在 PCA 空间中的坐标。不同人的编码系数会有差异我们正是利用这种差异进行分类。用公式近似表达训练阶段计算平均脸meanFace mean(X)中心化Xc X - meanFace求协方差矩阵的特征向量取前 K 个构成投影矩阵 W把训练样本投影到特征空间Y_train Xc * W识别阶段新图像向量化为 x减平均脸x_c x - meanFace投影y x_c * W用最近邻或余弦距离与所有训练投影 Y_train 比较距离最小的类别即为识别结果2.3 三种方案对比方案特征维度是否需要训练对光照/姿态鲁棒性实现难度适用场景原始像素直接比对极高不需要直接算距离很差简单几乎不适合实际人脸识别PCA 最近邻中需要较差但可通过预处理提升中等课程设计、经典基线、小数据集实验深度学习 CNN自动学习需要大量数据和 GPU较好需要数据增强高生产系统、大型数据集、真实场景从这个表能看出来PCA 方法的定位非常清晰它不是最强的但它是理解“传统机器学习解决视觉问题”的最好入口之一。3. 环境准备与数据集选择3.1 MATLAB 环境本文代码主要依赖 MATLAB 基础函数和图像处理工具箱Image Processing Toolbox中的 imread、rgb2gray、imresize 等函数。从经验来看R2016b 及以上版本可以直接运行R2018a 之后的版本兼容性更好。代码用到矩阵自动扩展特性如果使用的是更早版本可能需要把A - B这种广播写法改成bsxfun。建议安装MATLAB 主体版本不限R2020b 以上更好Image Processing ToolboxStatistics Toolbox 可选只在调用内置 pca 函数时需要3.2 人脸数据集训练集目录结构建议如下face_dataset/ subject01/ 1.jpg 2.jpg 3.jpg subject02/ 1.jpg 2.jpg 3.jpg每个子文件夹代表一个人。子文件夹名可以任意但建议按 person01、person02 命名方便排序。常见数据集数据集人数每人张数图像尺寸说明ORL4010112×92经典入门数据集姿态和表情有变化Yale1511320×243光照变化较明显Yale B38约64192×168光照变化大更适合做鲁棒性实验自建数据集任意建议不低于10自由设置用手机或摄像头采集注意背景一致从材料看ORL 是搭配 PCA 人脸识别最常用的数据集。只需要搜索 ORL face database 即可找到公开下载渠道注意使用学术数据时要遵守数据集协议。4. 核心流程拆解整个系统的流程可以分成五个阶段。理顺这个流程比直接看代码更重要。4.1 数据读取与预处理读取每个人子文件夹下的图片统一 resize 到固定尺寸转成灰度图把二维图像矩阵展开成一行向量。所有图片的行向量堆叠起来构成训练矩阵 X。这一步最容易出错的是图片尺寸不一致。如果不统一 resize后面所有图像向量的长度就不同PCA 计算直接失败。4.2 图像向量化与中心化每张图片变成一行向量后计算所有训练样本的均值也就是“平均脸”。每个样本减去平均脸得到中心化矩阵 Xc。中心化的目的让 PCA 找到的主方向是描述“相对平均脸的差异”而不是绝对亮度。平均脸是一张统计意义上的“普通人脸”很有可视化价值。4.3 用特征分解求 PCA 投影方向理论上要计算协方差矩阵 Xc * Xc但它的维度是 d×d在 d10304 时内存开销巨大。这里用到小样本技巧先计算 Gram 矩阵 L Xc * Xc维度只有 n×n再对 L 做特征分解。由于 L 的特征向量可以通过关系式映射回原始空间我们不需要直接处理 d×d 矩阵。这个技巧是 PCA 人脸识别的核心工程细节也是很多入门教程不讲的坑。4.4 投影与最近邻分类把中心化的训练样本投影到 W 张成的 K 维子空间得到所有训练样本的“模板坐标”。识别时新图片先做同样的预处理和投影然后和所有模板坐标计算距离。距离最小的类别就是识别结果。默认使用欧氏距离也可以尝试余弦距离。在有些数据集上余弦距离对光照归一化的样本更友好。4.5 流程总结阶段输入输出关键技术点读取与预处理原始人脸图向量矩阵 Xresize、灰度化、按类标签中心化X平均脸 Xc特征脸可解释性的基础PCA 训练Xc投影矩阵 W小样本技巧、归一化投影 分类新图片类别标签最近邻、距离度量可视化评估权重图特征脸图、识别率帮助理解算法在做什么5. 完整示例与代码实现下面提供四个主要文件。在实际使用时把它们放在同一个目录下。5.1 数据加载函数文件路径load_face_dataset.mfunction [X, labels, imageSize, classNames] load_face_dataset(dataDir, imageSize) %LOAD_FACE_DATASET 从子文件夹加载人脸图像数据 % dataDir训练集根目录每个子文件夹代表一个人 % imageSize统一缩放尺寸如 [112, 92] % % 输出 % X : n×d 矩阵每行是一张人脸的灰度像素向量 % labels : n×1 列向量每行对应该图像的类别标签 1,2,... % imageSize : 实际使用的图像尺寸 % classNames : 子文件夹名称用于识别结果反查 if nargin 2 imageSize [112, 92]; end subDirs dir(dataDir); subDirs subDirs([subDirs.isdir] ~ismember({subDirs.name}, {., ..})); if isempty(subDirs) error(未在 %s 中找到任何训练子文件夹, dataDir); end X []; labels []; classNames cell(length(subDirs), 1); for s 1:length(subDirs) classNames{s} subDirs(s).name; folderPath fullfile(dataDir, subDirs(s).name); imgFiles [dir(fullfile(folderPath, *.jpg)); ... dir(fullfile(folderPath, *.png)); ... dir(fullfile(folderPath, *.pgm)); ... dir(fullfile(folderPath, *.bmp))]; if isempty(imgFiles) warning(文件夹 %s 中没有找到图片跳过, folderPath); continue; end for f 1:length(imgFiles) imgPath fullfile(folderPath, imgFiles(f).name); img imread(imgPath); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, imageSize); X [X; double(img(:))]; labels [labels; s]; end end if isempty(X) error(没有加载到任何有效人脸图像请检查 dataDir 目录结构); end labels labels(:); [~, sortIdx] sort(labels); X X(sortIdx, :); labels labels(sortIdx); end关键点dir会返回文件夹内容通过isdir过滤出子文件夹。每种图片扩展名都尝试读取兼容 jpg、png、pgm、bmp。在循环里使用[X; newRow]是方便演示的写法。数据集较大时建议先统计图片总数预分配矩阵再逐行填充速度会快很多。5.2 PCA 训练函数文件路径pca_face_train.mfunction model pca_face_train(X, labels, numComponents) %PCA_FACE_TRAIN 训练 PCA 特征脸识别模型 % Xn×d 样本矩阵每行一张人脸向量 % labelsn×1 类别标签 % numComponents保留的主成分数量 K % % 输出 model 结构体 % - meanFace 平均脸向量 % - eigenfaces 特征脸矩阵d×numComponents % - trainProjected 训练样本在特征空间的投影坐标 % - trainLabels 训练样本标签 % - numComponents 实际保留的主成分个数 if nargin 3 numComponents 20; end n size(X, 1); if numComponents n numComponents n - 1; end if n 2 error(训练样本数量不足至少需要 2 张图片); end % 1. 中心化减去平均脸 meanFace mean(X, 1); Xc X - meanFace; % 2. 小样本技巧计算 n×n Gram 矩阵 % 避免直接求解 d×d 协方差矩阵的特征向量 L Xc * Xc; % 3. 特征分解 [eigVecs, eigVals] eig(L); eigVals diag(eigVals); % 4. 按特征值降序排列 [~, idx] sort(eigVals, descend); eigVecs eigVecs(:, idx); % 5. 映射回 d 维原始空间 eigenfaces Xc * eigVecs; % 6. 归一化每个特征脸向量 norms sqrt(sum(eigenfaces.^2, 1)); norms(norms eps) 1; eigenfaces eigenfaces ./ norms; % 7. 取前 K 个主成分 model struct(); model.eigenfaces eigenfaces(:, 1:numComponents); % 8. 投影训练数据 model.trainProjected Xc * model.eigenfaces; model.trainLabels labels; model.meanFace meanFace; model.numComponents numComponents; end这里重点解释为什么先算L Xc * Xc。原始协方差矩阵是Xc * Xc维度为 d×d。在 d10304 时直接求特征分解内存开销很大。而Xc * Xc维度是 n×n对于 ORL 这种 400 张图的训练集只有 400×400求解非常快。eigVecs是 n×n 矩阵不是最终特征向量。通过左乘Xc我们把它映射回 d 维空间这就是“小样本技巧”的完整闭环。5.3 识别函数文件路径recognize_face.mfunction [predLabel, scores] recognize_face(imgPath, model, method) %RECOGNIZE_FACE 使用训练好的 PCA 模型识别一张人脸图片 % 输入 % imgPath - 待识别图片路径 % model - pca_face_train 输出的模型结构体 % method - 距离度量可选 euclidean 或 cosine % 输出 % predLabel - 预测的类别编号 % scores - 与每个训练样本的距离分数向量 if nargin 3 method euclidean; end % 1. 读取并预处理流程必须和训练阶段保持一致 img imread(imgPath); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, model.imageSize); imgVec double(img(:)); % 2. 中心化 imgVec imgVec - model.meanFace; % 3. 投影到特征空间 proj imgVec * model.eigenfaces; % 4. 计算与所有训练样本的距离 if strcmpi(method, cosine) normA sqrt(sum(model.trainProjected.^2, 2)); normB sqrt(sum(proj.^2, 2)); scores 1 - (model.trainProjected * proj) ./ (normA * normB eps); else diff model.trainProjected - proj; scores sqrt(sum(diff.^2, 2)); end % 5. 距离最小的样本标签作为预测结果 [~, bestIdx] min(scores); predLabel model.trainLabels(bestIdx); end注意model.imageSize不是训练函数自动保存的需要主程序在调用识别函数之前赋值。原因很简单pca_face_train接收的是已经向量化的矩阵 X函数内部无法知道原始图像的二维尺寸。5.4 主程序文件路径main_pca_face.m%% MATLAB 基于 PCA 的人脸识别系统 - 主程序 clear; clc; close all; %% 1. 参数设置 dataDir face_dataset; % 训练集根目录 testImgPath test_face.jpg; % 待识别图片 imageSize [112, 92]; % 统一缩放尺寸 numComponents 25; % PCA 保留主成分数 %% 2. 加载训练数据 fprintf(正在加载训练数据...\n); [X, labels, imageSize, classNames] load_face_dataset(dataDir, imageSize); fprintf(加载完成%d 张训练图像%d 类。\n, size(X,1), length(unique(labels))); %% 3. 训练 PCA 模型 fprintf(正在训练 PCA 模型...\n); model pca_face_train(X, labels, numComponents); model.imageSize imageSize; %% 4. 可视化平均脸与前 16 个特征脸 figure(Name, 平均脸); imshow(reshape(model.meanFace, imageSize), []); title(平均脸); figure(Name, 特征脸); for i 1:min(16, numComponents) subplot(4, 4, i); imshow(reshape(model.eigenfaces(:, i), imageSize), []); title(sprintf(特征脸 %d, i)); end %% 5. 识别测试图片 if isfile(testImgPath) fprintf(正在识别 %s ...\n, testImgPath); [predLabel, ~] recognize_face(testImgPath, model, euclidean); fprintf(识别结果%s\n, classNames{predLabel}); figure(Name, 识别结果); imshow(imread(testImgPath)); title(sprintf(识别结果%s, classNames{predLabel})); end这段代码已经足够完成一个完整的 PCA 人脸识别演示。运行时你能看到平均脸和特征脸的可视化图命令行会输出识别结果。这里容易踩的一个坑是如果numComponents设置得比训练样本数还大eigenfaces(:, 1:numComponents)会越界。好在pca_face_train内部做了保护会自动把 K 截断到 n-1。6. 运行结果与效果验证6.1 预期输出运行主程序后预期出现三类可视化结果平均脸一张模糊的灰度人脸代表训练集的整体面部结构。前 16 个特征脸每个子图是一张“脸部基元”第一张通常对应亮度和整体轮廓后面的特征脸对应更细的纹理差异。识别结果图待识别图片下方显示预测类别。命令行输出类似正在加载训练数据... 加载完成400 张训练图像40 类。 正在训练 PCA 模型... 正在识别 test_face.jpg ... 识别结果subject056.2 如何判断系统是否正常平均脸没有出现明显横竖条纹说明图像 resize 和向量化正确。特征脸不是全黑或全白说明特征分解和归一化成功。识别结果能返回一个具体类别说明整体流程跑通。如果特征脸出现全黑通常是eigenfaces归一化时除数为 0或者图片读取后仍为 uint8 类型需要在中心化之前转成 double。6.3 用测试集评估识别率单张图片识别成功不代表系统可靠。为了验证模型效果建议把数据集按“每人前 5 张训练、后 5 张测试”的方式划分画出 K 值与识别率的关系曲线。文件路径evaluate_pca_face.m%% 评估 K 值对 PCA 人脸识别率的影响 clear; clc; dataDir face_dataset; imageSize [112, 92]; numTrainPerClass 5; % 每人前 5 张训练 KList [1, 2, 5, 10, 15, 20, 25, 30, 39, 50]; % 加载所有数据 [X, labels, imageSize, classNames] load_face_dataset(dataDir, imageSize); % 同一类内部按顺序划分保证共享标签含义 nClasses length(unique(labels)); trainIdx []; testIdx []; for c 1:nClasses classIdx find(labels c); nTrain min(numTrainPerClass, length(classIdx)); trainIdx [trainIdx; classIdx(1:nTrain)]; testIdx [testIdx; classIdx(nTrain1:end)]; end if isempty(testIdx) error(测试集为空请降低 numTrainPerClass 或增加每类图片数量); end accuracies zeros(length(KList), 1); for k 1:length(KList) K KList(k); model pca_face_train(X(trainIdx,:), labels(trainIdx), K); model.imageSize imageSize; % 预测全部测试样本 XcTest X(testIdx,:) - model.meanFace; testProj XcTest * model.eigenfaces; correct 0; for i 1:size(testProj, 1) dists sqrt(sum((model.trainProjected - testProj(i,:)).^2, 2)); [~, bestIdx] min(dists); if model.trainLabels(bestIdx) labels(testIdx(i)) correct correct 1; end end accuracies(k) correct / length(testIdx) * 100; fprintf(K%3d 识别率%.2f%%\n, K, accuracies(k)); end figure; plot(KList, accuracies, o-, LineWidth, 1.5); xlabel(保留的主成分个数 K); ylabel(识别率 (%)); grid on; title(PCA 人脸识别K 值与识别率关系);从材料看ORL 数据集中每人 10 张图取 5 张训练、5 张测试是很常见的做法。K 值从 1 开始增长时识别率会快速上升到某个区间后进入平台期如果 K 过分接近样本数反而可能因为保留过多噪声而下降。在绝大多数标准人脸库实验中K 取 20 到 40 之间都能获得不错效果。具体数值与数据集、图像预处理方式、距离度量都有关系不要只看一个固定值。7. 常见问题与排查思路问题现象可能原因排查方式解决方案读取图片后矩阵尺寸不匹配训练集中图片尺寸不一致查看 size(X, 2) 是否为统一像素数确保 imresize 到固定 imageSize特征脸全黑或全白double 类型转换遗漏检查 X 的数据类型是否为 double在向量化前执行 double(img(:))运行时提示矩阵维度不一致训练阶段 imageSize 与测试阶段不一致对比识别函数中的 imresize 参数训练后保存 imageSize识别前统一赋值识别