十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Matlab的PCA人脸识别系统:原理、实现与源码解析

基于Matlab的PCA人脸识别系统:原理、实现与源码解析 基于 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 和 8000Matlab 在拼接矩阵时会直接报错。所以读入每一张图后必须先做三件事如果图像是 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 到 400D 取 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测试图像的处理路径和训练时完全一致先转灰度、统一尺寸、展开成向量然后减去平均脸再乘以 WtestVec 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 投影和距离判断如果没理清楚界面做得再漂亮也无助于识别。先把核心链路跑通后续包装会很快。这个方案真正落地时最该盯住的不是功能列表而是输入数据格式、资源占用和失败重试。数据格式决定了能不能训练资源占用决定了能不能跑真实数据失败重试决定了批量任务能不能持续运行。只要这三件事处理干净后续扩展就很少会踩坑。
返回列表