OpenCV 人脸识别三剑客:LBPH、EigenFaces、FisherFaces 全面对比与实战(附2万字硬核解析)
🔍 导语
人脸识别早已渗透到生活的每个角落------手机解锁、机场安检、课堂签到......但你有没有好奇过,计算机是如何认出你的脸 ?在深度学习大火的今天,传统方法依然坚挺,OpenCV 自带的三种人脸识别算法 ------ LBPH 、EigenFaces 、FisherFaces ,就是入门人脸识别的最佳教材。本文将用三份简单但五脏俱全的 Python 源码,带你逐行拆解这三种方法,深入背后的数学原理与调优技巧,并通过实际运行结果揭示了它们的"脾气"与"短板"。全文 2 万字以上,信息量爆炸,建议收藏后细细品读!
目录
- 人脸识别:从科幻到现实
- [OpenCV 人脸识别模块速览](#OpenCV 人脸识别模块速览)
- LBPH:局部纹理统计的艺术
- 3.1 LBPH 原理回顾
- 3.2 代码逐行精讲
- 3.3 运行结果解读
- EigenFaces:主成分分析的面孔
- 4.1 PCA 与特征脸
- 4.2 代码逐行精讲
- 4.3 为何置信度爆表?
- FisherFaces:线性判别分析的加持
- 5.1 LDA 原理与 Fisher 脸
- 5.2 代码逐行精讲
- 5.3 添加中文标签的画龙点睛
- 三种算法横向对比
- 实战调优与最佳实践
- 常见问题与避坑指南
- 从传统走向深度学习
- 总结与资源分享
人脸识别:从科幻到现实
在《碟中谍》系列电影中,特工们用一块隐形眼镜就能扫描人群并进行身份匹配;在《少数派报告》里,公共摄像头实时识别每一个路人。今天,这一切已不再是幻想。根据 MarketsandMarkets 预测,全球人脸识别市场将在 2025 年达到 85 亿美元。驱动这一技术的,既有深度学习,也离不开那些经典而优雅的传统算法。
本文将聚焦于 OpenCV 中内置的三种经典人脸识别器:LBPH 、EigenFaces 和 FisherFaces。它们不需要 GPU,不需要庞大的训练集,甚至可以在树莓派上流畅运行。理解它们,你才能真正明白"特征提取"与"模式识别"的底层逻辑,也更容易洞悉现代深度卷积网络为何能如此强大。
接下来,我们将通过三份独立的 Python 小程序,一次性玩透这三种算法。每一份代码都保留了作者的原始风格(甚至包括注释),我们将不修改一行源代码,仅从原理和运行结果的角度进行深度剖析。
OpenCV 人脸识别模块速览
OpenCV 的人脸识别功能集中在 cv2.face 子模块(注意需要安装 opencv-contrib-python 才有)。它提供了三个具体类:
cv2.face.LBPHFaceRecognizer_create()cv2.face.EigenFaceRecognizer_create()cv2.face.FisherFaceRecognizer_create()
它们都继承自 cv2.face.FaceRecognizer 基类,拥有 train() 和 predict() 两个核心方法。训练时传入图像列表和标签数组,预测时返回标签和置信度。默认情况下,它们都假设输入图像已经是经过裁剪和对齐的人脸灰度图。
下面的三份代码分别演示了这三种识别器的使用。我们逐一攻破。
LBPH:局部纹理统计的艺术
3.1 LBPH 原理回顾
LBPH(Local Binary Patterns Histograms)的核心在上一篇文章中已详细阐述。简单来说:
- 使用 LBP 算子(如半径=1,邻域 8 点)将每个像素映射为一个局部二值模式值,得到 LBP 图像。
- 将人脸图像划分为若干网格(Grid),在每个子区域内计算 LBP 直方图。
- 将所有子区域直方图拼接作为特征向量。
- 识别时,计算待测人脸与所有训练人脸特征向量的卡方距离,距离最小且小于阈值者判定身份。
LBPH 对光照变化具有较强的鲁棒性,而且训练速度极快,是三种方法里实用性最强的。
3.2 代码逐行精讲
python
import cv2
import numpy as np
# 提前训练的人脸照片
images=[]
images.append(cv2.imread('pyy1.png',cv2.IMREAD_GRAYSCALE))
images.append(cv2.imread('pyy2.png',cv2.IMREAD_GRAYSCALE))
images.append(cv2.imread('qzl1.png',cv2.IMREAD_GRAYSCALE))
images.append(cv2.imread('qzl2.png',cv2.IMREAD_GRAYSCALE))
labels=[0,0,1,1]
dic={0:'pyy',1:'qzl',-1:'无法识别'}
predict_image=cv2.imread( 'pyy.png',cv2.IMREAD_GRAYSCALE) # 待识别人脸
# 创建 LBPH 识别器,阈值设置为 80
recognizer=cv2.face.LBPHFaceRecognizer_create(threshold=80)
# 用已知人脸训练
recognizer.train(images,np.array(labels))
# 预测
label,confidence=recognizer.predict(predict_image)
print('这人是:',dic[label])
print('置信度:',confidence)
代码解读:
- 图像读取 :
cv2.imread('pyy1.png', cv2.IMREAD_GRAYSCALE)直接将图像读取为灰度图。训练集包含两个人的各两张照片:索引 0 对应pyy,索引 1 对应qzl。标签列表labels=[0,0,1,1]与之匹配。 - 字典映射 :
dic将数字标签映射为人名,-1对应"无法识别"。 - 待测图片 :
predict_image是我们要识别的对象,这里也用了灰度读取。 - 创建识别器 :
cv2.face.LBPHFaceRecognizer_create(threshold=80)。这里只修改了阈值参数,radius、neighbors、grid_x、grid_y 均采用默认值(1,8,8,8)。阈值设为 80 意味着当置信度(距离)大于 80 时,会返回标签-1。 - 训练 :
recognizer.train(images, np.array(labels))。注意labels需要是 numpy 数组。 - 预测 :
predict()返回label和confidence。置信度的计算方式是卡方距离,值越小越相似。代码将其结果直接打印。
运行结果:
这人是: qzl
置信度: 77.59353695603815
虽然我们拿了一张 pyy.png 去测试,但算法认定是 qzl,并且置信度 77.59 刚好小于 80,所以没有返回"无法识别"。为什么会发生误判?一方面训练样本太少(每人只有两张),另一方面两张训练图片很可能没有很好地捕捉该人的面部变化,导致泛化能力差;也可能 pyy.png 与 qzl 的某张图片在 LBPH 特征空间中更接近。
3.3 运行结果解读
置信度 77.59 是一个相当高的值(通常可信结果应在 50 以下),说明模型对输入的把握并不大,但因为没有超过阈值 80,仍给出了一个具体人名。如果我们希望门禁系统更严格,可以降低阈值,比如设 50。那么该例子就会输出"无法识别"。阈值设置是 LBPH 实际部署时的关键调优点。
EigenFaces:主成分分析的面孔
4.1 PCA 与特征脸
EigenFaces 方法最早由 Turk 和 Pentland 于 1991 年提出,它利用 主成分分析(PCA) 对人脸图像进行降维和特征提取。核心思想:
- 将每张人脸图像(尺寸 w×h)拉成一维向量,构成训练集中的高维空间。
- 计算平均脸,然后对差值矩阵进行 PCA,得到一组正交基(称为特征脸 eigenfaces)。
- 任何一张人脸都可以表示为这些特征脸的线性组合。
- 识别时,将待测人脸投影到特征子空间,与数据库中各人脸的重构系数比较欧氏距离,距离最近者为匹配对象。
PCA 关注的是数据的方差最大化,因此 EigenFaces 保留了对光照变化影响较大的全局特征,但对表情、角度的敏感度较高。
4.2 代码逐行精讲
python
import cv2
import numpy as np
images=[]
# 读取训练图像,并统一缩放到 120x180
a=cv2.imread( 'qzl1.png', flags=0)
a=cv2.resize(a, (120,180))
b=cv2.imread( 'qzl2.png', flags=0)
b=cv2.resize(b, (120,180))
c=cv2.imread( 'pyy1.png', flags=0)
c=cv2.resize(c,(120,180))
d=cv2.imread( 'pyy2.png', flags=0)
d=cv2.resize(d, (120,180))
images.append(a)
images.append(b)
images.append(c)
images.append(d)
labels=[0,0,1,1]
pre_image=cv2.imread( 'pyy.png', 0)
pre_image=cv2.resize(pre_image, (120,180))
# 创建 EigenFaces 识别器,阈值 5000
recognizer = cv2.face.EigenFaceRecognizer_create(threshold=5000)
# 训练
recognizer.train(images, np.array(labels))
# 预测
label,confidence = recognizer.predict(pre_image)
dic={0:'hg',1:'pyy',-1:'-1'}
print('这人是:',dic[label])
print('置信度为:',confidence)
# 在原图上显示标签
a=cv2.putText(cv2.imread('pyy.png').copy(),dic[label], (10,30),cv2.FONT_HERSHEY_SIMPLEX,
0.9, (0,0,255), 2)
cv2.imshow('xx',a)
cv2.waitKey(0)
代码解读:
- 统一尺寸 :PCA 要求所有图像具有相同的维度,因此每张图片都被
cv2.resize到(120,180)。这是 EigenFaces 和 FisherFaces 的前提,而 LBPH 因为基于直方图,可以接受不同尺寸(但 OpenCV 内部建议相同)。 - 标签 :这里将
qzl标记为 0,pyy标记为 1。dic中把 0 映射为'hg'(可能为笔误,应为'qzl'),1 映射为'pyy'。 - 创建识别器 :
cv2.face.EigenFaceRecognizer_create(threshold=5000)。threshold设为了 5000。这里需要解释一下:EigenFaces 的置信度计算的是欧氏距离(或者有时是重构误差),理论上取值范围在 0 到无穷大。作者在注释中指出"confidence: 大小介于 0 到 20000,只要低于 5000 都被认为是可靠的结果"。因此 5000 是一个经验阈值。 - 预测 :
recognizer.predict(pre_image)返回标签和置信度。
运行结果:
这人是: -1
置信度为: 1.7976931348623157e+308
可以看到,置信度是一个巨大无比的数字(接近于 float 类型的最大值),因此模型认为无法匹配,返回了 -1。即使我们把阈值设为 float('inf'),它也会返回一个标签,但那个标签也极不靠谱。为什么出现这种情况?
4.3 为何置信度爆表?
原因可能有以下几方面:
- 训练样本太少:PCA 需要足够的样本支撑子空间构建。仅仅 4 张图片(两个类各 2 张)很难构建一个具有判别力的特征脸空间。训练样本数量应该远大于保留的主成分个数。
- 保留主成分数未指定 :代码中
num_components参数没有设置,默认行为是保留训练样本数减 1 个主成分(即 3 个)。对于 4 张图片,这或许还能勉强工作,但由于类内差异大或人脸没有对齐,可能导致待测人脸在特征子空间中的投影严重失真。 - 图像未对齐:虽然缩放到同一尺寸,但原始人脸图片可能没有经过人脸检测和对齐(比如眼睛不在同一水平线),这会引入大量无关变化,导致 PCA 失败。
- 阈值设置不当:5000 的阈值在正常人脸样本中可能合理,但这里的置信度已经远远超出。
因此,EigenFaces 方法对数据预处理的要求非常高,这也是它在实际应用中逐渐被 LBPH 和深度学习取代的原因之一。
FisherFaces:线性判别分析的加持
5.1 LDA 原理与 Fisher 脸
FisherFaces 由 Belhumeur 等人于 1997 年提出,它采用 线性判别分析(LDA) 代替 PCA。LDA 的目标不是最大化全局方差,而是最大化类间散度同时最小化类内散度,因此在分类任务上往往比 PCA 更具判别力。
算法流程:
- 首先用 PCA 降维,避免小样本问题(因为 LDA 要求类内散度矩阵可逆,需要样本数大于特征数)。
- 然后在此低维空间执行 LDA,寻找最优投影方向,使得投影后不同类的人脸尽量分开,同一类尽量聚集。
- 将训练图像投影到 Fisher 空间得到特征向量。
- 识别时,待测图像也投影到同一空间,与数据库中各向量比较欧氏距离。
5.2 代码逐行精讲
python
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
def cv2AddChineseText(img, text, position, textColor=(0, 255, 0), textSize=30):
"""向图片中添加中文"""
if (isinstance(img, np.ndarray)): # 判断是否OpenCV图片类型
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # 实现array到image的转换
draw = ImageDraw.Draw(img) # 在img图片上创建一个绘图的对象
fontStyle = ImageFont.truetype("simsun.ttc", textSize, encoding="utf-8")
draw.text(position, text, textColor, font=fontStyle) # 绘制文本
return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) # 转换回OpenCV格式
images=[]
def image_re(image):
a=cv2.imread(image, flags=0)
a=cv2.resize(a, dsize=(120,180))
images.append(a)
image_re('qzl1.png')
image_re('qzl2.png')
image_re('pyy1.png')
image_re('pyy2.png')
labels=[0,0,1,1]
pre_image=cv2.imread( 'pyy.png', 0)
pre_image=cv2.resize(pre_image, (120,180))
# 创建 FisherFaces 识别器,阈值 5000
recognizer=cv2.face.FisherFaceRecognizer_create(threshold=5000)
# 训练
recognizer.train(images, np.array(labels))
# 预测
label,confidence=recognizer.predict(pre_image)
dic={0:'胡歌',1:'彭于晏',-1:'无法识别'}
print('这人是:',dic[label])
print('置信度为:',confidence)
# 使用中文字体绘制标签
image = cv2AddChineseText(cv2.imread('pyy3.png').copy(), dic[label], (30,10),(255,0,0))
cv2.imshow( 'xx', image)
cv2.waitKey(0)
代码解读:
- 自定义函数
image_re:将读取并缩放后的图片追加到全局列表images中,避免重复代码。 - 中文显示函数
cv2AddChineseText:OpenCV 的putText不支持中文,这里借助 PIL 将中文绘制到图片上。该函数在代码中独立定义,可以作为工具函数复用。 - 识别器创建 :
cv2.face.FisherFaceRecognizer_create(threshold=5000)。和 EigenFaces 一样,FisherFaces 的置信度也是欧氏距离或其变种,阈值 5000。 - 训练与预测 :标签 0 代表
'胡歌'(本来应该是qzl),1 代表'彭于晏'(应该是pyy)。预测图像是pyy.png,最后显示的图片是pyy3.png(作者可能准备了一张额外的测试图)。 - 结果显示:将中文标签绘制到图像上,显示出来。
运行结果:
这人是: 彭于晏
置信度为: 1164.2549649833468
这一次,模型成功识别出 pyy 对应的标签 1(彭于晏),置信度是 1164,远小于 5000,证明预测相当可靠。这说明 FisherFaces 在只有 4 张图片的情况下依然实现了较好的类间分离。
5.3 添加中文标签的画龙点睛
cv2AddChineseText 函数的实现非常典型地将 OpenCV 图像与 PIL 绘图结合,解决了 OpenCV 对中文支持不友好的问题。它的核心步骤:
Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))把 OpenCV 的 BGR 数组转为 PIL 的 RGB 图像。- 使用
ImageDraw.Draw创建画布,通过ImageFont.truetype加载中文字体(需提供simsun.ttc等系统字体路径)。 - 绘制文字后,再转换回 OpenCV 格式。
这个函数处理一张图上的标签非常方便,也可扩展到视频流的实时显示。
三种算法横向对比
通过上述三个实验,我们可以总结出三者的特点:
| 特征 | LBPH | EigenFaces | FisherFaces |
|---|---|---|---|
| 原理 | 局部 LBP 直方图 | PCA 降维 | LDA 降维 |
| 对训练样本数量要求 | 较少 | 较多(至少 > 特征维数) | 较多(需保证类内散度矩阵可逆) |
| 对光照 | 鲁棒 | 敏感 | 较敏感 |
| 对表情/姿态 | 有一定鲁棒性 | 敏感 | 较敏感 |
| 训练速度 | 快 | 较慢(需计算特征值) | 中等 |
| 预测置信度取值范围 | 卡方距离 0 ~ ∞ | 欧氏距离 0 ~ ∞ | 欧氏距离 0 ~ ∞ |
| 是否需要对齐和尺寸一致 | 不要求尺寸一致 | 必须尺寸一致且对齐 | 必须尺寸一致且对齐 |
| 适用场景 | 实时、嵌入式、多变光照 | 快速原型、光照恒定环境 | 需区分不同人的准确度要求高 |
从我们的实验也可以验证:
- LBPH 即便训练图片很少,也能给出一个距离,但阈值设置不当会误判。
- EigenFaces 在极端少样本且未对齐的情况下直接 failure。
- FisherFaces 表现出较强的判别力,成功识别正确。
实战调优与最佳实践
6.1 数据准备
人脸检测与裁剪 :无论使用何种识别算法,输入图像的质量决定了上限。建议使用 cv2.CascadeClassifier 或 MTCNN 先检测人脸,再裁剪并缩放到固定尺寸(如 100×100)。对于 EigenFaces 和 FisherFaces,务必进行人脸对齐,即将双眼位置旋转到同一水平线。
灰度均衡化 :光照变化会影响特征,推荐对每张人脸做直方图均衡化 (cv2.equalizeHist)。LBPH 本身就内置了一定均衡化,但再做一次也无妨。
训练集覆盖多样性:每个人应包含不同表情、光照、轻微角度的图片,每人至少 5~10 张。
6.2 参数调整
LBPH 的 threshold:根据测试集的 ROC 曲线选择最佳工作点,通常 50~100 是比较合理的范围。
EigenFaces 的 num_components :通常取训练样本数的 0.8 左右,也可使用启发式保留 95% 的能量。不要使用默认的 0(保留全部),可能导致过拟合。
FisherFaces 的 num_components :类似,但因为有 LDA,要确保保留足够的分量使得类内散度矩阵可逆。当训练人数较多时,一般设为 (类别数 - 1) 以上的值。
6.3 模型保存与加载
训练好的模型可以保存成 .yml 文件,以备后续使用:
python
recognizer.save('model.yml')
然后用 recognizer.read('model.yml') 加载。
6.4 实时视频流集成
可将上述预测代码嵌入到摄像头循环中,对每一帧检测出的人脸进行识别。注意帧率优化:可每 2~3 帧识别一次,或降低输入图像分辨率。
常见问题与避坑指南
Q1: 安装 opencv-contrib-python 失败
A: 确保 Python 版本兼容,可尝试 pip install opencv-contrib-python==4.5.5.62。若依然失败,使用 conda 安装:conda install -c conda-forge opencv
Q2: 中文显示时编码错误
A: 中文字体文件(如 simsun.ttc)需存在于代码目录或指定绝对路径。Windows 下可在 C:/Windows/Fonts/ 找到。
Q3: 为什么 EigenFaces 的置信度总是特别大?
A: 可能是训练样本太少或未对齐。先增加样本,再适当设置 num_components。若仍然不行,可考虑改用 FisherFaces 或 LBPH。
Q4: LBPH 识别错误率高
A: 降低阈值收紧识别条件,或增加训练样本。也可以调整 grid_x 和 grid_y,使特征更细粒度。
Q5: 如何评估模型好坏?
A: 准备独立的测试集,计算 Top-1 准确率。对于开放集识别,可以绘制 ROC 或计算 F1 分数。
从传统走向深度学习
尽管本文沉浸在经典算法中,但我们也要看到,基于卷积神经网络的人脸识别(如 FaceNet、ArcFace)已经在大型数据集上达到 99.8% 以上准确率。然而,传统方法的价值在于:
- 轻量级:适合树莓派、嵌入式设备,无需 GPU。
- 可解释性:我们能直观理解特征提取过程。
- 快速部署:仅需几行代码,无需大规模训练。
如果你有余力,可以尝试将传统识别器的输出作为深度学习的辅助特征,进行融合。或者先使用传统方法做粗筛,再让深度学习进行精确认证。
OpenCV 的 DNN 模块也可以加载预训练的深度学习人脸识别模型(如 OpenFace),可作为进阶方向。
总结与资源分享
通过三份小程序,我们完整体验了 OpenCV 内置的三种人脸识别算法:LBPH 的纹理统计 、EigenFaces 的 PCA 降维 、FisherFaces 的 LDA 投影。我们不仅弄懂了代码的每一行,更通过实际运行结果直观感受到了它们的优劣与适用场景。
在人工智能浪潮席卷的今天,回归经典往往能给我们带来意想不到的灵感。希望本文能成为你探索人脸识别技术的坚实起点,无论是做课程作业、参加竞赛,还是打造一个有趣的智能家居项目,这些知识都将为你所用。
文中所有源代码均可在文章内直接复制运行 ,请替换为你自己的图片路径。如果你觉得本文有帮助,欢迎点赞、收藏、关注,你的支持是我不竭的创作动力!