简介:这是一套面向人脸识别算法学习与实验的Matlab项目资源,以ORL人脸数据库为核心,并附带Yale人脸数据集,适合新手及有经验的开发人员用于课程设计、毕业设计或算法对比研究。压缩包共8个文件,以5个Matlab脚本(m文件)为主,覆盖数据预处理、PCA降维、LDA降维、多方式比对与预测等完整流程,同时包含1个mat格式的人脸数据文件、1个readme说明及1个Word文档,整包仅174KB,无需庞大数据集即可快速上手。目前已有1486人参与学习或下载,源码经过亲测校正,下载后可直接运行。借助这份资源,读者既能获取标准化的ORL/Yale人脸数据库,又能对照源码理解经典人脸识别算法的实现细节,还可根据文档中的说明调整参数、替换数据集,为后续改进或扩展实验提供清晰的技术起点。
1. ORL:400张灰度照片为什么仍是人脸识别的入门样本
2002年前后,剑桥大学AT&T实验室整理的那批人脸照片,后来被简称为ORL数据库。40个人、每人10张、112×92分辨率的PGM灰度图,一共400张,放在今天连一张手机照片的体积都比不上,却是无数人脸识别论文里跑通第一个实验的地方。原因并不在于数据量大,而在于它把“同一张脸的不同变化”压缩到了最紧凑的形态:表情、微小的姿态偏转、是否戴眼镜、闭眼与睁眼,每样都有一点,但每样都不极端。对一个刚把PCA、LDA读明白的工程师来说,第一件该做的事,就是用这个数据集亲手跑通一次“训练—投影—分类”的完整管线,把矩阵运算和图像语义之间的关系彻底理顺。这篇博客就围绕ORL数据库,用MATLAB从读图、建特征空间到分类验证,讲清楚参数怎么设、坑在哪里。
2. 用MATLAB把ORL数据库读成训练矩阵:路径、归一化与划分
还没有开始写PCA或者LDA之前,大部分人的第一个瓶颈出现在数据加载上。ORL的图片不是常见的JPG或PNG,而是PGM格式——一种最简的灰度图像格式,MATLAB的imread可以读,但读出来是uint8类型,如果不转成double,后面的中心化和协方差计算都会出问题。另一个坑是文件命名:ORL数据库的图片分布在40个子目录里(s1到s40),每张图名为1.pgm、2.pgm这种连续数字,不会自动带上人的标签,读图时要把“文件夹编号”与“图像编号”同时记录下来。
第一节代码建议直接写成一个加载函数,一次性完成图像读取、向量化、按行拼装矩阵三个动作。常见做法是以一个根目录为输入,返回一个样本矩阵和对应的标签向量:
function [X, y] = load_orl(root_dir) persons = dir(fullfile(root_dir, 's*')); X = []; y = []; for i = 1:length(persons) img_dir = fullfile(root_dir, persons(i).name); imgs = dir(fullfile(img_dir, '*.pgm')); for j = 1:length(imgs) img = imread(fullfile(img_dir, imgs(j).name)); img = double(img) / 255; % 归一化到[0,1],并转为double X = [X; img(:)']; % 每张图展成一行向量 y = [y; i]; % 类别标签是文件夹序号 end end end这段代码有三个值得注意的设计。第一,double(img)/255把灰度值从0~255缩放到0~1,这一步能显著提升后续PCA计算时的数值稳定性,尤其是在像素均值不为零时,中心化后的矩阵特征值分布更平稳。第二,img(:)把112×92的矩阵展成10304维的列向量,再用'转置成行向量,拼到X矩阵里,最终X的尺寸是400×10304,每一行是一张完整的人脸。第三,标签直接使用文件夹序号i,MATLAB里这就是多分类SVM或判别分析可以直接接受的类别编号。
读取之后,大部分人容易忽略的一个问题是如何划分训练集和测试集。ORL是40个人各10张图,如果直接拿前5张训练后5张测试,运气成分很大——某人的5张训练图可能恰好都是同一姿势,而测试图像变化又很大。常见的做法是分层随机划分:对每个人的10张图做随机排列,取前k张做训练,后10-k张做测试。下面这个函数返回的是抽取索引,而不是直接返回数据,使得同一个划分可以重复用到PCA、LDA、SVM等多个实验中:
function [train_idx, test_idx] = split_orl(y, k) train_idx = []; test_idx = []; for i = 1:max(y) idx = find(y == i); idx = idx(randperm(length(idx))); train_idx = [train_idx; idx(1:k)]; test_idx = [test_idx; idx(k+1:end)]; end endsplit_orl的输入k是每人取几张训练,randperm保证随机顺序,循环保证每个类别都严格分到k张训练图和10-k张测试图。把固定划分保存下来,再继续往下做,是保证实验结果可以被复现的关键习惯。
3. 用PCA特征脸建立低维人脸子空间:从原理到MATLAB实现
读入数据之后,核心问题来了:10304维的向量直接去做分类,不仅计算量大,而且很多维度实际上是在描述光照噪声和背景变化,对识别没有帮助。PCA的思想,是把原始高维空间中的数据点,投影到方差最大的若干正交方向上,用少量维度保留最主要的分布结构。在人脸识别里,这些正交方向一张张展开成图像以后,看起来像是模糊的人脸轮廓,所以被称为“特征脸”。
特征脸的计算有一个在MATLAB里必须掌握的技巧:直接用10304×10304的协方差矩阵做特征值分解,内存和耗时都不可接受。好在当样本数m远小于特征维度n时,可以转而计算m×m的小矩阵的特征向量,再映射回原空间。其数学依据是:若A是中心化后的训练矩阵(m行n列),则A·Aᵀ的尺寸只有m×m,其特征向量v满足AAᵀv=λv,两边同时左乘Aᵀ,得到AᵀA(Aᵀv)=λ(Aᵀv),也就是说Aᵀv就是原协方差矩阵的特征向量,归一化后即可用。
落到代码上,仍以X为样本矩阵、k_train为每人训练样本数为例:
[X, y] = load_orl('ORL'); [train_idx, test_idx] = split_orl(y, 5); X_train = X(train_idx, :); X_test = X(test_idx, :); % 中心化 mu = mean(X_train, 1); X_train_c = X_train - mu; X_test_c = X_test - mu; % 小矩阵特征分解 AA = X_train_c * X_train_c'; % 尺寸为 num_train x num_train [V, D] = eig(AA); D = diag(D); [D, order] = sort(D, 'descend'); V = V(:, order); % 映射回原空间,并进行单位化 eigenfaces = X_train_c' * V; for i = 1:size(eigenfaces, 2) eigenfaces(:, i) = eigenfaces(:, i) / norm(eigenfaces(:, i)); end这里的中心化用X_train - mu而不是手动循环,MATLAB对矩阵广播支持得很好,前提是所有样本矩阵是double类型。eig返回的特征值D和特征向量V都是按升序排列的,所以用sort(D,'descend')同时把V的列重排,取前几个主成分时直接取前列就行。X_train_c' * V就是上面推导的Aᵀv,归一化的目的是让投影系数在不同维度上具有一致的尺度,后续计算欧氏距离时各维度才能公平比较。
有了特征脸矩阵,剩下就是投影。把训练集和测试集分别投到前r个主成分上,得到一组低维坐标:
r = 50; % 取前50个主成分 W = eigenfaces(:, 1:r); X_train_proj = X_train_c * W; X_test_proj = X_test_c * W;从这一步开始,人脸识别就不再是图像处理,而是一个标准的K近邻或最小距离分类问题。每一张测试图投影后得到一个50维的向量,与所有训练投影向量计算欧氏距离,距离最小的那个训练样本的标签,就是预测结果。距离计算用MATLAB自带函数即可:
d = pdist2(X_test_proj, X_train_proj, 'euclidean'); [~, idx_min] = min(d, [], 2); pred = train_label(idx_min); accuracy = mean(pred == test_label);pdist2第二行输出每一行里最小值的列位置,train_label取自y(train_idx)。如果不想依赖Statistics Toolbox,也可以自己写sqrt(sum((X_test_proj - X_train_proj).^2, 2)),但要用两层循环,速度不如pdist2快。这里r=50只是一个起步值,实际效果取决于数据本身,下一节会说明如何把r调到一个有意义的值。
4. 识别率实验中必须调好的三个参数:主成分数、距离度量与训练集大小
直接跑一次上面的代码,识别率通常在0.85到0.95之间,具体数字取决于两个因素:r取多少,以及每个人用几张图做训练。这两个参数背后的规律,恰恰是ORL这个数据库最有教学价值的地方。
主成分数r决定了特征空间保留了原始数据多少信息。PCA理论里常用“累计贡献率”来判断:第i个特征值λᵢ占总特征值之和的比例,就是该主成分解释的方差比例。事实上在ORL上,前20个主成分通常能解释80%以上的方差,前50个能到95%左右。但高贡献率并不等于高识别率,因为方差大的方向不一定是最适合区分身份的方向,这可能受光照和姿态影响。换句话说,识别率随r的变化不是单调上升,而是先升后稳再可能下降,实验时务必要画出这条曲线,而不是想当然地取一个固定值。
下面这段代码把r从5扫到100,记录识别率的变化:
r_list = 5:5:100; acc = zeros(size(r_list)); for ri = 1:length(r_list) W = eigenfaces(:, 1:r_list(ri)); Xtr = X_train_c * W; Xte = X_test_c * W; d = pdist2(Xte, Xtr, 'euclidean'); [~, idx_min] = min(d, [], 2); acc(ri) = mean(pred(idx_min) == test_label); end plot(r_list, acc, 'o-');从实际经验看,r取到30~50之间识别率曲线会进入平台期,再增大主成分数可能引入噪声导致轻微下降。计算时要注意,X_train_c和X_test_c必须用同一个mu和同一个W,这是很多人实验复现对不上的原因——测试集不能参与中心化均值和特征空间的估计。
第二个参数是距离度量。pdist2除了欧氏距离,常用的还有余弦距离。欧氏距离对向量长度敏感,如果某张图整体偏亮或偏暗,投影后的模长也会整体偏大,可能导致同一张脸的两个样本被拉远。余弦距离只考虑方向、不看模长,对光照偏置有一定容忍度:
d_cos = pdist2(Xte, Xtr, 'cosine');ORL的光照变化相对温和,因此欧氏距离和余弦距离的区别不会太大,但在实际工程中,特别是遇到光照不均的人脸时,余弦距离往往更稳。建议两种都跑一次,记录对比,不要凭感觉选。
第三个参数是训练集大小。为了评估5张训练、5张测试之外的设置,把split_orl(y, k)里的k分别设为3、5、7,观察识别率变化。一个直观的结果是:k=3时识别率通常不到0.85,k=5能过0.9,k=7可以到0.97以上。把这一组结果整理成一张表格,能很清楚看出性能随样本量的变化趋势:
| 每人训练张数 | 测试总张数 | PCA维数 | 欧氏距离识别率 | 余弦距离识别率 |
|---|---|---|---|---|
| 3 | 280 | 50 | 0.84 | 0.86 |
| 5 | 200 | 50 | 0.92 | 0.93 |
| 7 | 120 | 50 | 0.97 | 0.97 |
这张表是典型的ORL结果形态,具体数字每一次运行会有微小波动。如果识别率明显低于这个区间,通常不是算法问题,而是前面的预处理出了岔子——最常遇到的是忘记中心化测试集,或者特征向量没有单位化。另一个值得注意的细节是:split_orl内部用了randperm,每次运行划分都不同,所以比较不同k值结果时要固定随机种子,或者在同一数据划分下评估,否则表格里的差异可能来自划分噪声。
5. 用留一法验证识别结果:定位是哪一类最容易分错
最后一个建议是把实验从固定划分换成留一法交叉验证,并且把分错样本单独打印出来。ORL数据库只有400张图,留一法一共做400次实验,每次以1张测试、399张训练,这在MATLAB里整个过程只有几十秒。与之前只做一次划分相比,留一法得到的识别率更稳定,也更能暴露模型的系统性问题。
留一法的实现思路很简单,对每个样本i,把第i行从X中剔除作为训练集,剩下的那个样本作为测试,循环400次统计正确次数:
rng(42); n = size(X, 1); correct = 0; for i = 1:n Xtr = X([1:i-1, i+1:end], :); ytr = y([1:i-1, i+1:end]); Xte = X(i, :); yte = y(i); mu = mean(Xtr, 1); Xtr_c = Xtr - mu; Xte_c = Xte - mu; AAt = Xtr_c * Xtr_c'; [V, D] = eig(AAt); D = diag(D); [~, order] = sort(D, 'descend'); V = V(:, order); W = Xtr_c' * V; for j = 1:size(W, 2) W(:, j) = W(:, j) / norm(W(:, j)); end W = W(:, 1:50); Xtr_p = Xtr_c * W; Xte_p = Xte_c * W; dt = pdist2(Xte_p, Xtr_p, 'euclidean'); [~, idx] = min(dt, [], 2); pred = ytr(idx); if pred == yte correct = correct + 1; else fprintf('样本 %d 真实标签 %d 预测标签 %d\n', i, yte, pred); end end fprintf('留一法准确率: %.2f%%\n', correct / n * 100);这段代码直接把整个PCA流程嵌套在循环里,每次重新计算特征空间,结果比固定划分更严格。400次循环里,每次都要做一次小型PCA,计算量上完全可接受,但如果换成更大的数据集,这种做法就不够高效了。
分错样本被打印出来以后,建议顺手做一个统计:把每个类别的错误次数单独汇总。具体做法是把i换算成类别号ceil(i/10),用一个向量记录错误数,然后bar画出来。如果错误集中在某几个人,往往说明这几个人的照片变化剧烈,比如胡子、眼镜变化明显,或者姿态偏转角度大。反过来,如果错误分散在很多类别,说明特征空间的区分力不够,优先增加r或者提取更鲁棒的特征。最后一件事是把前几个特征脸画出来看一下:
figure; for i = 1:9 subplot(3, 3, i); imshow(reshape(W(:, i), 112, 92), []); end这里W(:, i)是第i个特征向量,reshape回112×92就能看到特征脸。如果前几个特征脸有明显的人脸轮廓,说明PCA提取到的是身份相关结构,如果看起来像噪声,则训练集划分或中心化多半有问题。这个可视化检查值得每次实验都做一次,它比识别率数字更快地告诉你算法到底学到了什么。只想快速跑通流程的话,r取50、欧氏距离、每人5张训练,已经是ORL上性价比最高的一套默认配置,后续要提升识别率,再考虑从特征脸切到Fisher脸或更现代的表示方法。
本文还有配套的精品资源,点击获取