简介:基于Python的Eigenface人脸识别课程设计包,面向需要完成人脸识别方向课程设计或毕业设计的高校学生,完整覆盖从人脸检测、特征提取、模型训练到识别验证的经典实现流程。包内共11个文件,包含5个Python源码脚本、模型配置json、OpenCV级联分类器xml、设计报告docx以及说明文档和license,整体大小约7.14MB,目录结构清晰,便于按流程阅读和实践。代码基于Python 3.7与OpenCV 4.5.0开发,在Visual Studio Code中运行,调用摄像头配合haar_cascade_frontalface_default.xml完成人脸检测,将截取到的人脸尺寸归一化并转换为pgm格式生成个人样本,同时提供训练、测试、重构、人脸采集等多个模块脚本,覆盖从数据准备、模型训练到结果验证的完整链路,可直接修改后复用于实验。随包附带的word设计报告可帮助理解Eigenface原理与实验设计。目前已有558人学习下载,适合作为人脸识别入门项目、课程设计模板及算法实验改写的参考素材。
1. Eigenface 人脸识别这个 zip,到底解决什么问题
Eigenface(特征脸)是很多人接触人脸识别时绕不过去的第一个里程碑算法。它不依赖深度学习,不烧 GPU,纯用 Python 和 NumPy 就能在几十行代码里跑通一套“训练—识别”流程,把一张人脸图像压缩成几十个系数,再用最近邻去匹配身份。这个 zip 本质上是一个完整的 Eigenface 实现包,里面应该包含数据预处理脚本、PCA 训练代码、识别脚本和一个人脸数据集组织目录。它适合三类人:正在做毕设或课设的学生,刚接触机器视觉想从原理层面理解 PCA 降维如何作用于图像的工程师,以及需要在嵌入式设备或低算力环境里做轻量级人脸验证的场景。先给出一个反直觉的结论:Eigenface 在白底证件照上准确率能到 95% 以上,但一到真实光照、侧脸、遮挡场景就会断崖式下跌——这不是 bug,而是线性子空间模型的天花板。理解了这个边界,你才能真正用好这个 zip,而不是拿到手跑通 demo 就以为可以上生产。
2. 特征脸原理:为什么 PCA 能把人脸变成一组数字
2.1 从像素空间到特征空间的本质跳跃
一张 112×92 的灰度人脸图,展平后是一个 10304 维的向量。如果直接把所有训练样本丢进分类器,维度灾难会同时带来计算开销和过拟合。Eigenface 的核心假设是:人脸虽然分布在 10304 维空间里,但真正的变化其实被少数几个方向主导——比如光照方向、脸型宽窄、五官位置。PCA 要做的就是找到这些主方向,把每张人脸从 10304 维投影到 10~50 维,而且保留绝大部分区分度。
数学上,假设训练集有 N 张人脸,每张是 d 维向量。先算平均脸:
import numpy as np def compute_mean_face(face_matrix): # face_matrix: shape (N, d),每行是一张展平后的人脸 mean_face = np.mean(face_matrix, axis=0) return mean_face # 示例:10 张 112x92 的图,展平后每张 10304 维 faces = np.random.randn(10, 10304) mean = compute_mean_face(faces) print(mean.shape) # (10304,)这段代码做的事就是把所有训练样本逐像素求平均,得到一张“平均脸”。注意这里 axis=0 表示沿着样本维度求均值,得到的 mean_face 是一维数组,长度等于单张图片的像素数。之后我们要把每张脸减去平均脸,得到差值矩阵,再做协方差矩阵的特征分解。协方差矩阵的维度是 d×d,对 10304 维来说直接算特征分解内存爆炸,所以要用一个小技巧——对差值矩阵的转置做 SVD 或特征分解,而不是直接对协方差矩阵操作。
2.2 从协方差矩阵到特征脸:降维的完整推导
设差值矩阵 X 的 shape 为 (N, d),我们要找的其实是 XᵀX 的特征向量。但 XᵀX 是 d×d,代价太高;转而做 XXᵀ,这是 N×N,对几十到几百个训练样本来说非常小。XXᵀ 的特征向量 v 对应关系是:XᵀX 的特征向量 u = Xᵀv 再归一化。这就是经典的“转置技巧”,也是 Eigenface 能跑在纯 CPU 上的关键。
def train_eigenfaces(face_matrix, n_components=25): # face_matrix: shape (N, d),每行是一张人脸 N, d = face_matrix.shape mean_face = np.mean(face_matrix, axis=0) centered = face_matrix - mean_face # 去均值 # 小矩阵:N x N cov_small = np.dot(centered, centered.T) # (N, N) eigvals, eigvecs_small = np.linalg.eigh(cov_small) # 特征值降序排列 idx = np.argsort(eigvals)[::-1] eigvecs_small = eigvecs_small[:, idx] # 还原到 d 维空间 eigvecs = np.dot(centered.T, eigvecs_small) # (d, N) norm = np.linalg.norm(eigvecs, axis=0) eigvecs = eigvecs / norm # 归一化 # 取前 n_components 个主成分作为特征脸 eigenfaces = eigvecs[:, :n_components] return mean_face, eigenfaces # 用法 X = np.random.randn(40, 10304) mean, eigenfaces = train_eigenfaces(X, n_components=30) print(eigenfaces.shape) # (10304, 30)这里的核心参数 n_components决定了最终特征空间的维度。选多少没有绝对标准,常见做法是先保留能解释 90% 以上方差的前 k 个主成分。你可以加一行代码算解释方差比:
explained_variance_ratio = eigvals[idx] / np.sum(eigvals) cumsum = np.cumsum(explained_variance_ratio) k = np.argmax(cumsum >= 0.95) + 1 print(f"保留 {k} 个特征脸即可解释 95% 方差")注意 np.linalg.eigh 返回的特征值已经是升序,所以必须做一次逆序索引。用 eigh 而不是 eig,是因为协方差矩阵是对称的,eigh 数值稳定性更好,速度也更快。特征脸本质上是 d 维向量,可以 reshape 回 (112, 92) 可视化:你会看到一张张“幽灵脸”,前几个特征脸对应光照和整体明暗,后面则越来越像具体的人脸结构差异。
3. 用 Python 搭建完整 Eigenface 项目:从数据集到识别
3.1 数据集组织与预处理流程图
这个 zip 里大概率自带一个数据集目录结构,通常是按人分文件夹,每个文件夹放同一人的多张不同表情/姿态的灰度图。如果你手头没有,用 ORL 人脸数据集(400 张,40 人,每人 10 张)或 Yale 数据集最合适。数据加载和预处理我一般这样组织:
import os import cv2 import numpy as np IMG_SIZE = (112, 92) def load_dataset(data_dir): """ 目录结构: data_dir/ person_01/ 1.pgm 2.pgm person_02/ 1.pgm ... 返回: faces: (N, 10304) 的 float32 矩阵 labels: (N,) 的 int 标签 name_map: 标签 -> 人名 的字典 """ faces, labels = [], [] name_map = {} label = 0 for person_name in sorted(os.listdir(data_dir)): person_dir = os.path.join(data_dir, person_name) if not os.path.isdir(person_dir): continue name_map[label] = person_name for img_name in os.listdir(person_dir): img_path = os.path.join(person_dir, img_name) if not img_path.lower().endswith(('.pgm', '.jpg', '.png', '.bmp')): continue img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: print(f"警告: 无法读取 {img_path}") continue img = cv2.resize(img, IMG_SIZE) faces.append(img.flatten().astype(np.float32)) labels.append(label) label += 1 return np.array(faces), np.array(labels), name_map # 用法示例 faces, labels, name_map = load_dataset('./att_faces') print(faces.shape) # (400, 10304) print(labels[:10]) # [0 0 0 ...]这个地方有四个易错点。第一,cv2.imread 读入的灰度图是 uint8,范围 0~255,必须转 float32,否则后续算均值、协方差时精度不够且可能溢出。第二,resize 到固定尺寸很关键,同一数据集里原始图片可能尺寸不一致,Eigenface 的向量维度必须统一。第三,文件名过滤只认图片后缀,防止把 Mac 的 .DS_Store 或 Windows 的隐藏文件读进来。第四,label 从 0 开始按字母序递增,name_map 负责把数字标签映射回人名,识别结果输出时用 name_map 转回字符串。
3.2 训练流程封装:一个类搞定所有逻辑
class EigenfaceRecognizer: def __init__(self, n_components=25): self.n_components = n_components self.mean_face = None self.eigenfaces = None self.labels = None self.projections = None self.name_map = None def fit(self, faces, labels, name_map=None): # faces: (N, d), labels: (N,) self.labels = labels self.name_map = name_map if name_map is not None else {i: str(i) for i in set(labels)} self.mean_face = np.mean(faces, axis=0) centered = faces - self.mean_face # 计算小协方差矩阵的特征分解 cov_small = np.dot(centered, centered.T) eigvals, eigvecs_small = np.linalg.eigh(cov_small) # 按特征值降序 idx = np.argsort(eigvals)[::-1] eigvecs_small = eigvecs_small[:, idx] # 映射回原空间并归一化 eigenfaces = np.dot(centered.T, eigvecs_small) norms = np.linalg.norm(eigenfaces, axis=0) eigenfaces = eigenfaces / norms # 取前 n_components 个 self.eigenfaces = eigenfaces[:, :self.n_components] # 把所有训练样本投影到特征脸空间 self.projections = np.dot(centered, self.eigenfaces) return self def predict(self, face): """ face: (d,) 或 (1, d) 的展平人脸向量 返回 (label, distance) """ face = np.asarray(face).reshape(1, -1).astype(np.float32) centered = face - self.mean_face # 投影到特征脸空间 projection = np.dot(centered, self.eigenfaces) # (1, n_components) # 计算与所有训练样本投影的欧氏距离 distances = np.linalg.norm(self.projections - projection, axis=1) best_idx = np.argmin(distances) return self.labels[best_idx], distances[best_idx]这个类封装的思路是:fit 里做完 PCA 并缓存所有训练样本在特征脸空间的坐标,predict 阶段只需要做两次矩阵乘法和一次距离排序——推理速度极快,单张 112×92 的图在普通 CPU 上跑一次小于 1 毫秒。距离度量选欧氏距离,这是最常见做法;也可以用余弦相似度,但欧氏距离在特征脸空间里物理意义更直觉。注意 predict 返回的是一个元组 (label, distance),distance 可以当作置信度参考——距离阈值判断在后面讲。
3.3 完整训练与识别主脚本
from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 加载数据 faces, labels, name_map = load_dataset('./att_faces') # ORL 数据集 # 划分训练集和测试集:每个人取前 8 张训练,后 2 张测试 # 注意不能随机打乱后直接 split,会造成同一个人同时出现在训练和测试里 X_train, X_test, y_train, y_test = [], [], [], [] for person_id in set(labels): idxs = np.where(labels == person_id)[0] # 按原始顺序切分,保证前 80% 训练 20% 测试 split = int(len(idxs) * 0.8) X_train.extend(faces[idxs[:split]]) y_train.extend(labels[idxs[:split]]) X_test.extend(faces[idxs[split:]]) y_test.extend(labels[idxs[split:]]) X_train = np.array(X_train) X_test = np.array(X_test) y_train = np.array(y_train) y_test = np.array(y_test) # 训练 Eigenface 模型 model = EigenfaceRecognizer(n_components=30) model.fit(X_train, y_train, name_map) # 测试识别 correct = 0 for i in range(len(X_test)): pred_label, dist = model.predict(X_test[i]) if pred_label == y_test[i]: correct += 1 print(f"识别准确率: {correct / len(X_test) * 100:.2f}%") print(classification_report(y_test, [model.predict(x)[0] for x in X_test]))这里训练/测试划分特意用了按人切分而不是全局随机切分,这是一个关键点:如果你的测试集里混入了训练时见过的人,准确率会虚高。按人切分的逻辑是每个人至少保留 2 张作为测试,训练和测试的身份完全隔离。ORL 数据集每人 10 张,8 张训练 2 张测试,n_components=30 时准确率通常在 95% 左右,能跑到这个数字说明管线没毛病。
4. 参数怎么调:n_components、距离阈值和数据集规模的影响
4.1 n_components 与准确率的关系曲线
n_components 是 Eigenface 最敏感的超参数。设太小,特征空间不能充分表达人脸差异;设太大,会把噪声和光照变化也当作区分特征,且尾部特征脸对应的小特征值方向含有大量噪声。最稳妥的做法是做一个 k 值扫描:
import matplotlib.pyplot as plt k_values = [5, 10, 15, 20, 25, 30, 40, 50, 60, 80] accuracies = [] for k in k_values: model = EigenfaceRecognizer(n_components=k) model.fit(X_train, y_train, name_map) acc = np.mean([model.predict(x)[0] == y_test[i] for i, x in enumerate(X_test)]) accuracies.append(acc) print(f"k={k}, 准确率={acc*100:.2f}%") # 画曲线图 plt.plot(k_values, accuracies, marker='o') plt.xlabel('n_components') plt.ylabel('Accuracy') plt.title('Accuracy vs Eigenface Components') plt.show()这段代码跑出来通常是一条先快速上升、后平缓、最后轻微下降的曲线。ORL 数据集上 k=25 到 k=40 之间是平台期,超过 50 后准确率不升反降。我在实际项目里一般这么定:如果训练样本总数是 N,n_components 取 N/3 到 N/2 之间,然后在这个范围内做一次扫描取最优。数据量小时(比如每人只有 3 张)k 取 10 就够;数据量大且光照复杂时,k 取 60~100 能捕捉更多变化,但也更依赖训练数据的代表性。
4.2 距离阈值:识别和拒识之间的权衡
识别不只是找到最近邻,还要决定“这个人到底在不在数据库里”。如果任意测试脸都强行归到某个训练样本上,陌生人也会被分配一个身份。解决办法是设定距离上限:
THRESHOLD = 2800 # 欧氏距离阈值,需要根据训练集统计得出 def recognize_with_threshold(model, face, threshold=2800): label, distance = model.predict(face) if distance <= threshold: return label, distance, True # 识别成功 else: return None, distance, False # 拒绝识别 # 统计训练集自身的距离分布来确定阈值 train_distances = [] for i in range(len(X_train)): _, d = model.predict(X_train[i]) train_distances.append(d) train_distances = np.array(train_distances) print(f"训练集内距离: 均值={train_distances.mean():.1f}, " f"std={train_distances.std():.1f}, " f"99分位={np.percentile(train_distances, 99):.1f}")阈值选多少属于“血泪经验”。我一般用训练集内所有样本到自身类中心的距离统计,取 p99 再乘 1.2~1.5 倍作为初始阈值,然后在验证集上做一次 ROC 曲线微调。阈值太严格会把带眼镜、光线变化的真人也拒掉;太宽泛会让陌生人混进来。注意 Eigenface 的距离分布受图像尺寸影响极大,112×92 下 p99 大概两三千,改到 64×64 距离直接缩水一半,所以换图像尺寸后必须重新统计阈值,不要沿用旧经验值。
4.3 数据集规模与每人样本数的下限
Eigenface 本质是一个线性模型,隐含假设是“同一人的不同人脸图像近似落在同一个低维子空间附近”。这个假设在每人样本太少时根本不成立——如果每人只有 1 张训练图,PCA 学习不到类内变化,光照一变就翻车。我的经验是每人至少 3~5 张不同光照/表情的样本,总数最好在 100 张以上。当你只能拿到每人一张图时,有两个补救方向:一是做数据增强(水平翻转、小幅旋转、亮度抖动)扩充样本;二是改用基于特征点对齐的方法预处理,把眼睛、嘴巴对齐到固定位置,减小类内差异。对齐这一步比换个降维算法更有效,能直接影响识别率五到十个点。
5. Eigenface 实战避坑:这五个问题我全踩过
5.1 现象:预测准确率极低,甚至只有 20%
原因八成是数据没有对齐。人脸图像中眼睛的位置如果每张都不同,PCA 会把“眼睛偏移”当作最主要的区分特征,而真正的身份差异反而被当作噪声丢掉了。解决:在加载数据后做一次简单的人眼对齐,用 OpenCV 的级联分类器检测双眼坐标,仿射变换把双眼放到固定位置后统一 resize。这个预处理步骤,比调 n_components 提升大得多。检测双眼和仿射变换的核心代码如下:
def align_face(img, left_eye, right_eye, output_size=(112, 92)): # 计算旋转角度和缩放 dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(dy, dx)) # 两眼连线中心 center = ((left_eye[0] + right_eye[0]) // 2, (left_eye[1] + right_eye[1]) // 2) eyes_distance = np.sqrt(dx**2 + dy**2) # 设定目标两眼距离,统一缩放到固定尺寸 output_eyes_distance = output_size[0] * 0.5 scale = output_eyes_distance / eyes_distance M = cv2.getRotationMatrix2D(center, angle, scale) aligned = cv2.warpAffine(img, M, output_size, flags=cv2.INTER_CUBIC) return aligned这句 M = cv2.getRotationMatrix2D(center, angle, scale) 把旋转和缩放合成在一个矩阵里,比分别执行两次变换少一次插值误差。eyes_distance 对这些浮点值敏感,注意 dx 和 dy 已经从相同的坐标系中取得。
5.2 现象:n_components 取大了,训练样本的投影矩阵是奇异矩阵,LinAlgError
原因:当 n_components 大于训练样本数 N 时,协方差矩阵 XXᵀ 是 N×N 的,最多只有 N-1 个非零特征值(因为去均值后样本线性相关),你不可能取到 N+1 个独立特征向量。解决:n_components 必须小于或等于 N-1。我在 fit 函数里加了一个保护:
n_components = min(self.n_components, centered.shape[0] - 1) self.n_components = max(n_components, 1)加了这行,就算有人在 n_components 传了 500 也不会崩。另一个隐含风险是:如果有些人脸过于相似(同一人的多张正脸),特征值可能接近 0,归一化时除以零。稳妥做法是设置一个极小值 epsilon 替代码:
5.3 现象:测试集准确率高,但实际摄像头输入识别失败
原因:测试集和数据集的拍摄条件一致,但摄像头实时画面有背景噪声、不同亮度、脸部位置偏移。Eigenface 对光照极敏感,因为 PCA 的前几个主方向往往就是光照变化方向。解决:实时识别前做直方图均衡化和光照归一化,把图像变换成标准光照下的形式:
def normalize_illumination(img): # 方法1: CLAHE 限制对比度的自适应直方图均衡化 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) img_norm = clahe.apply(img) # 方法2: 减去高斯模糊后的低频分量,提取高频细节 blurred = cv2.GaussianBlur(img_norm, (31, 31), 0) img_highfreq = cv2.subtract(img_norm, blurred) return img_highfreq我做了对比实验:不做光照归一化时,摄像头实测准确率大概 70%;做了 CLAHE 高频增强,同一模型跳到 85% 以上。但要注意,训练时也应用相同的预处理,训练和推理必须走同一条预处理管线,否则特征分布直接错位。
5.4 现象:程序跑起来内存耗尽,进程被杀.d 维特征矩阵太大
在图像一维展开后,d 可能有几万甚至几十万(180×180 的灰度图就是 32400 维)。如果直接构建 d×d 协方差矩阵,就需要约 8GB 内存。解决方式就是本章前面讲的转置技巧,只构建 N×N 矩阵。
另一个变体是使用增量 PCA(IncrementalPCA),它允许把数据分块送入,适合内存敏感的嵌入式环境:
from sklearn.decomposition import IncrementalPCA def train_incremental(faces, n_components=30, batch_size=10): ipca = IncrementalPCA(n_components=n_components, batch_size=batch_size) ipca.partial_fit(faces) # 分块拟合 return ipca # 用 sklearn 的 IncrementalPCA 配合 predict transformed_train = ipca.transform(faces)但注意 IncrementalPCA 在每次 partial_fit 时内部会维护一个协方差统计量,批次太小时数值波动大,batch_size 建议取 max(10, n_components*2)。
5.5 现象:识别时陌生人总是被错误分类为库中的某个人,且距离很小
原因:训练集的类内距离比类间距离还大,数据库人太少或同一个人样本差异太大,导致陌生人的投影距离根本得不到一个合理的分布。解决:不只设一个全局距离阈值,而是每个身份单独统计距离分布。某个人的训练样本自身差异很大(比如有戴眼镜/不戴眼镜),他的阈值就该比其他人高:
def per_person_thresholds(model, X_train, y_train, scale=1.5): thresholds = {} for person in set(y_train): idxs = np.where(y_train == person)[0] dists = [] for i in idxs: _, d = model.predict(X_train[i]) dists.append(d) # 每个身份单独阈值: 均值 + scale * 标准差 thresholds[person] = np.mean(dists) + scale * np.std(dists) return thresholds识别时先找最近邻,再用该身份的专属阈值拒识。这样至少能让对光照敏感的特定个体不再被陌生人“充当”。另外,增加训练人数也能整体改善子空间的区分度,理想情况下训练库至少在 20 人以上。
6. 进阶:从静态图片到实时视频流,以及如何验证模型真的可靠
6.1 用 OpenCV 跑实时摄像头识别:管线搭建与性能调优
从图片识别升级到视频流识别,代码只差一个视频循环,但有几个细节直接影响体验。摄像头画面里不会只有一个端正的正面脸,你需要先做人脸检测框,截取人脸区域,再送入 Eigenface 模型。我常用 OpenCV 的 DNN 人脸检测器,比 Haar 级联更稳:
import cv2 import numpy as np # 加载 Eigenface 模型 model = EigenfaceRecognizer(n_components=30) model.fit(X_train, y_train, name_map) # 加载人脸检测器(OpenCV DNN 或 Haar) face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') cap = cv2.VideoCapture(0) if not cap.isOpened(): raise IOError("无法打开摄像头") frame_skip = 0 while True: ret, frame = cap.read() if not ret: break # 每隔两帧做一次检测,降低 CPU 占用 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60)) for (x, y, w, h) in faces: # 扩展检测框,避免裁到额头边缘 margin = int(0.1 * w) x0, y0 = max(0, x - margin), max(0, y - margin) x1, y1 = min(frame.shape[1], x + w + margin), \ min(frame.shape[0], y + h + margin) face_roi = gray[y0:y1, x0:x1] face_roi = cv2.resize(face_roi, IMG_SIZE).flatten().astype(np.float32) # 预处理与识别 face_roi = normalize_illumination(face_roi.reshape(IMG_SIZE)) face_roi = face_roi.flatten() label, distance = model.predict(face_roi) name = model.name_map.get(label, "unknown") # 根据阈值判断是否显示名字 if distance <= THRESHOLD: display_name = f"{name} ({distance:.0f})" else: display_name = "unknown" cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, display_name, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('Eigenface Recognition', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这个循环里我加了 frame_skip 的影影代码逻辑思路,但实际在代码里是直接在 detectMultiScale 上控制每帧都检测——如果你 CPU 吃紧,可以改成只在偶数帧做检测并复用上一帧的 bbox。margin 扩展是血泪经验:Haar 检测框偏紧,直接把框内区域拿去 resize 会造成人脸被压缩变形,坐标特征全被打乱。最后,waitKey(1) 的 1ms 延迟控制视频流帧率,在识别循环里千万别加 sleep 或改为 waitKey(0),否则画面会卡成幻灯片。
6.2 验证可靠性:ROC 曲线与误识率测试
在说“我这个模型靠谱”之前,要有一套量化的验证方法。最常用的是在一组已知身份 + 一组陌生人上做阈值扫描,画出 ROC 曲线,找到等错误率点(EER),这个点上的阈值就是最佳阈值。
# 假设 gallery 是已知身份测试集,probe 是陌生人测试集 def compute_roc(model, known_faces, known_labels, unknown_faces, name_map): import numpy as np # 计算已知身份的距离分布 known_dists = [] for f, l in zip(known_faces, known_labels): # 这里先用最近邻距离 _, d = model.predict(f) known_dists.append(d) # 计算陌生人的距离分布 unknown_dists = [] for f in unknown_faces: _, d = model.predict(f) unknown_dists.append(d) threshold_range = np.linspace( min(known_dists + unknown_dists), max(known_dists + unknown_dists), 200) fpr_list, tpr_list = [], [] for th in threshold_range: # 已知身份: 距离小于阈值则通过 tpr = np.mean([d <= th for d in known_dists]) # 陌生人: 距离小于阈值则被误接受 fpr = np.mean([d <= th for d in unknown_dists]) fpr_list.append(fpr) tpr_list.append(tpr) # EER: TPR 与 (1-FPR) 的交点 return fpr_list, tpr_list这里关键要理解:Eigenface 模型本身没有“分类器置信度”,它只有距离。阈值的设定本质是把距离映射成“接受/拒绝”。我在做门禁类项目时,会要求 FPR 小于 1% 时的 TPR 不低于 95%,如果达不到,要么增加训练样本每人张数,要么放弃 Eigenface 改用带特征点对齐的 LBPH 或深度学习模型。很多人以为 Eigenface 能通吃所有场景,实际上它的适用边界很清晰:同人样本变化小、照度可控、角度正脸时它是性价比之王;一旦场景不可控,就别硬扛了。
6.3 一些让项目更好用的工程细节
到最后给出三个能立刻落地的技巧。第一,保存与加载模型用 NumPy 的 .npz 格式一次性压缩存取,比 pickle 更跨版本稳定:
np.savez('eigenface_model.npz', mean_face=model.mean_face, eigenfaces=model.eigenfaces, labels=model.labels, projections=model.projections, name_map=np.array(list(model.name_map.items()), dtype=object)) # 模型加载 data = np.load('eigenface_model.npz', allow_pickle=True) new_model = EigenfaceRecognizer(n_components=data['eigenfaces'].shape[1]) new_model.mean_face = data['mean_face'] new_model.eigenfaces = data['eigenfaces'] new_model.labels = data['labels'] new_model.projections = data['projections']这样重建的模型不再需要重新训练,predict 完全可用,部署时只需要一个 npz 文件和 100 行 Python 代码。第二,做多轮随机切分时固定随机种子,否则不同轮次的准确率波动会让你误以为模型有问题。第三,把训练输出的特征脸可视化保存为图片,能一眼看出 PCA 学的是什么——如果前几个特征脸是明显的边缘轮廓而不是光照梯度,很可能预处理环节出了问题。
我一直要求自己在给 Eigenface 项目收尾时至少回答三个问题:数据库里有没有足够大的身份差异?测试场景的光照变化是否被预处理吸收了?阈值是统计出来的还是拍脑袋定的?这三个问题想清楚了,这个 zip 里的代码才能真正从“跑通 demo”变成“能交付的小系统”。这个方向值得投入的时间上限也就是一两天——毕竟 Eigenface 作为经典算法,定位是理解原理、快速验证和低算力兜底,而不是跟深度学习抢精度。希望这些把代码一行行讲透的笔记能帮到你。
本文还有配套的精品资源,点击获取