4. 人脸检测基础

人脸检测,说白了就是让计算机在一张图里找到人脸的位置。它是整个人脸识别系统的第一步,也是最关键的一步。你想想看,如果连脸都找不到,后面还谈什么识别?

我最早接触人脸检测是在2015年,那时候深度学习还没像现在这么普及。当时用的最多的就是Haar Cascade。虽然现在有了MTCNN、RetinaFace这些更强大的方法,但Haar Cascade依然有它的用武之地——轻量、快速、不需要GPU。在一些嵌入式设备上,它还是首选。

核心观点: Haar Cascade是传统计算机视觉中的人脸检测经典方法,基于Haar-like特征和AdaBoost级联分类器。理解它的原理,能帮你更好地理解现代检测方法。

4.1 Haar Cascade分类器原理

Haar Cascade的核心思想其实很简单:用一些简单的矩形特征来区分人脸和非人脸。这些特征就是Haar-like特征,也叫矩形特征。

常见的Haar特征有三种:

  • 边缘特征: 检测明暗变化明显的区域,比如眼睛和脸颊的边界
  • 线特征: 检测细长的明暗条纹,比如鼻梁
  • 中心特征: 检测中心与周围对比明显的区域,比如眼睛比周围暗

为什么这些特征能检测人脸?因为人脸有一些固定的明暗规律。比如:

  • 眼睛区域比脸颊暗
  • 鼻梁比两侧亮
  • 嘴巴区域比周围暗

Haar特征就是通过计算白色矩形和黑色矩形内像素和的差值,来捕捉这些规律。

我的经验: 我在项目中遇到过一个问题——光照不均匀时,Haar特征的计算结果会偏差很大。后来我用了直方图均衡化做预处理,效果好了不少。所以,预处理真的很重要。

但光有特征还不够。一张24x24的图片,Haar特征的数量能达到16万多个。如果每个特征都去计算,速度会慢得离谱。这时候就需要AdaBoost算法出场了。

AdaBoost的核心思想是:从海量特征中挑选出最有区分力的那些,组合成一个强分类器。具体来说:

  1. 给每个训练样本一个权重
  2. 训练一个弱分类器(其实就是选一个Haar特征+一个阈值)
  3. 计算这个弱分类器的错误率
  4. 增加被错分样本的权重,降低正确样本的权重
  5. 重复步骤2-4,直到达到预设的弱分类器数量
  6. 把这些弱分类器加权组合成一个强分类器

但即使这样,直接用一个强分类器去扫描整张图片,速度还是不够。Viola和Jones提出了一个巧妙的方法——级联结构。

级联结构就像是一个层层筛选的漏斗:

  • 前几层用很少的特征快速排除明显不是人脸的区域
  • 后几层用更多的特征精细判断
  • 只有通过所有层的区域才被认为是人脸

这样做的好处是:大部分非人脸区域在前几层就被排除了,只有少数候选区域需要经过全部层的判断。速度提升了10倍以上。

注意: 级联结构的层数不是越多越好。层数太多,虽然误检率低了,但漏检率会上升。我曾经调过一个30层的级联分类器,结果人脸检测率只有60%多。后来降到20层,检测率回到了85%。所以,要平衡。
Haar Cascade 级联分类器检测流程 输入图像 第1层:2个特征 第2层:5个特征 ... 第N层:20个特征 非人脸 → 排除 检测到人脸 级联结构特点 前几层:少特征,快速排除 后几层:多特征,精细判断 通过全部层 → 人脸

4.2 OpenCV实现人脸检测

OpenCV里已经训练好了Haar Cascade模型,我们直接拿来用就行。模型文件在OpenCV的安装目录下,常见的几个:

模型文件 检测目标 特点
haarcascade_frontalface_default.xml 正面人脸 最常用,检测正面效果不错
haarcascade_frontalface_alt.xml 正面人脸 比default更严格,误检率低
haarcascade_profileface.xml 侧面人脸 检测侧脸,但效果一般
haarcascade_eye.xml 眼睛 可做人眼检测

基本的使用流程是这样的:

import cv2

# 加载模型
face_cascade = cv2.CascadeClassifier(
    'haarcascade_frontalface_default.xml'
)

# 读取图片
img = cv2.imread('test.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 检测人脸
faces = face_cascade.detectMultiScale(
    gray,
    scaleFactor=1.1,
    minNeighbors=5,
    minSize=(30, 30)
)

# 绘制结果
for (x, y, w, h) in faces:
    cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)

cv2.imshow('Face Detection', img)
cv2.waitKey(0)

这段代码看起来简单,但有几个关键点需要注意:

  • 转灰度图: Haar特征是基于灰度计算的,所以要先转灰度
  • detectMultiScale: 这个函数名里的MultiScale就是多尺度检测的意思
  • 返回结果: 返回的是人脸矩形框的列表,每个框是(x, y, w, h)
我的习惯: 我一般会在检测前先做直方图均衡化,特别是光照不均匀的图片。代码就一行:gray = cv2.equalizeHist(gray)。效果很明显。

4.3 检测参数调优

detectMultiScale有四个关键参数,调好了效果天差地别。我一个个说。

参数 作用 推荐值 调大/调小的影响
scaleFactor 图像缩放比例 1.05 - 1.2 调大:速度快,但可能漏检;调小:检测更精细,但速度慢
minNeighbors 最小邻居数 3 - 6 调大:误检率低,但可能漏检;调小:检测更多,但误检多
minSize 最小人脸尺寸 (30, 30) 调大:忽略小脸,速度快;调小:能检测小脸,但误检增加
maxSize 最大人脸尺寸 不设置 一般不用设置,除非你知道人脸最大多大

scaleFactor 这个参数,我刚开始用的时候总搞不明白。它控制的是图像金字塔的缩放比例。每次缩放后,用固定大小的检测窗口去扫描。scaleFactor=1.1表示每次缩小10%。

为什么要有这个参数?因为人脸在图片里大小不一。通过缩放图片,让检测窗口能匹配到不同大小的人脸。

我曾经踩过的坑: 有一次做视频实时检测,我把scaleFactor设成了1.01,想着检测更精细。结果一帧要处理2秒,完全没法用。后来改成1.15,速度上来了,但漏检了一些小脸。最后折中到1.08,效果和速度都还行。所以,要根据实际场景来调。

minNeighbors 这个参数也很关键。它控制的是:一个候选区域周围有多少个其他检测框,才认为它是真正的人脸。

你想想看,如果minNeighbors设成1,那只要有一个检测框就算人脸。这样误检会很多。设成5,就需要周围有5个框都检测到才算。误检少了,但可能漏掉一些真实的人脸。

我一般这样调:

  • 如果误检多,增大minNeighbors
  • 如果漏检多,减小minNeighbors
  • 通常3-6之间比较合适

4.4 多尺度检测

多尺度检测,说白了就是解决一个问题:人脸在图片里大小不一样。

比如一张合影,站在前面的人脸大,后面的人脸小。如果用固定大小的检测窗口,大脸能检测到,小脸就漏了。

OpenCV的做法是构建图像金字塔:

  1. 把原图按scaleFactor逐级缩小
  2. 对每一级缩放后的图片,用固定大小的检测窗口扫描
  3. 把所有检测结果合并起来

这样做的好处是:不管人脸在图片里多大,总有一级金字塔能匹配上。

但代价也很明显——计算量大了。金字塔层数越多,需要处理的图片就越多。

性能优化建议:
  • 如果知道人脸的大致大小,可以设置minSize和maxSize,减少金字塔层数
  • scaleFactor不要太小,1.1-1.2之间比较合理
  • 可以先缩小原图再检测,比如把1080p缩到720p

我做过一个测试:一张1920x1080的图片,scaleFactor=1.1时,检测耗时约200ms;scaleFactor=1.2时,耗时约120ms。速度提升了40%,但漏检率从2%升到了5%。

所以,调参就是一个权衡的过程。没有完美的参数,只有适合你场景的参数。

我的建议: 如果你刚开始用Haar Cascade,先用默认参数跑一遍,看看效果。然后根据结果,一次只调一个参数。别上来就全调,那样你都不知道是哪个参数起了作用。

最后说一句,Haar Cascade虽然老了,但它的思想——用简单特征+级联结构实现快速检测——至今仍有借鉴意义。理解了它,你再去看YOLO、SSD这些现代检测方法,会发现很多相似之处。