OpenCV人脸识别三剑客:LBPH、EigenFaces、FisherFaces实战解析

OpenCV 人脸识别三剑客:LBPH、EigenFaces、FisherFaces 全面对比与实战(附2万字硬核解析)

🔍 导语

人脸识别早已渗透到生活的每个角落------手机解锁、机场安检、课堂签到......但你有没有好奇过,计算机是如何认出你的脸 ?在深度学习大火的今天,传统方法依然坚挺,OpenCV 自带的三种人脸识别算法 ------ LBPHEigenFacesFisherFaces ,就是入门人脸识别的最佳教材。本文将用三份简单但五脏俱全的 Python 源码,带你逐行拆解这三种方法,深入背后的数学原理与调优技巧,并通过实际运行结果揭示了它们的"脾气"与"短板"。全文 2 万字以上,信息量爆炸,建议收藏后细细品读!


目录

  1. 人脸识别:从科幻到现实
  2. [OpenCV 人脸识别模块速览](#OpenCV 人脸识别模块速览)
  3. LBPH:局部纹理统计的艺术
    • 3.1 LBPH 原理回顾
    • 3.2 代码逐行精讲
    • 3.3 运行结果解读
  4. EigenFaces:主成分分析的面孔
    • 4.1 PCA 与特征脸
    • 4.2 代码逐行精讲
    • 4.3 为何置信度爆表?
  5. FisherFaces:线性判别分析的加持
    • 5.1 LDA 原理与 Fisher 脸
    • 5.2 代码逐行精讲
    • 5.3 添加中文标签的画龙点睛
  6. 三种算法横向对比
  7. 实战调优与最佳实践
  8. 常见问题与避坑指南
  9. 从传统走向深度学习
  10. 总结与资源分享

人脸识别:从科幻到现实

在《碟中谍》系列电影中,特工们用一块隐形眼镜就能扫描人群并进行身份匹配;在《少数派报告》里,公共摄像头实时识别每一个路人。今天,这一切已不再是幻想。根据 MarketsandMarkets 预测,全球人脸识别市场将在 2025 年达到 85 亿美元。驱动这一技术的,既有深度学习,也离不开那些经典而优雅的传统算法。

本文将聚焦于 OpenCV 中内置的三种经典人脸识别器:LBPHEigenFacesFisherFaces。它们不需要 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)

代码解读:

  1. 图像读取cv2.imread('pyy1.png', cv2.IMREAD_GRAYSCALE) 直接将图像读取为灰度图。训练集包含两个人的各两张照片:索引 0 对应 pyy,索引 1 对应 qzl。标签列表 labels=[0,0,1,1] 与之匹配。
  2. 字典映射dic 将数字标签映射为人名,-1 对应"无法识别"。
  3. 待测图片predict_image 是我们要识别的对象,这里也用了灰度读取。
  4. 创建识别器cv2.face.LBPHFaceRecognizer_create(threshold=80)。这里只修改了阈值参数,radius、neighbors、grid_x、grid_y 均采用默认值(1,8,8,8)。阈值设为 80 意味着当置信度(距离)大于 80 时,会返回标签 -1
  5. 训练recognizer.train(images, np.array(labels))。注意 labels 需要是 numpy 数组。
  6. 预测predict() 返回 labelconfidence。置信度的计算方式是卡方距离,值越小越相似。代码将其结果直接打印。

运行结果:

复制代码
这人是: qzl
置信度: 77.59353695603815

虽然我们拿了一张 pyy.png 去测试,但算法认定是 qzl,并且置信度 77.59 刚好小于 80,所以没有返回"无法识别"。为什么会发生误判?一方面训练样本太少(每人只有两张),另一方面两张训练图片很可能没有很好地捕捉该人的面部变化,导致泛化能力差;也可能 pyy.pngqzl 的某张图片在 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)

代码解读:

  1. 统一尺寸 :PCA 要求所有图像具有相同的维度,因此每张图片都被 cv2.resize(120,180)。这是 EigenFaces 和 FisherFaces 的前提,而 LBPH 因为基于直方图,可以接受不同尺寸(但 OpenCV 内部建议相同)。
  2. 标签 :这里将 qzl 标记为 0,pyy 标记为 1。dic 中把 0 映射为 'hg'(可能为笔误,应为 'qzl'),1 映射为 'pyy'
  3. 创建识别器cv2.face.EigenFaceRecognizer_create(threshold=5000)threshold 设为了 5000。这里需要解释一下:EigenFaces 的置信度计算的是欧氏距离(或者有时是重构误差),理论上取值范围在 0 到无穷大。作者在注释中指出"confidence: 大小介于 0 到 20000,只要低于 5000 都被认为是可靠的结果"。因此 5000 是一个经验阈值。
  4. 预测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)

代码解读:

  1. 自定义函数 image_re :将读取并缩放后的图片追加到全局列表 images 中,避免重复代码。
  2. 中文显示函数 cv2AddChineseText :OpenCV 的 putText 不支持中文,这里借助 PIL 将中文绘制到图片上。该函数在代码中独立定义,可以作为工具函数复用。
  3. 识别器创建cv2.face.FisherFaceRecognizer_create(threshold=5000)。和 EigenFaces 一样,FisherFaces 的置信度也是欧氏距离或其变种,阈值 5000。
  4. 训练与预测 :标签 0 代表 '胡歌'(本来应该是 qzl),1 代表 '彭于晏'(应该是 pyy)。预测图像是 pyy.png,最后显示的图片是 pyy3.png(作者可能准备了一张额外的测试图)。
  5. 结果显示:将中文标签绘制到图像上,显示出来。

运行结果:

复制代码
这人是: 彭于晏
置信度为: 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_xgrid_y,使特征更细粒度。

Q5: 如何评估模型好坏?

A: 准备独立的测试集,计算 Top-1 准确率。对于开放集识别,可以绘制 ROC 或计算 F1 分数。


从传统走向深度学习

尽管本文沉浸在经典算法中,但我们也要看到,基于卷积神经网络的人脸识别(如 FaceNet、ArcFace)已经在大型数据集上达到 99.8% 以上准确率。然而,传统方法的价值在于:

  • 轻量级:适合树莓派、嵌入式设备,无需 GPU。
  • 可解释性:我们能直观理解特征提取过程。
  • 快速部署:仅需几行代码,无需大规模训练。

如果你有余力,可以尝试将传统识别器的输出作为深度学习的辅助特征,进行融合。或者先使用传统方法做粗筛,再让深度学习进行精确认证。

OpenCV 的 DNN 模块也可以加载预训练的深度学习人脸识别模型(如 OpenFace),可作为进阶方向。


总结与资源分享

通过三份小程序,我们完整体验了 OpenCV 内置的三种人脸识别算法:LBPH 的纹理统计EigenFaces 的 PCA 降维FisherFaces 的 LDA 投影。我们不仅弄懂了代码的每一行,更通过实际运行结果直观感受到了它们的优劣与适用场景。

在人工智能浪潮席卷的今天,回归经典往往能给我们带来意想不到的灵感。希望本文能成为你探索人脸识别技术的坚实起点,无论是做课程作业、参加竞赛,还是打造一个有趣的智能家居项目,这些知识都将为你所用。

文中所有源代码均可在文章内直接复制运行 ,请替换为你自己的图片路径。如果你觉得本文有帮助,欢迎点赞、收藏、关注,你的支持是我不竭的创作动力!