ARTICLE DETAIL

资讯详情

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

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

Eigenface人脸识别:PCA降维原理与Python实现 简介面向Python课程设计与计算机视觉入门学习者这份Eigenface人脸识别实现包提供了完整可运行的课程设计方案。项目基于OpenCV的人脸级联检测器调用摄像头截取人脸统一缩放至ATT数据库规格并转为pgm格式覆盖从数据采集、模型训练到识别与重建的全流程。压缩包共11个文件包含5个Python脚本、设计报告Word文档、XML级联模型、JSON配置文件、说明文档等整体仅7.14MB结构紧凑便于查阅。其中mytrain、mytest、myface等脚本分别对应训练、测试与实时采集环节报告则详细阐述了Eigenface原理与实现细节。目前已有558人学习适合需要快速上手人脸识别项目、撰写课程设计的同学直接参考。1. 这是什么Eigenface人脸识别为什么值得自己动手写一遍如果你接过人脸识别门禁相关的课设或毕设大概率会面临两种选择直接调现成人脸识别SDK或者从经典算法自己写一遍。前者一天跑通但黑匣子答辩时讲不出细节后者麻烦但每一步都透明出了问题能翻源码。Eigenface就是那个“自己写一遍”的最佳起点——它用PCA把一张人脸图像压缩成一组“特征脸”再用最近邻完成识别整套流程在Python里用NumPy和OpenCV几十行代码就能落地。这个zip包里的项目核心就是这件事把训练、识别、阈值判定串成一个最小可用的工程。它适合刚入门Python和图像处理的同学也适合想搞懂“人脸识别算法到底怎么区分张三和李四”的从业者。别指望它超过现在的人脸识别门禁机但理解它之后再看任何深度学习的face embedding都会顺畅得多。2. Eigenface原理拆解PCA降维在人脸上到底做了什么2.1 人脸为什么能压缩成特征脸一张112×92的灰度人脸图展平后是10304维的向量。但人脸不是随机数据眼睛大致对称鼻子在中间脸型有共性。也就是说10304维空间里真实人脸只是在很小的子空间内分布。PCA做的就是找到一组正交基让人脸数据在这些基方向上的投影方差最大。这组基的前几个方向捕捉的是人脸的整体结构比如明暗变化、五官大致位置后几个方向捕捉细节差异。把基向量还原成图像看起来是一张张模糊的“平均脸残差”这就是Eigenface的来历。第一次看到自己跑出来的特征脸很多人会觉得像鬼影这其实说明你已经把数据里的主模式提取出来了。玄学一点说特征脸就是人脸数据在统计意义上的“骨架”。2.2 特征脸的计算流程协方差矩阵与特征值分解标准PCA流程分四步第一步把训练集里每一张人脸展平成向量组成矩阵每一行是一张人脸第二步对所有向量求平均得到平均脸然后每个向量减去平均脸得到中心化数据第三步计算协方差矩阵求其特征值和特征向量第四步按特征值从大到小取前k个特征向量就得到特征脸空间。这里有一个必须避开的坑如果直接对10304维的协方差矩阵求特征分解矩阵大小是10304×10304一次运算就是上亿浮点操作普通笔记本直接卡死。常见做法是转个弯先计算N×N的小矩阵N是训练样本数求其特征向量再通过线性变换映射回原空间。数学上等价但计算量从D²降到N²N通常几十到几百非常轻松。这个trick是Eigenface实现里最核心的优化也是面试或答辩喜欢问的点。2.3 用NumPy实现PCA的核心代码下面是我常用的Eigenface PCA实现输入是形如(N, D)的二维数组N是人脸样本数D是图像展平后的像素数输出平均脸、特征脸和对应的特征值。import numpy as np def eigenface_pca(face_matrix, n_components20): # face_matrix: (N, D) 每行是一张人脸向量D height * width N, D face_matrix.shape # 1. 计算平均脸并中心化 mean_face np.mean(face_matrix, axis0) centered face_matrix - mean_face # (N, D) # 2. 构造小协方差矩阵 centered centered.T形状为 (N, N) # 注意用 eigh 而不是 eig因为协方差矩阵是对称矩阵 cov_small centered centered.T / N eigenvalues, eigenvectors np.linalg.eigh(cov_small) # 3. 特征值降序排列 idx np.argsort(eigenvalues)[::-1] eigenvalues eigenvalues[idx] eigenvectors eigenvectors[:, idx] # 4. 把小协方差矩阵的特征向量映射回原空间 # 每一项除以 sqrt(特征值) 目的是让特征脸单位化 eigenfaces centered.T eigenvectors / np.sqrt(eigenvalues 1e-9) # 5. 取前 n_components 列作为投影基 eigenfaces eigenfaces[:, :n_components] return mean_face, eigenfaces, eigenvalues[:n_components]这段代码有两个关键参数需要说明。n_components是你保留的特征脸数量直接影响识别效果后续章节会讲怎么选。1e-9是防止特征值为0时除零的微小常数加了它是安全措施。为什么用np.linalg.eigh而不是np.linalg.eig因为cov_small是对称矩阵eigh专门针对对称矩阵做优化数值稳定性更好计算也更快。如果你用的是彩色图或没有中心化这里算出来的特征脸会有问题我见过有人图方便少写了减均值那一步结果出来的特征脸全是一片灰那个翻车现场很典型。3. 用Python复现Eigenface从数据集到最近邻识别的完整代码3.1 数据集准备用ORL人脸库还是自己拍照Eigenface的标准实验数据集是ORL人脸库40个人每人10张共400张112×92的灰度图。这个体积很小几乎任何机器都能跑得动而且每个人有表情、细微姿态和光照差异正好能测试算法鲁棒性。如果你的zip包里已经带了数据集和脚本优先用里面现成的省去采集时间。如果没有自己动手也很快用OpenCV调用摄像头对同一个人在不同角度、轻微表情下拍10张存到以人名为目录的文件夹里。注意两点一是图像必须统一为灰度图二是尺寸必须一致。Eigenface本质是向量运算尺寸不一致会在展平后直接导致矩阵维度对不上。我一般会写一个小脚本读取所有路径并强制resize到固定尺寸比如(112, 92)同时检查有没有损坏的图。这一步看似简单但能避免后面80%的矩阵崩溃错误。3.2 训练代码读图、对齐、构建特征空间这里给出一套可复制的训练流程包含从文件夹读图到生成投影矩阵的完整代码。import cv2 import os import numpy as np def load_dataset(dataset_path, image_size(112, 92)): faces, labels [], [] for person_name in sorted(os.listdir(dataset_path)): person_dir os.path.join(dataset_path, person_name) if not os.path.isdir(person_dir): continue for img_name in os.listdir(person_dir): img_path os.path.join(person_dir, img_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: print(f[warning] broken image: {img_path}) continue img cv2.resize(img, image_size) faces.append(img.flatten()) labels.append(person_name) faces np.array(faces, dtypenp.float32) labels np.array(labels) return faces, labels # 假设 dataset_path 下有 person_01, person_02 ... 子目录 faces, labels load_dataset(./dataset) print(样本数:, faces.shape[0], 特征维度:, faces.shape[1]) # 训练得到平均脸和特征脸空间 mean_face, eigenfaces, eigenvalues eigenface_pca(faces, n_components20) # 把所有训练样本投影到特征脸空间得到低维表示 projected (faces - mean_face) eigenfaces # shape (N, 20) # 保存模型参数供识别阶段复用 np.savez(eigenface_model.npz, mean_facemean_face, eigenfaceseigenfaces, eigenvalueseigenvalues, labelslabels, projectedprojected)load_dataset里的image_size参数是全局统一尺寸一旦确定就不要改动。如果训练时用(112, 92)识别时必须一致否则投影矩阵维度对不上报错信息会提示“形状不匹配”。faces转成float32很重要因为图像原始数据是uint8直接用会在计算均值减法时溢出。参数说明n_components在这里设为20是ORL数据集上的常用值但正式用之前应做交叉验证。np.savez保存的是NumPy的二进制格式比pickle更轻量读取也方便后续加载用np.load即可。3.3 识别代码最近邻分类与阈值判定识别阶段把待测人脸向量投影到特征空间然后与所有训练样本的投影向量比对找距离最近的一个作为识别结果。同时设定一个阈值如果最小距离太大说明这张脸从未出现在训练集里判为“未知”。这是门禁系统最需要的功能——不能把所有陌生人硬看成某个员工。def recognize(query_img, model, threshold3000.0, image_size(112, 92)): mean_face model[mean_face] eigenfaces model[eigenfaces] projected model[projected] labels model[labels] # 预处理与训练完全一致 img cv2.imread(query_img, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(fcannot read image: {query_img}) img cv2.resize(img, image_size) vec img.flatten().astype(np.float32) # 中心化并投影 centered vec - mean_face q_proj centered eigenfaces # shape (k,) # 计算到所有训练样本的距离 dists np.linalg.norm(projected - q_proj, axis1) min_idx np.argmin(dists) min_dist dists[min_idx] # 阈值判定 if min_dist threshold: return unknown, min_dist return labels[min_idx], min_dist这段代码里最容易出问题的是np.linalg.norm的axis参数。projected是(N, k)矩阵q_proj是(k,)向量两者相减会自动完成广播得到(N, k)的差值矩阵axis1是沿着每个样本方向求范数得到N个距离值。很多人初学写成axis0结果距离值变成k个后面argmin就是错的。threshold的默认值3000是我在ORL上的经验值自己采集数据后必须重新标定。标定方法拿着训练集里已有的样本去跑一遍识别看它们的匹配距离一般多大再拿几张不属于任何人的照片测试看最小距离多大。选一个介于两者之间的阈值。这一步是门禁误识别率的分水岭别图省事。4. Eigenface参数调优k值、光照归一化与距离度量的取舍4.1 保留的主成分数量k怎么定Eigenface最关键的参数就是特征脸数量k。k太小丢失的判别信息太多不同人脸在低维空间几乎重叠k太大又会被噪声和表情细节带偏而且计算和存储开销上升。常见做法是用特征值累计贡献率选定k。特征值代表对应方向上的方差大小累计贡献率就是前k个特征值之和占总特征值之和的比例。def select_k(eigenvalues, ratio0.95): total np.sum(eigenvalues) cumsum np.cumsum(eigenvalues) / total k np.argmax(cumsum ratio) 1 return k # 完整特征值由 eigenface_pca 返回但注意它只返回了前 n_components 个 # 如果需要计算累计贡献率应在函数里返回全部特征值或单独处理注意一个容易踩的坑很多实现只保留前k个特征值就丢弃剩下的导致无法计算累计贡献率。正确做法是先求全量特征值再按贡献率选k。ORL数据集上通常选择20~30个特征脸累计贡献率已经可达95%左右。但“贡献率高”不等于“识别率高”因为PCA是保留数据方差最大的方向而不是区分人最明显的方向。这跟FisherfaceLDA的核心差异就在这PCA无监督LDA有监督。实战中我会先用一个固定值比如20跑通再在交叉验证里扫k。如果k每增加5验证集准确率几乎不变说明已经达到饱和如果准确率掉头向下就是过拟合了。这个拐点就是最好的k。4.2 光照归一化直方图均衡化与Gamma校正Eigenface对光照极其敏感。同一张脸左边打光半边亮半边暗欧氏距离可能比两张不同人脸还大。所以预处理必须是训练和识别共用一套。最简单有效的是直方图均衡化把灰度分布拉开抑制光照不均。Gamma校正则是另一种常见手段对过亮或过暗的图像做非线性映射。def preprocess_face(img): # 输入是灰度图输出是归一化后的灰度图 if img is None: return None # 直方图均衡化增强对比度 img_eq cv2.equalizeHist(img) # Gamma校正gamma1 让暗区变亮gamma1 让亮区变暗 gamma 0.8 img_gamma np.uint8(255 * (img_eq / 255.0) ** gamma) return img_gamma这里gamma0.8是我在室内光照不均场景下的常用值不需要每次都调。关键点在于预处理的每一个参数都必须同时应用到训练集和待识别图像。很多人训练时偷懒没做预处理识别时加了均衡化结果模型看到的“人脸空间”和测试输入不匹配准确率不升反降这就是典型的翻车现场。另外要注意cv2.equalizeHist只接受单通道灰度图如果传给彩色图会直接报错。所以我在load_dataset和recognize里都强制IMREAD_GRAYSCALE从源头避免。4.3 距离度量欧氏距离与余弦相似度的取舍投影到特征空间后比较两个人脸通常有欧氏距离和余弦相似度两种方式。欧氏距离直接度量低维坐标的绝对差距直觉上就是“在特征脸空间里离得远近”。余弦相似度度量两个向量之间的夹角更关注方向而非长度。对人脸这种受光照影响的信号余弦相似度往往更稳健因为光照变化可能让向量整体变长或变短但方向变化较小。# 手写余弦相似度 def cosine_distance(a, b): # a: (k,), b: (N, k) a_norm a / (np.linalg.norm(a) 1e-9) b_norm b / (np.linalg.norm(b, axis1, keepdimsTrue) 1e-9) return 1 - a_norm[None, :] b_norm.T # 返回 1 - 相似度变成“距离”实际使用中我偏爱先把特征向量单位化再算欧氏距离。数学上单位化后的欧氏距离和余弦相似度单调相关但欧氏距离的阈值更容易从数据集中估计。还有一个更实用的技巧当训练样本只有每个员工几张照片时不要直接用欧氏距离而是计算待测脸到每个类别均值向量的距离可以减少单张照片表情波动的影响。代码很简单先对projected按labels分组求平均得到每人的类中心再与q_proj计算距离。这个改动对现场测试的稳定提升非常明显代价只是几个类中心向量存储开销几乎为零。5. Eigenface避坑指南5个识别率上不去的真实排查记录5.1 图像尺寸不一致导致矩阵运算崩溃现象训练跑到特征分解时报错ValueError: operands could not be broadcast together或者更迷惑的“维度不匹配”。原因load_dataset里有些图加载失败或尺寸不对劲被flatten()后行数相同但列数不同整个face_matrix变成了“锯齿形”二维数组。解决在load_dataset里先检查img.shape是否等于预设尺寸不相等就resize同时检查img是否为None因为cv2.imread读不出文件会返回None而不是抛异常。下列代码放在读取之后if img is None: print(f[skip] unreadable: {img_path}) continue if img.shape ! image_size: img cv2.resize(img, image_size)其实源头是文件损坏或路径不对最好每次训练前打印样本总数确认和预期一致。5.2 灰度图与RGB混用导致特征向量失真现象特征脸看起来带有“格子纹”或者重建出的平均脸颜色发花识别率很低。原因有的图片用彩色模式读入flatten()后一个像素变成三个数值BGR相当于特征维度变成3倍而且这些通道信息对身份判别没有意义。解决统一用cv2.imread(path, cv2.IMREAD_GRAYSCALE)并且在训练前断言所有图都是二维的。如果你自己采集数据最好在保存时就转成灰度避免后续混淆。如果zip包里已经有彩色图那么预处理脚本里务必加一句img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)。5.3 训练样本太少时协方差矩阵奇异现象在求特征向量时出现LinAlgError: Eigenvalues did not converge或者特征值出现负数。原因样本数量N小于有效秩或者某些类只有1张图导致类内方差为零。在Eigenface的标准流程中协方差矩阵是N×N的只要N1理论上就能算但若去均值后某些方向方差恰好为0数值分解就会不稳定。解决给特征值加小常数1e-6见第2章代码里的1e-9更稳妥的做法是改用SVD实现避免显式计算协方差矩阵。centered faces - np.mean(faces, axis0) U, S, Vt np.linalg.svd(centered, full_matricesFalse) eigenfaces U[:, :n_components]np.linalg.svd数值稳定性远好于eigh在样本量少、噪声大的情况下我优先用SVD版本。5.4 识别阈值设错导致误判率飙升现象陌生人照片被识别成某个人或者自己人戴个眼镜就变成“unknown”。原因阈值拍脑袋定义没有根据实际数据分布设置。解决用验证集做阈值标定。让所有训练样本通过识别函数记录各自的最小距离得到“类内距离”分布再拿一批不属于任何人的图像记录“类间距离”。阈值最好取两类分布的交叉点。简单脚本示意train_dists [] for label in labels: dist calc_min_dist(样本) # 自己实现 train_dists.append(dist) threshold min(train_dists) * 1.2 # 保险系数严格做法是画出两个分布的直方图找误识率与拒识率平衡点。这个调参过程不能省否则现场事故率极高。5.5 内存占用过高用SVD代替直接特征分解现象训练集只有几百张图但内存占用却飙到几个G甚至卡死。原因虽然我们用了N×N的小协方差矩阵但如果有人直接对D×D的大协方差矩阵调np.linalg.eigD10304时就会分配一个包含上亿元素的浮点矩阵。解决严格使用第2章或第5.3节的SVD路径。如果样本数量N也很大比如超过1000还可以用增量式PCA分批读取图像并更新均值但Eigenface场景通常用不到。我建议调试时先用n_components10跑通流程再逐步加大避免内存问题掩盖算法问题。6. 验证与进阶用交叉验证评估Eigenface再跳向Fisherface6.1 用留一法评估模型真实性能Eigenface最容易被质疑的点是“实验设置不严谨”。如果训练集和测试集来自同一天的采集识别率高到好看但换一天重新拍摄立刻掉链子。标准做法是“留一个人全部样本做测试”训练时排除这个人的所有图像用剩下的人训练然后用这个人未见过的图像测试。每个人轮流做一次统计平均准确率。这样能防止同一人照片出现在训练和测试里的数据泄漏。实现上可以手工循环也可以借助sklearn.model_selection.LeaveOneLabelOut。我习惯手工写因为还能顺便记录每个类别的误识情况。from sklearn.model_selection import LeaveOneLabelOut lolo LeaveOneLabelOut() for train_idx, test_idx in lolo.split(faces, labels, groupslabels): train_faces, test_faces faces[train_idx], faces[test_idx] mean, eig, _ eigenface_pca(train_faces, n_components20) train_proj (train_faces - mean) eig test_proj (test_faces - mean) eig # 对每个测试样本在 train_proj 中找到最近邻判断 label 是否一致注意输出指标不只是准确率还要看“每个类被误认成谁”这可能暴露某两个人长得太像导致算法天然分不开。ORL数据上Eigenface一般能到90%左右低于80%说明实现里有bug。6.2 从Eigenface到Fisherface的迁移路径Eigenface的短板是PCA不关心类别信息。Fisherface换用LDA目标是让类间散布最大、类内散布最小。在光照变化剧烈的数据集上Fisherface通常比Eigenface高5到10个百分点。迁移成本很低只需要把投影矩阵从PCA换成LDA特征向量后面的最近邻逻辑完全不用动。如果你把第2章代码里的PCA替换为np.linalg.eig求解广义特征值问题就能得到Fisherface。更省事的做法是直接用OpenCV的cv2.face.FisherFaceRecognizer_create但那个黑匣子只能看到输入输出不适合学习。我自己的经验是先彻底掌握Eigenface再花半天读LDA公式然后自己写Fisherface进阶速度最快。6.3 我的收尾建议如果你拿到这个zip是为了交作业或搭演示系统别急着改算法。先按第3章的代码把原样跑通确保能识别出已知人员并拒绝陌生人。然后挨个调n_components和threshold每次只改一个参数记录识别率。翻车时优先怀疑预处理不一致其次怀疑距离度量方向错误。等你把这几步都走完就可以把PCA换成LDA、把最近邻换成SVM甚至叠加一个人脸检测框实现实时门禁。我做过不少这类项目最大的教训是“不要相信一次测试的分数”至少跑三次不同光照下的采集取稳定结果。希望这篇笔记能帮你少走弯路把Eigenface真正吃透。本文还有配套的精品资源点击获取
返回列表