Matlab基于PCA人脸识别系统:从原理到源码实现
平时在图像处理与模式识别相关课程里,人脸识别几乎是绕不开的经典项目。很多人第一次接触 PCA(Principal Component Analysis,主成分分析)人脸识别时,总会被“特征脸”“降维”“训练集”“识别率”这几个词绕晕。网上相关的 Matlab 源码版本很多,但大部分只给代码不解释原理,遇到报错或者想改功能时完全没有头绪。
本文将围绕 Matlab 环境下基于 PCA 的人脸识别系统,从原理讲到代码,再讲到如何扩展和定制修改。内容面向两类读者:
- 正在做课程设计、毕业设计,需要快速跑通一套人脸识别系统的同学;
- 想真正理解 PCA 降维原理、需要二次开发或调整识别流程的开发者。
读完本文,你将掌握:
- PCA 人脸识别的基本原理与核心步骤;
- Matlab 实现 PCA 人脸识别的完整源码结构;
- 如何准备人脸数据集、训练模型、测试并可视化结果;
- 常见的报错原因与排查思路;
- 如何对源码做定制修改,比如增加 GUI、更换数据库、调整降维维度等。
1. PCA人脸识别的核心概念
1.1 什么是 PCA 人脸识别
PCA 人脸识别属于基于全局特征的传统人脸识别方法。它把人脸图像看成一个高维向量,例如一张 92×112 的灰度图,展开后就是一个长度为 10304 的列向量。直接处理这么高维的数据计算量很大,而且很多维度之间是相关的。PCA 的任务就是找到一组正交基,把原始高维数据映射到一个低维子空间,同时尽量保留数据的方差信息,也就是保留最有区分度的特征。
在人脸识别中,这些低维子空间里的基向量在还原成图像后,看起来像是一张张模糊的“人脸”,所以被称为“特征脸”(Eigenface)。识别时,把训练样本和测试样本都投影到特征脸空间,然后用最近邻等分类器判断测试样本属于哪一个人。
1.2 为什么用 PCA 做人脸识别
PCA 最大的优势在于降维。对于小样本的人脸数据集,比如每个类别只有 5 张训练图,直接对原始像素做分类很容易过拟合。通过 PCA 把维度降到几十维甚至十几维后,分类器能更稳定地工作。
另外,PCA 是无监督的,训练时不需要标签就能计算出特征空间。它很适合用来做人脸的粗识别、图像重建,以及作为深度学习之外的一种轻量级基线方案。
1.3 与深度学习的区别
深度学习人脸识别(比如 FaceNet、ArcFace)通常需要大量数据和 GPU 训练,模型的泛化能力强,但对硬件和数据集要求高。PCA 人脸识别在小型数据集上效果不错,训练速度极快,代码量也小,尤其适合教学演示和轻量级应用。需要注意的是,PCA 对光照、姿态、表情变化比较敏感,实际项目中如果需要强鲁棒性,还需要配合人脸对齐、光照归一化等预处理。
2. 环境准备与版本说明
本文示例基于如下环境,版本可以根据你自己的电脑适当调整。
| 项目 | 建议版本 |
|---|---|
| 操作系统 | Windows 10 / 11,macOS,Ubuntu 均可 |
| Matlab | R2018b 及以上(本文代码兼容 R2018b ~ R2023b) |
| 工具箱 | Image Processing Toolbox(图像读取与缩放) |
| 数据集 | ORL 人脸数据库,或自建人脸数据 |
如果没有 Image Processing Toolbox,也可以只用基本函数读取图像,但代码里如果用到了imread、imresize等函数,建议安装对应工具箱。大多数版本的 Matlab 都默认包含这些基础图像函数。
数据集建议使用经典的 ORL(Olivetti Research Laboratory)数据集,它包含 40 个人的共 400 张人脸图像,每人 10 张,图像尺寸为 92×112。这个数据集规模适中,非常适合用来演示 PCA 人脸识别。你可以在网络上下载到该数据集,注意整理成统一的文件夹结构。
3. PCA人脸识别原理拆解
3.1 数学基础:协方差矩阵与特征值分解
PCA 的核心是协方差矩阵的特征值分解。
假设训练集有 N 张图像,每张图像大小为 m×n,将所有图像展开成 d = m×n 维的列向量,并组成矩阵 X,形状为 d×N。首先计算平均脸:
[ \bar{x} = \frac{1}{N}\sum_{i=1}^{N}x_i ]
然后将每个样本中心化:
[ x_i' = x_i - \bar{x} ]
中心化后的数据矩阵记为 A。协方差矩阵为:
[ C = \frac{1}{N}A A^T ]
C 的大小是 d×d,直接计算特征值和特征向量非常耗时。因此实际实现中常用“转置技巧”:计算 A^T A(大小为 N×N)的特征向量,再通过左乘 A 恢复原始协方差矩阵的特征向量。
对协方差矩阵做特征值分解后,取前 k 个最大特征值对应的特征向量,组成投影矩阵 W。训练样本在 W 上的投影就是降维后的特征向量。
3.2 人脸识别流程
一个完整的 PCA 人脸识别系统通常包含以下步骤:
- 读取训练图像,将每张图像灰度化、缩放、转为列向量;
- 计算平均脸,对训练样本中心化;
- 用转置技巧计算特征值和特征向量;
- 选取前 k 个特征向量组成特征脸空间;
- 将训练图像投影到特征脸空间,得到训练特征矩阵;
- 将测试图像投影到同一空间,得到测试特征向量;
- 使用最近邻分类器(如欧式距离)计算测试特征与所有训练特征的距离;
- 输出距离最近的人脸标签作为识别结果。
3.3 如何选择降维维度 k
k 的选择会直接影响识别率和计算速度。k 太小会丢失太多信息,k 太大则降维效果不明显,还可能引入噪声。
一个常用方法是根据特征值的能量比例来确定:
[ \text{energy} = \frac{\sum_{i=1}^{k}\lambda_i}{\sum_{i=1}^{N}\lambda_i} ]
当累计能量达到 90% 或 95% 时,可以认为保留了足够的特征信息。在代码里,我们也可以直接使用固定的 k,例如训练样本数的一半或 80%。本文为了演示,会在代码中同时展示固定 k 和按能量阈值两种方式。
4. 完整实战:Matlab 实现 PCA 人脸识别系统
下面我们用一个可运行的 Matlab 脚本和两个函数来搭建完整系统。代码不依赖外部工具箱中特殊的深度学习函数,适合直接复制运行。
4.1 数据集准备
假设你下载了 ORL 数据集,文件夹结构如下:
ORL/ s1/ 1.pgm 2.pgm ... 10.pgm s2/ ... s40/每个子文件夹代表一个人。测试时,一般每类取前若干张作为训练,后几张作为测试。本文示例取每类 5 张训练、5 张测试,你也可以修改比例。
如果你没有 ORL 数据集,也可以自建数据集。自建时需要注意:每个人一个文件夹,图片统一为灰度图,统一尺寸,例如 92×112。
4.2 主函数与核心文件
为了结构清晰,将系统拆成三个文件:
pca_face_recognition.m // 主脚本 read_face_data.m // 读取数据集 pca_face_train.m // PCA训练首先是人脸数据读取函数。这里读取每类目录下的所有图片,返回数据矩阵和标签。
% 文件路径:read_face_data.m function [data, labels] = read_face_data(datasetPath, imgSize, trainNum, testNum) % 读取人脸数据集 % datasetPath: 数据集根目录,如 './ORL' % imgSize: 图像缩放后的尺寸,如 [112, 92] % trainNum: 每类训练样本数 % testNum: 每类测试样本数 % data: 返回的数据矩阵 % labels: 对应的类别标签 personDirs = dir(datasetPath); personDirs = personDirs([personDirs.isdir]); % 过滤掉 . 和 .. personDirs = personDirs(~ismember({personDirs.name}, {'.', '..'})); numPersons = length(personDirs); trainData = []; trainLabels = []; testData = []; testLabels = []; for i = 1:numPersons personPath = fullfile(datasetPath, personDirs(i).name); imageFiles = dir(fullfile(personPath, '*.pgm')); % 如果没有 pgm 图片,尝试 jpg if isempty(imageFiles) imageFiles = dir(fullfile(personPath, '*.jpg')); end if isempty(imageFiles) error(['文件夹 ' personPath ' 中没有找到 pgm 或 jpg 图片']); end allImages = []; for j = 1:length(imageFiles) img = imread(fullfile(personPath, imageFiles(j).name)); if size(img, 3) == 3 img = rgb2gray(img); end img = imresize(img, imgSize); img = img(:); % 转成列向量 allImages = [allImages, img']; end % 按训练集和测试集切分 % 这里使用每类前 trainNum 张作为训练,后 testNum 张作为测试 trainCount = min(trainNum, length(allImages)); testCount = min(testNum, length(allImages) - trainCount); trainData = [trainData; allImages(1:trainCount, :)]; trainLabels = [trainLabels; ones(trainCount, 1) * i]; if testCount > 0 testStart = trainCount + 1; testEnd = trainCount + testCount; testData = [testData; allImages(testStart:testEnd, :)]; testLabels = [testLabels; ones(testCount, 1) * i]; end end data.train = trainData; data.test = testData; labels.train = trainLabels; labels.test = testLabels; end接下来是 PCA 训练函数。这里返回投影矩阵、特征脸、平均脸以及降维后的训练特征。
% 文件路径:pca_face_train.m function model = pca_face_train(trainData, k) % PCA 人脸识别训练函数 % trainData: 训练数据,每行是一张人脸向量的转置,即 N x d 矩阵 % k: 保留的主成分个数 % model: 包含训练结果的结构体 [N, d] = size(trainData); % 1. 计算平均脸 meanFace = mean(trainData, 1); % 2. 中心化 X = trainData - repmat(meanFace, N, 1); % 3. 用转置技巧计算特征向量 % 协方差矩阵为 (1/N) * X' * X % 但 X'*X 是 d x d 矩阵,计算量太大 % 改为计算 X * X' ,它是 N x N 矩阵 L = X * X'; [eigVectors, eigValues] = eig(L); eigValues = diag(eigValues); % 4. 按特征值从大到小排序 [~, sortIdx] = sort(eigValues, 'descend'); eigVectors = eigVectors(:, sortIdx); % 5. 通过转置技巧得到原始空间的特征向量 % 原始空间特征向量 = X' * eigVectors,然后归一化 eigenfaces = X' * eigVectors; for i = 1:N normVal = norm(eigenfaces(:, i)); if normVal > 0 eigenfaces(:, i) = eigenfaces(:, i) / normVal; end end % 6. 取前 k 个特征向量 if k > N k = N; end W = eigenfaces(:, 1:k); % 7. 训练数据投影到特征脸空间 trainProjected = X * W; % 8. 保存模型 model.meanFace = meanFace; model.W = W; model.trainProjected = trainProjected; model.trainLabels = (1:N)'; % 这是占位符,实际在外部将标签传入 end注意上面代码中model.trainLabels是一个占位符,实际训练时需要在外部把训练标签单独保存。更合理的方式是在主脚本中保存训练标签,或者修改函数让它返回更多信息。这里为了结构清晰,我们在主脚本中重新组织标签。
4.3 主脚本:训练 + 测试 + 可视化
现在编写主脚本,完成从数据读取到识别结果展示的全过程。
% 文件路径:pca_face_recognition.m clear; clc; close all; %% 参数设置 datasetPath = './ORL'; % 数据路径 imgSize = [112, 92]; % 图像缩放尺寸 trainNum = 5; % 每类训练样本数 testNum = 5; % 每类测试样本数 energyRatio = 0.95; % 能量保持比例,可用来动态确定 k useFixedK = true; % true 表示使用固定 k,false 表示按能量比 fixedK = 40; % 固定 k 值(只在 useFixedK=true 时生效) %% 1. 读取数据 [data, labels] = read_face_data(datasetPath, imgSize, trainNum, testNum); trainData = data.train; testData = data.test; trainLabels = labels.train; testLabels = labels.test; fprintf('训练样本数: %d\n', size(trainData, 1)); fprintf('测试样本数: %d\n', size(testData, 1)); fprintf('特征维度: %d\n', size(trainData, 2)); %% 2. PCA 训练 [N, d] = size(trainData); % 计算平均脸 meanFace = mean(trainData, 1); % 中心化 X = trainData - repmat(meanFace, N, 1); % 转置技巧:计算 X*X' 的特征向量 L = X * X'; [eigVectors, eigValues] = eig(L); eigValues = diag(eigValues); % 排序 [~, sortIdx] = sort(eigValues, 'descend'); eigVectors = eigVectors(:, sortIdx); % 特征脸 eigenfaces = X' * eigVectors; for i = 1:N normVal = norm(eigenfaces(:, i)); if normVal > 0 eigenfaces(:, i) = eigenfaces(:, i) / normVal; end end % 确定 k if useFixedK k = fixedK; if k > N k = N; end else % 根据能量比确定 k energy = cumsum(eigValues) / sum(eigValues); k = find(energy >= energyRatio, 1); if isempty(k) k = N; end end fprintf('保留的主成分个数: %d\n', k); % 投影矩阵 W = eigenfaces(:, 1:k); % 训练投影 trainProjected = X * W; % 测试投影 testCentered = testData - repmat(meanFace, size(testData, 1), 1); testProjected = testCentered * W; %% 3. 最近邻分类 numTest = size(testProjected, 1); predictLabels = zeros(numTest, 1); for i = 1:numTest % 计算测试样本与所有训练样本的欧式距离 diff = trainProjected - repmat(testProjected(i, :), N, 1); distances = sum(diff.^2, 2); [~, minIdx] = min(distances); predictLabels(i) = trainLabels(minIdx); end % 准确率 accuracy = sum(predictLabels == testLabels) / numTest * 100; fprintf('识别准确率: %.2f%%\n', accuracy); %% 4. 可视化部分结果 % 显示平均脸 figure('Name', '平均脸'); imshow(reshape(uint8(meanFace), imgSize)); title('平均脸'); % 显示前 10 个特征脸 numShow = min(10, k); figure('Name', '特征脸'); for i = 1:numShow subplot(2, 5, i); % 特征向量值可能不在 0-255 范围,做归一化显示 eigenfaceImg = eigenfaces(:, i); eigenfaceImg = eigenfaceImg - min(eigenfaceImg); eigenfaceImg = eigenfaceImg / max(eigenfaceImg) * 255; imshow(reshape(uint8(eigenfaceImg), imgSize)); title(['特征脸 ', num2str(i)]); end % 显示一些测试图像的识别结果 numDisplay = min(6, numTest); figure('Name', '识别结果'); for i = 1:numDisplay subplot(2, 3, i); imshow(reshape(uint8(testData(i, :)), imgSize)); title(sprintf('真实: %d, 预测: %d', testLabels(i), predictLabels(i))); end % 输出混淆矩阵 confMat = confusionmat(testLabels, predictLabels); figure('Name', '混淆矩阵'); imagesc(confMat); colorbar; xlabel('预测标签'); ylabel('真实标签'); title('混淆矩阵');4.4 运行与预期结果
将ORL数据集放在当前目录下,运行主脚本后:
- 命令行会输出训练样本数、测试样本数、特征维度、保留的主成分个数、识别准确率。
- 弹出平均脸图像,人脸轮廓清晰可见。
- 弹出前 10 个特征脸,每个特征脸像是人脸的“模板”组合。
- 弹出 6 张测试图像的识别结果。
- 显示混淆矩阵。
在 ORL 数据集上,如果训练集每类 5 张,测试集每类 5 张,k 取 40 左右,通常识别率可以达到 90% 以上。具体数值取决于图片对齐程度和数据集划分方式。
4.5 代码说明与优化点
上述代码有几个设计细节值得注意:
- 使用
dir获取文件夹列表,能够自动适配 ORL 标准目录结构; - 用
imresize统一图像尺寸,避免尺寸不一致导致reshape报错; - 转置技巧避免了直接计算高维协方差矩阵,计算量大大降低;
- 用
confusionmat输出混淆矩阵,能够直观看到哪些类别容易被混淆。
如果希望提高准确率,可以做以下优化:
- 对图像进行直方图均衡化(
histeq),减少光照影响; - 使用余弦相似度代替欧式距离作为分类度量;
- 在 PCA 之前先做人脸对齐,比如根据眼睛位置裁剪人脸区域;
- 测试不同 k 值,绘制识别率随 k 变化的曲线。
5. 如何定制修改源码
很多人下载到一套源码后,最头疼的是不知道从哪里下手修改。下面列出常见的定制需求及修改方法。
5.1 更换自己的数据集
将你的数据集整理成类似 ORL 的目录结构:
mydata/ 张三/ 1.jpg 2.jpg ... 李四/ 1.jpg然后修改datasetPath = './mydata';即可。注意read_face_data函数会根据图片扩展名自动识别.pgm或.jpg。如果你的图片是.png,可以修改代码中的dir匹配逻辑。
5.2 调整训练集和测试集比例
修改read_face_data函数中的trainNum和testNum,或者在主脚本中传入不同的参数。例如改为每类 7 张训练、3 张测试:
trainNum = 7; testNum = 3;5.3 动态确定 k 值
将useFixedK设为false,系统会根据能量比例自动选择 k。你可以修改energyRatio,比如改成 0.90 表示保留 90% 的能量。
5.4 增加 GUI 界面
很多课程设计要求有人机交互界面。你可以基于 Matlab Guide 或 App Designer 增加一个简单的 GUI,控件包括:
- “加载训练集”按钮;
- “选择测试图片”按钮;
- “识别”按钮;
- “识别结果”文本框;
- “准确率”文本框。
核心逻辑就是主脚本中的 PCA 训练和测试部分,封装成函数供 GUI 回调调用。
例如,把训练过程封装成函数:
% 文件路径:train_pca_model.m function model = train_pca_model(trainData, k) % 与 pca_face_train 类似,但返回更完整的模型 [N, d] = size(trainData); meanFace = mean(trainData, 1); X = trainData - repmat(meanFace, N, 1); L = X * X'; [eigVectors, eigValues] = eig(L); eigValues = diag(eigValues); [~, sortIdx] = sort(eigValues, 'descend'); eigVectors = eigVectors(:, sortIdx); eigenfaces = X' * eigVectors; for i = 1:N normVal = norm(eigenfaces(:, i)); if normVal > 0 eigenfaces(:, i) = eigenfaces(:, i) / normVal; end end if k > N k = N; end W = eigenfaces(:, 1:k); trainProjected = X * W; model.meanFace = meanFace; model.W = W; model.trainProjected = trainProjected; end然后在 GUI 里调用:
model = train_pca_model(trainData, 40);测试时:
testImg = imread('test.jpg'); testImg = imresize(testImg, imgSize); if size(testImg, 3) == 3 testImg = rgb2gray(testImg); end testVec = testImg(:)'; testCentered = testVec - model.meanFace; testProjected = testCentered * model.W; diff = model.trainProjected - repmat(testProjected, size(model.trainProjected, 1), 1); distances = sum(diff.^2, 2); [~, minIdx] = min(distances); recognizedLabel = trainLabels(minIdx);5.5 修改分类器
如果想换用 SVM 或贝叶斯分类器,只需要把主脚本中最近邻分类的部分替换为相应分类器的训练和预测代码。MATLAB 的fitcecoc(多类 SVM)可以直接使用:
% 训练 SVM 分类器 SVMModel = fitcecoc(trainProjected, trainLabels); predictLabels = predict(SVMModel, testProjected);注意训练数据和测试数据要使用同一 PCA 投影空间。
6. 常见问题与排查思路
下面整理 PCA 人脸识别系统开发中常见的问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
运行read_face_data时报“找不到文件夹” | 数据集路径设置错误,或当前工作目录不对 | 打印pwd查看当前目录,将datasetPath改为绝对路径 |
| 报错“图像尺寸不一致,无法组合矩阵” | 数据集中存在尺寸不同的图片,或读取时没有统一缩放 | 在读取时统一调用imresize,确保所有图片尺寸与imgSize一致 |
| 特征向量全是 0 或 NaN | 数据没有中心化,或归一化时分母为 0 | 检查是否减去了平均脸;检查特征向量归一化时是否判断了范数大于 0 |
| 识别准确率很低,只有 20%-30% | 训练样本与测试样本差异过大,或 k 选择不合适 | 尝试增加训练样本数;对图像做直方图均衡化;调整 k 值 |
| 混淆矩阵不是方阵 | 测试集中某些类别数量不一致 | 检查数据集是否每类图片数量一致,或修改标签生成逻辑 |
| 运行很慢,训练过程卡顿 | 数据集过大,高维矩阵计算耗时 | 使用转置技巧;先将图片缩小到如 64×64;使用single类型减少内存 |
| 显示特征脸时图像模糊,但识别率正常 | 特征脸本身就是低分辨率重建,显示范围未做归一化 | 在显示时对特征向量做 min-max 归一化 |
如果遇到“未定义函数或变量 'PCA_FACE_RECOGNITION'”之类的报错,通常是脚本文件名与函数名不一致,或者当前路径没有包含该文件。检查文件名是否与主函数名一致,并确认所有.m文件都在当前工作目录下。
7. 最佳实践与工程建议
7.1 对训练数据进行清洗和预处理
PCA 对输入数据的质量比较敏感。训练前最好做以下处理:
- 统一图像灰度,避免某些图像是彩色,某些是灰度;
- 对人脸区域进行裁剪,减少背景干扰;
- 使用直方图均衡化消除部分光照影响;
- 通过主成分分析前对数据做标准化(减去均值后除以标准差)有时也有帮助,但对于图像像素,通常只做中心化即可。
7.2 多做交叉验证
不要只跑一次训练和测试就得出结论。推荐采用“K 折交叉验证”或多次随机划分数据集,计算平均识别率。这样能得到更可靠的模型评估结果。对于 ORL 数据,比较常用的做法是留一法(Leave-One-Out),即每次用 399 张训练,1 张测试,循环 400 次,虽然速度慢一点,但结果更稳定。
7.3 使用能量比例选择 k
固定 k 值虽然简单,但面对不同数据集时缺乏适应性。建议在训练时计算累计能量占比,并通过绘图观察识别率随 k 的变化曲线,选择曲线稳定且准确率较高的 k 值区间。可以在主脚本中增加一个简单的循环来绘制该曲线。
kRange = 5:5:min(N, 80); accuracies = zeros(size(kRange)); for j = 1:length(kRange) k = kRange(j); % 重新训练并测试,这里省略具体代码 % accuracies(j) = 计算得到的准确率 end plot(kRange, accuracies); xlabel('k'); ylabel('识别准确率(%)');7.4 代码模块化
不要把所有逻辑都堆在一个脚本里。将“读取数据”“训练”“测试”“可视化”拆分成不同函数,后续修改和维护都会方便很多。尤其是做课程设计答辩时,模块化代码更容易体现工程能力。
7.5 注意内存占用
当图像分辨率较高时,例如 256×256 的灰度图,展成向量后维度为 65536,训练集 100 张时,X' * X的维度是 65536×65536,大约需要 34GB 内存,普通电脑跑不动。因此务必:
- 使用
single类型存储数据矩阵; - 使用转置技巧计算 L = X * X';
- 必要时降低图像分辨率到 64×64 或 32×32;
- 分批读取数据,避免同时加载所有大图到内存。
7.6 安全边界:不要在生产环境忽略授权
人脸数据属于个人敏感信息。如果你要把这套系统部署到真实业务中,一定要遵守相关法律法规,在用户知情同意的前提下采集和使用人脸数据,做好数据加密和访问控制。本文代码仅供学习研究使用,不得用于未经授权的身份识别等场景。
8. 总结与下一步学习方向
通过本文的完整讲解,你已经掌握了:
- PCA 人脸识别的基本原理,包括中心化、协方差矩阵、特征值分解、特征脸;
- Matlab 中读取数据集、PCA 训练、最近邻分类的完整实现;
- 如何调整参数和使用能量比例动态选择 k;
- 如何更换数据集、分类器,甚至扩展为 GUI 程序;
- 常见报错的排查思路与工程优化建议。
如果你后续想继续深入,可以从这几个方向展开:
- 了解 LDA(线性判别分析)与 PCA 的区别,LDA 利用标签信息,有时能获得更好的分类效果;
- 学习核 PCA(Kernel PCA),用于处理非线性分布的人脸数据;
- 结合人脸检测、人脸对齐技术构建端到端的识别系统;
- 使用深度学习网络做人脸特征提取,对比传统方法与深度方法的差异。
技术学习最重要的还是动手实践。你可以先把本文代码跑通,再尝试修改数据集和参数,观察识别率的变化。如果过程中遇到奇怪的报错,欢迎在评论区讨论,也可以把你的运行结果截图发出来,大家一起分析。
如果本文对你有帮助,可以先收藏备用,后续要用到 Matlab 人脸识别相关功能时可以直接查阅。