从原理到代码,一篇弄懂传统人脸识别的三驾马车
前言
如果你是OpenCV初学者,在接触到人脸识别模块时,一定会遇到三个名字:LBPH、EigenFace、FisherFace。它们都位于cv2.face子模块中,用法高度相似——创建识别器、训练、预测。这种统一的API设计让我们可以轻松切换算法,但也容易产生困惑:它们到底有什么区别?在实际项目中该选哪个?
本文将从算法原理、代码实现、对比分析三个维度,带你彻底搞懂这三大人脸识别算法。
一、算法原理篇
1. EigenFace(特征脸)
核心思想:降维 + 重构
EigenFace基于主成分分析(PCA)。PCA可以找到数据中方差最大的方向(主成分),这些方向就是“特征脸”。每张人脸都可以表示为这些特征脸的线性组合,组合系数就是该人脸的“指纹”。
数学本质
将每张
W×H的人脸图像拉成一个N = W*H维的向量。对所有人脸向量计算协方差矩阵,求解特征值和特征向量。
取前
k个最大特征值对应的特征向量(即“特征脸”)。任意人脸投影到特征脸空间,得到一个
k维权重向量。识别时比较权重向量之间的欧氏距离。
来源
1987年由Sirovich和Kirby提出,1991年Turk和Pentland正式用于人脸识别。
优缺点
| 优点 | 缺点 |
|---|---|
| 算法简单,计算速度快 | 对光照、表情变化极其敏感 |
| 降维后数据量小 | 要求所有图片尺寸严格一致 |
| 可解释性强(特征脸可视化) | 不支持增量学习 |
2. FisherFace(费舍尔脸)
核心思想:最大化类间差异,最小化类内差异
FisherFace引入了线性判别分析(LDA)。LDA的目标与PCA不同:PCA追求“保留最多的信息”,而LDA追求“最好地区分不同类别”。它通过最大化类间散度矩阵与类内散度矩阵的比值,找到最佳投影方向。
数学本质
同样将人脸拉成向量。
计算类间散度矩阵
S_B和类内散度矩阵S_W。求解广义特征值问题
S_B v = λ S_W v。取前
c-1个最大特征值对应的特征向量(c为类别数)。由于
S_W通常奇异,实践中先做PCA降维,再执行LDA(即PCA+LDA两步策略)。
来源
LDA由统计学家Ronald Fisher于1936年提出,1997年Belhumeur等人将其用于人脸识别,提出FisherFace。
优缺点
| 优点 | 缺点 |
|---|---|
| 识别准确率通常高于EigenFace | 对光照敏感 |
| 对小样本集表现较好 | 要求图片尺寸一致,不支持增量学习 |
| 类别区分能力强 | 需要每个类别至少2张样本 |
3. LBPH(局部二值模式直方图)
核心思想:局部纹理描述 + 分块直方图
LBPH与前两者完全不同:它不关注全局形状,而是描述局部纹理特征。LBP算子通过比较中心像素与邻域像素的灰度值生成二进制编码,然后统计整张图的直方图作为特征。为了保留空间信息,LBPH将图像分成若干小块,分别统计每个小块的LBP直方图,最后将所有直方图拼接成一个特征向量。
计算步骤
LBP编码:对每个像素,与周围8个邻居比较,大于等于中心像素的记为1,否则为0,得到一个8位二进制数(0~255)。
分块:将LBP图像划分为
grid_x × grid_y个不重叠的块。统计直方图:对每个块统计LBP值的分布直方图(通常256个bin)。
串联特征:将所有块的直方图连接成一个长向量。
识别:使用卡方距离或直方图相交距离比较特征向量。
来源
LBP算子由Ojala等人于1996年提出,后扩展用于人脸识别。
优缺点
| 优点 | 缺点 |
|---|---|
| 对光照变化鲁棒 | 对严重遮挡和极端姿态敏感 |
| 支持增量学习(新增样本无需重训练全部) | 特征维度较高(分块数×256) |
| 不需要固定图片尺寸(但最好统一) | 旋转不变性有限 |
二、代码实战篇
OpenCV的face模块为这三种算法提供了统一的接口,以下给出完整可运行的示例。
环境准备
pip install opencv-python opencv-contrib-python numpy pillow注意:
cv2.face模块在opencv-contrib-python中,需安装contrib版本。
数据集准备
假设我们有两类人:胡歌(hg)和彭于晏(pyy),每人提供2张训练图片,1张测试图片。
项目目录/ ├── hg1.jpg ├── hg2.jpg ├── pyy1.jpg ├── pyy2.jpg └── hg.jpg (测试图)1. EigenFace 完整代码
import cv2 import numpy as np # 读取训练图片并统一尺寸 def load_image(path, size=(120, 180)): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f"无法读取图片: {path}") return cv2.resize(img, size) images = [] paths = ['hg1.jpg', 'hg2.jpg', 'pyy1.jpg', 'pyy2.jpg'] for p in paths: images.append(load_image(p)) labels = [0, 0, 1, 1] # 0:hg, 1:pyy # 读取测试图片 test_img = load_image('hg.jpg') # 创建EigenFace识别器 # num_components: 保留的主成分数量(默认0表示自动) # threshold: 置信度阈值,超过则返回-1 recognizer = cv2.face.EigenFaceRecognizer_create(num_components=80, threshold=80) # 训练 recognizer.train(images, np.array(labels)) # 预测 label, confidence = recognizer.predict(test_img) dic = {0: '胡歌', 1: '彭于晏', -1: '无法识别'} print(f"识别结果: {dic[label]}") print(f"置信度: {confidence}") # 显示结果 img_color = cv2.imread('hg.jpg') cv2.putText(img_color, dic[label], (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2) cv2.imshow('Result', img_color) cv2.waitKey(0) cv2.destroyAllWindows()结果展示:训练集过少,这里并没有成功预测
2. FisherFace 完整代码
import cv2 import numpy as np def load_image(path, size=(120, 180)): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) return cv2.resize(img, size) images = [load_image(p) for p in ['hg1.jpg', 'hg2.jpg', 'pyy1.jpg', 'pyy2.jpg']] labels = [0, 0, 1, 1] test_img = load_image('hg.jpg') # FisherFace识别器 # num_components: LDA保留的成分数,最多为类别数-1 recognizer = cv2.face.FisherFaceRecognizer_create(num_components=0, threshold=5000) recognizer.train(images, np.array(labels)) label, confidence = recognizer.predict(test_img) dic = {0: '胡歌', 1: '彭于晏', -1: '未知'} print(f"识别结果: {dic[label]}, 置信度: {confidence}")结果展示:
3. LBPH 完整代码
import cv2 import numpy as np # LBPH不需要强制resize,但建议统一 def load_image(path, size=(120, 180)): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(path) return cv2.resize(img, size) images = [load_image(p) for p in ['hg1.jpg', 'hg2.jpg', 'pyy1.jpg', 'pyy2.jpg']] labels = [0, 0, 1, 1] test_img = load_image('hg.jpg') # LBPH识别器参数说明: # radius: LBP采样半径,默认1 # neighbors: 邻域采样点数,默认8 # grid_x, grid_y: 水平/垂直方向分块数,默认8 # threshold: 阈值,默认DBL_MAX recognizer = cv2.face.LBPHFaceRecognizer_create(radius=1, neighbors=8, grid_x=8, grid_y=8, threshold=80) recognizer.train(images, np.array(labels)) label, confidence = recognizer.predict(test_img) dic = {0: '胡歌', 1: '彭于晏', -1: '未知'} print(f"识别结果: {dic[label]}, 置信度: {confidence}")三、代码对比与本质区别
3.1 相同的流程
三个脚本都遵循“读取训练集 → 创建识别器 → 训练 → 预测”的模式。这种统一封装使得我们只需要修改一行创建识别器的代码,就能切换算法,非常便于实验对比。
3.2 核心差异点
| 对比维度 | EigenFace | FisherFace | LBPH |
|---|---|---|---|
| 创建函数 | EigenFaceRecognizer_create | FisherFaceRecognizer_create | LBPHFaceRecognizer_create |
| 是否强制resize | 是(PCA要求向量长度一致) | 是(LDA要求一致) | 否(但建议resize) |
| 置信度范围 | 0~20000 | 0~20000 | 0~200左右 |
| 典型阈值 | 80~5000 | 5000左右 | 80~120 |
| 增量学习 | 不支持 | 不支持 | 支持(通过update方法) |
| 对光照鲁棒性 | 差 | 一般 | 好 |
| 计算复杂度 | 低 | 中 | 中(取决于分块数) |
3.3 为什么需要不同的阈值?
EigenFace/FisherFace返回的
confidence是待测样本与最近邻样本之间的欧氏距离。距离取决于特征空间的尺度,可能很大(如几千)。因此阈值要设得较大。LBPH返回的是卡方距离(Chi-Square Distance)或直方图相交距离。直方图值范围0~255,分块后距离通常在0~200之间。所以阈值设为80~120较合理。
如何确定合适的阈值?可以用一批已知身份的人脸图片(但不在训练集中)进行测试,观察
confidence的分布,然后设置一个使错误接受率(FAR)可接受的阈值。
四、如何选择适合的算法?
在实际项目中,可以根据以下场景进行选择:
| 应用场景 | 推荐算法 | 理由 |
|---|---|---|
| 光照变化大(如户外、监控) | LBPH | 对光照最鲁棒 |
| 实时性要求极高(如嵌入式设备) | EigenFace | 计算最快,模型小 |
| 小样本集(每人1~2张图) | FisherFace | LDA在小样本下表现优于PCA |
| 需要动态增加新用户(不能重训练) | LBPH | 支持增量学习 |
| 表情/姿态变化大 | LBPH | 局部纹理比全局形状稳定 |
| 要求识别精度最高(传统方法中) | FisherFace | 通常优于EigenFace |
如果你的项目追求极致识别率,建议直接使用深度学习(如FaceNet、ArcFace)。但传统方法在资源受限或快速原型验证时仍然非常有用。
五、常见问题与排坑指南
Q1:训练时提示error: (-215:Assertion failed) images[i].size() == images[0].size()
原因:EigenFace和FisherFace要求所有训练图片尺寸完全相同。
解决:统一使用cv2.resize将所有图片缩放到同一尺寸。
Q2:预测总是返回-1或置信度特别大
原因:阈值设置过小,或者测试图片与训练集差异太大(光照、角度、尺寸)。
解决:
先打印出
confidence,观察正常匹配时的值,然后合理设置threshold。确保测试图片与训练图片的预处理一致(灰度、缩放)。
Q3:cv2.face找不到
原因:未安装opencv-contrib-python,或者版本太旧。
解决:
pip uninstall opencv-python opencv-contrib-python pip install opencv-contrib-python==4.5.5.64Q4:LBPH 的训练图片可以大小不一吗?
理论上可以,因为LBPH是基于局部直方图的,最终特征向量长度只与分块数有关,与原始图片尺寸无关。但为了公平比较和稳定性能,建议也resize到统一尺寸。
六、总结
EigenFace:基于PCA的全局方法,速度快但对光照敏感,适合光照稳定、快速响应的场景。
FisherFace:基于LDA的判别方法,分类能力最强,是小样本场景下的首选。
LBPH:基于局部纹理的直方图方法,鲁棒性好、支持增量学习,是目前OpenCV传统人脸识别中最实用的选择。
通过本文的原理讲解和代码实战,你应该能够理解这三者的本质区别,并能根据实际需求灵活选用。下一步,你可以尝试用这三种算法分别跑同一个测试集(比如不同光照、不同表情的人脸),直观感受它们在置信度和鲁棒性上的差异。
如果你觉得本文对你有帮助,欢迎点赞、收藏、评论交流!你的支持是我持续输出优质技术文章的动力。
参考资料
OpenCV官方文档:
cv::face::FaceRecognizerTurk, M., Pentland, A. "Eigenfaces for recognition", 1991.
Belhumeur, P. et al. "Eigenfaces vs. Fisherfaces", 1997.
Ojala, T. et al. "Multiresolution gray-scale and rotation invariant texture classification with local binary patterns", 2002.