资讯详情

资讯详情

Eigenface人脸识别原理与Python实现:PCA降维实战指南

简介这份资源面向计算机视觉入门者、课程设计学生及需要完成人脸识别实验的开发者提供了一套基于Python与OpenCV实现的Eigenface人脸识别完整方案。包内共11个文件以5个Python脚本为核心覆盖训练、测试、重建与排名评估等环节另含json模型文件、xml人脸检测器、md说明、txt记录、docx设计报告及license压缩包约7.14MB。项目调用摄像头配合haar_cascade_frontalface_default.xml完成人脸检测调整框大小切出人脸并resize至与ATT数据库一致再转换为pgm格式命名保存环境基于Python 3.7与OpenCV 4.5.0主要依赖cv2、numpy、matplotlib。已有560人学习下载。读者可借此掌握从人脸采集、预处理到特征脸训练与识别的完整流程并参考设计报告理解算法原理与实验组织方式适合作为课程设计或入门实践的参考模板。1. 从一张压缩包说起Eigenface 人脸识别到底能不能打很多人第一次接触人脸识别不是从深度学习开始的而是从一个叫基于Python实现的Eigenface人脸识别.zip的压缩包开始的。解压之后通常是一堆.py文件加一个ORL或者att_faces数据集跑起来能识别出人但换一张手机自拍就翻车。这不是代码写错了而是 Eigenface 这套方法的边界本来就在那里。它解决的是「小规模、正脸、光照稳定、每人多张样本」的识别问题适合课程设计、门禁原型验证、算法入门理解 PCA 降维到底在干什么。如果你手上正好有这样一个包或者想自己从零写一个不依赖face_recognition、easyai人脸识别这类封装库的版本那这篇笔记就是按这个目标来的先把原理立住再让代码能跑最后告诉你什么场景下该换arcface人脸识别那类方案。Eigenface 的核心不是「识别」而是「降维后比较」。它把每张人脸图拉成一个长向量用 PCA 找出一组最能代表人脸变化方向的基向量也就是所谓的「特征脸」。任何一张新脸都可以表示成这组特征脸的线性组合组合系数就是它的「身份坐标」。识别时比较坐标之间的距离最近的那个就是答案。听起来简单但里面有几个参数和预处理步骤直接决定你是 95% 还是 60% 的准确率。下面按「数据怎么进 → 特征怎么算 → 识别怎么做 → 坑在哪 → 怎么验证」的顺序推一遍。2. 把图片变成矩阵ORL 数据集的读取与预处理2.1 为什么预处理比算法本身还重要Eigenface 对光照和对齐极其敏感。同一张脸左边打光和右边打光在像素空间里的欧氏距离可能比两张不同的脸还大。PCA 找的是方差最大的方向如果光照变化占了方差的大头那特征脸学到的就是「灯从哪边来」而不是「这人是谁」。所以标准流程里读图之后必须做直方图均衡化把灰度分布拉均匀削弱整体亮度差异。ORL 数据集本身已经做了裁剪和对齐每张图是 92x92 的灰度图每人 10 张共 40 人 400 张。如果你用的是自己拍的数据先用人脸检测框裁出人脸区域缩放到统一尺寸再做均衡化这一步不能省。常见做法是用cv2读图转灰度然后cv2.equalizeHist。注意cv2.imread默认读出来是 BGR 三通道必须显式转灰度否则后面拉向量时维度会变成 92x92x3PCA 算出来的东西完全不对。下面这段代码把整个数据集读成一个二维矩阵每行是一张图展平后的向量。import cv2 import numpy as np import os def load_orl(data_dir): X [] y [] # ORL 目录结构通常是 s1/1.pgm ... s40/10.pgm for subject_id in range(1, 41): sub_dir os.path.join(data_dir, fs{subject_id}) for img_name in range(1, 11): img_path os.path.join(sub_dir, f{img_name}.pgm) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 直接读灰度 if img is None: raise FileNotFoundError(img_path) img cv2.equalizeHist(img) # 光照归一化 X.append(img.flatten().astype(np.float32)) y.append(subject_id) return np.array(X), np.array(y) X, y load_orl(ORL) print(X.shape) # (400, 8464) 92*928464逻辑说明IMREAD_GRAYSCALE保证单通道equalizeHist做全局直方图均衡flatten把 92x92 拉成 8464 维向量。参数上唯一要调的是图像尺寸如果你用自己数据建议统一到 100x100 或 92x92太大 PCA 慢且噪声多太小丢失判别信息。astype(np.float32)是为了后面做矩阵运算时不被整型截断。2.2 训练集和测试集的划分方式ORL 每人 10 张标准做法是每人取前 5 张做训练后 5 张做测试这样训练集 200 张测试集 200 张。不要随机打乱后全局划分因为同一个人不同表情和角度的图如果同时进了训练和测试准确率会虚高。按人划分才能反映「没见过的新图能不能认出来」。下面这个划分逻辑很直接def split_by_subject(X, y, train_per_subject5): train_idx, test_idx [], [] for sid in np.unique(y): idx np.where(y sid)[0] train_idx.extend(idx[:train_per_subject]) test_idx.extend(idx[train_per_subject:]) return X[train_idx], y[train_idx], X[test_idx], y[test_idx] X_train, y_train, X_test, y_test split_by_subject(X, y) print(X_train.shape, X_test.shape) # (200, 8464) (200, 8464)这里train_per_subject是唯一参数取 5 是 ORL 上的惯例。如果你数据量少可以取 6 或 7但测试集相应变小评估波动会变大。注意索引顺序np.where返回的索引是按原顺序的ORL 里同一人的 10 张图文件名是 1 到 10前 5 张和后 5 张在拍摄条件上略有差异这个划分是合理的。3. 特征脸怎么算PCA 降维的矩阵推导与代码实现3.1 从协方差矩阵到特征向量把训练集看成矩阵 $X \in \mathbb{R}^{N \times D}$N200D8464。先算平均脸 $\mu \frac{1}{N}\sum x_i$然后每张图减去平均脸得到中心化矩阵 $\tilde{X}$。PCA 要解的是协方差矩阵 $C \tilde{X}^T \tilde{X}$ 的特征值问题但 C 是 8464x8464直接算特征分解非常慢。实际用的是「小样本技巧」先算 $L \tilde{X} \tilde{X}^T$大小是 200x200对 L 做特征分解得到特征向量 $v_i$再通过 $u_i \tilde{X}^T v_i$ 还原出原始空间的特征脸。这样计算量从 8464 的三次方降到 200 的三次方快几个数量级。这个技巧是 Eigenface 能落地的关键很多手写实现卡在这里。def pca_eigenface(X_train, n_components50): mu np.mean(X_train, axis0) # 平均脸 Xc X_train - mu # 中心化 L Xc Xc.T # N x N 小矩阵 eigvals, eigvecs np.linalg.eigh(L) # 对称矩阵用 eigh # eigh 返回升序取最大的 n_components 个 idx np.argsort(eigvals)[::-1][:n_components] eigvals eigvals[idx] eigvecs eigvecs[:, idx] # 还原到原始空间 U Xc.T eigvecs # D x n_components # 归一化保证基向量单位长度 U U / np.linalg.norm(U, axis0) return mu, U, eigvals mu, U, eigvals pca_eigenface(X_train, n_components50) print(U.shape) # (8464, 50)逻辑说明eigh专门用于对称矩阵比eig快且稳定argsort降序取前 k 个U的每一列就是一张特征脸可以 reshape 成 92x92 用cv2.imshow看前几个像鬼影后面越来越像噪声这是正常的。参数n_components是最关键的调参点取多少直接决定识别率和速度。下面单独说。3.2 n_components 取多少用累计方差贡献率定特征值大小代表该方向承载的方差。把特征值从大到小排列算累计和占总和的比例通常取到 90% 到 95% 就够了。ORL 上一般 50 到 100 个主成分能覆盖 90% 以上。取太少判别信息丢失识别率下降取太多把光照和噪声也学进来同样下降而且投影和比对变慢。可以画一条累计贡献率曲线来定代码很简单def choose_k_by_variance(eigvals, threshold0.95): sorted_vals np.sort(eigvals)[::-1] cum np.cumsum(sorted_vals) / np.sum(sorted_vals) k np.searchsorted(cum, threshold) 1 return k # 注意这里要用全部特征值不是截断后的 mu_full, U_full, eigvals_full pca_eigenface(X_train, n_components199) k choose_k_by_variance(eigvals_full, 0.95) print(建议主成分数:, k)参数threshold取 0.95 是常用值想更激进可以 0.90想更稳可以 0.98。注意n_components最大只能取 N-1因为中心化后秩最多 N-1这里 N200所以最多 199。取满没有降维意义但可以用来观察方差分布。实际识别时我一般直接取 50 到 80 之间在 ORL 上差别不大但速度差一倍。4. 识别与比对最近邻分类器的实现细节4.1 投影到特征脸空间训练完之后每张训练图投影到 U 上得到坐标$w_i U^T (x_i - \mu)$。测试图同样投影得到 $w_{test}$。识别就是找训练集中与 $w_{test}$ 欧氏距离最小的那个 $w_i$返回它的标签。这里有个容易忽略的点投影前必须减同一个平均脸 $\mu$不能用测试集自己的均值否则坐标系不一致距离全乱。def project(X, mu, U): return (X - mu) U # N x k W_train project(X_train, mu, U) W_test project(X_test, mu, U) def predict(W_train, y_train, W_test): preds [] for w in W_test: dists np.linalg.norm(W_train - w, axis1) preds.append(y_train[np.argmin(dists)]) return np.array(preds) preds predict(W_train, y_train, W_test) acc np.mean(preds y_test) print(识别率:, acc)逻辑说明project做的是矩阵乘法(X - mu)是 N x DU是 D x k结果 N x k。predict里对每个测试样本算到所有训练样本的距离取最小。这个循环在 200 个样本时毫秒级不用优化。acc就是准确率ORL 上 5 张训练 5 张测试k50 时通常能到 90% 以上具体取决于预处理和划分。4.2 距离度量选欧氏还是余弦欧氏距离衡量的是绝对位置差异余弦距离衡量的是方向差异。Eigenface 的坐标已经中心化过欧氏距离是标准做法。但在光照残留差异大时余弦距离有时更稳因为它对向量长度不敏感。可以两个都试看哪个在验证集上高。改起来就一行def predict_cosine(W_train, y_train, W_test): preds [] W_train_norm W_train / np.linalg.norm(W_train, axis1, keepdimsTrue) for w in W_test: w_norm w / np.linalg.norm(w) sims W_train_norm w_norm # 余弦相似度 preds.append(y_train[np.argmax(sims)]) return np.array(preds)注意余弦是相似度越大越近所以用argmax。欧氏是距离越小越近用argmin。别搞反。实际项目里我一般先用欧氏跑基线如果发现同一人被误判成另一人且两张图光照差异明显再换余弦对比。5. 避坑与排查Eigenface 落地时最容易翻车的 5 个点5.1 现象识别率忽高忽低换一次随机种子就变原因训练测试划分没有按人隔离或者用了随机划分导致同人不同图跨集。解决严格按subject_id划分训练集和测试集的标签集合完全一致但图片不重叠。检查方法很简单打印两个集合的标签交集应该等于全部标签但图片索引交集为空。5.2 现象训练时准确率 99%测试时 50%原因过拟合。n_components取太大把训练集的噪声也学进去了或者训练样本太少。解决把n_components降到 30 到 50 之间增加训练样本或者加一点正则。Eigenface 本身没有正则项靠的是主成分截断来抑制过拟合所以 k 不能贪大。5.3 现象报错ValueError: operands could not be broadcast原因读图时有的图是彩色有的灰度flatten后维度不一致。解决统一用IMREAD_GRAYSCALE并在load函数里加断言assert img.shape (92, 92)。如果数据源尺寸不一先cv2.resize到统一尺寸再展平。5.4 现象特征脸显示出来全是噪声识别率极低原因忘了做直方图均衡化或者均衡化做在了错误通道上。解决确认equalizeHist的输入是单通道 uint8。如果读进来是 float先转回 uint8 再均衡否则equalizeHist会报错或结果异常。5.5 现象新拍的照片识别不出来但测试集上正常原因新照片没有对齐和裁剪人脸在画面中的位置、大小和训练集不一致。Eigenface 对像素级对齐极度敏感偏移几个像素距离就飞了。解决先用cv2.CascadeClassifier或dlib检测人脸并裁出缩放到训练集相同尺寸再做均衡化。这一步不做换arcface也救不了。6. 进阶验证用混淆矩阵和 ROC 看 Eigenface 的真实边界跑通准确率只是第一步要知道它在哪些人之间容易混。用混淆矩阵可以看出来如果某两个人互相误判多说明他们的特征脸坐标在低维空间里靠得近可能是脸型或光照相似。代码用sklearn的confusion_matrix就行不依赖额外库。from sklearn.metrics import confusion_matrix, classification_report cm confusion_matrix(y_test, preds) print(classification_report(y_test, preds, zero_division0)) # 找出误判最多的类别对 mis [(i1, j1, cm[i, j]) for i in range(40) for j in range(40) if i ! j and cm[i, j] 0] mis.sort(keylambda x: -x[2]) print(Top 误判对:, mis[:5])逻辑说明classification_report给出每类的 precision/recallmis列出所有误判对并按次数排序。如果发现某对误判特别多可以单独把这两人的图拿出来看通常是光照或角度差异。这个分析比单纯看准确率有用因为它告诉你模型在哪里失效。另一个验证是看重建误差。Eigenface 除了识别还能用前 k 个主成分重建人脸。重建误差大的图说明它偏离了训练集的主成分方向识别大概率也会错。重建代码def reconstruct(x, mu, U): w (x - mu) U return mu w U.T x X_test[0] x_rec reconstruct(x, mu, U) err np.linalg.norm(x - x_rec) / np.linalg.norm(x) print(相对重建误差:, err)参数上U的列数就是 k。重建误差可以作为一个拒识阈值如果测试图的重建误差超过某个值就判为「未知人」而不是强行分到某一类。这在门禁场景里很实用避免把陌生人认成已注册的人。阈值取多少要看数据一般取训练集重建误差的 95 分位数。我自己的习惯是每次调完n_components和预处理先看混淆矩阵和重建误差分布再决定要不要上线。Eigenface 不是不能用而是要知道它什么时候不能用。小规模、可控光照、正脸场景它足够快足够简单一旦到了人脸识别门禁机那种户外多角度场景直接换arcface或facenet别在 Eigenface 上硬调。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →