查看全课大纲(第 23 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

人脸检测的传统方法

基于 Haar 特征与级联分类器的人脸检测

小象实战讲义 · 基于深度学习的计算机视觉

在深度学习兴起之前,计算机视觉领域已发展出许多经典且高效的算法来解决实际问题。人脸检测,作为计算机视觉中最经典的任务之一,其早期解决方案巧妙地结合了特征工程与集成学习思想,实现了在有限算力下的实时检测。本节我们将深入剖析这一经典方法的核心:Haar-like特征、积分图加速计算,以及由AdaBoost驱动的级联分类器架构。理解这些原理,不仅是对历史的回顾,更是对特征设计、模型集成思想的深刻洞察。

💡 核心导读

  • Haar-like特征:一种通过黑白矩形区域的像素和差值来捕捉图像局部对比度(如眼睛与脸颊)的简单特征模板。
  • 积分图:一种高效的图像预处理技术,通过一次遍历图像,即可在常数时间内计算任意矩形区域的像素和,是Haar特征快速计算的关键。
  • 弱分类器与AdaBoost:将大量简单的Haar特征作为“弱分类器”,通过AdaBoost算法迭代地挑选并加权组合,构建出强大的“强分类器”。
  • 级联分类器:将多个强分类器串联,形成“过五关斩六将”的检测流程,通过早期快速拒绝非人脸区域,实现检测速度的飞跃。

Haar-like特征:捕捉人脸的“明暗密码”

在深度学习之前,人脸检测的核心挑战在于如何设计一种能够有效区分人脸与非人脸区域的特征。Haar-like特征应运而生,其灵感来源于Haar小波,但形式更为直观。

Haar-like特征本质上是一种特征模板,由白色和黑色两种矩形区域构成。其特征值定义为白色矩形区域内所有像素灰度值之和,减去黑色矩形区域内所有像素灰度值之和。

import numpy as np

# 假设我们有一个6x6的灰度图像块,模拟人脸某个区域(例如眼睛下方)
# 数值越大代表越亮(白),越小代表越暗(黑)
image_patch = np.array([
    [10, 10, 10, 200, 200, 200],
    [10, 10, 10, 200, 200, 200],
    [10, 10, 10, 200, 200, 200],
    [15, 15, 15, 210, 210, 210],
    [15, 15, 15, 210, 210, 210],
    [15, 15, 15, 210, 210, 210]
], dtype=np.float32)

# 定义一个简单的Haar-like特征模板:左边3列是“黑”矩形,右边3列是“白”矩形(模拟边缘特征)
# 计算特征值:白区域和 - 黑区域和
white_region = image_patch[:, 3:6]  # 右边三列
black_region = image_patch[:, 0:3]  # 左边三列

haar_value = np.sum(white_region) - np.sum(black_region)
print(f"Haar-like特征值 (白-黑): {haar_value}")
Haar-like特征值 (白-黑): 3465.0

这个较大的正值表明,该图像块的右侧明显比左侧更亮。这正是人脸侧面光照或眼睛与脸颊区域对比度的典型体现。类似地,鼻梁区域本身较亮、两侧较暗,用一个“黑-白-黑”的竖直线性模板滑动上去,也会得到较大的响应。可见 Haar 模板并非万能特征,而是靠大量不同位置、尺度和类型的模板去“扫描”人脸的结构规律。

根据矩形区域的不同组合方式,Haar-like特征可以分为几种基本类型,用于捕捉不同模式的结构信息:

  1. 边缘特征:用于检测图像中明暗分明的边缘,如人脸轮廓、发际线。
  2. 线性特征:用于检测细长的线条状结构,如鼻梁(两侧暗,中间亮)。
  3. 中心环绕特征:用于检测中心区域与周围区域的亮度差异。
  4. 特定方向特征:如对角线特征,用于捕捉更复杂的纹理模式。

下图展示了部分Haar-like特征模板:

边缘特征(2矩形):
[白][黑]   [白]
           [黑]

线性特征(3矩形):
[白][黑][白]   [白]
               [黑]
               [白]

中心环绕特征(中心与四周):
[黑][黑][黑]
[黑][白][黑]
[黑][黑][黑]

对角线(四矩形)特征:
[黑][白]
[白][黑]

这些简单的模板,当滑动、缩放并应用到图像的不同位置时,能够构成一个庞大的特征池,为后续的分类器提供丰富的判别信息。按四类基本模板在不同位置与尺度穷举计数,仅 16×16 的检测窗口就有约 3.2 万个 Haar 特征;窗口扩大到 36×36 时,特征数增至约 80 万个。正是这个量级,使积分图和特征筛选成为必需。

积分图:让特征计算“飞起来”

直接计算Haar特征值需要反复对图像像素进行求和,当特征模板数量达到数万甚至数十万时,计算成本将变得无法承受。积分图(Integral Image)技术完美地解决了这个问题。

积分图是一种数据结构和算法,它允许我们在常数时间(O(1))内计算出图像中任何矩形区域的像素和。其构建规则如下:

  • 积分图通常比原图多一行一列(第0行、第0列全为0)。位于位置(y, x)的值ii[y, x],是原图像i中从左上角(0,0)到(x-1, y-1)所围成的矩形区域内所有像素值的和。

数学表示为: ii[y, x] = Σ_{y'<y, x'<x} i[y', x']

一旦计算出整张图像的积分图,计算任意矩形区域D(顶点为(x1, y1), (x2, y2),均含端点,且x2 ≥ x1, y2 ≥ y1)的像素和sum(D)就变得异常简单:

sum(D) = ii[y2+1, x2+1] - ii[y1, x2+1] - ii[y2+1, x1] + ii[y1, x1]

import numpy as np

# 1. 构建一个小图像的积分图
def compute_integral_image(img):
    # 积分图通常比原图多一行一列(上边和左边填充0),便于边界计算
    h, w = img.shape
    ii = np.zeros((h+1, w+1), dtype=np.float32)
    # 逐行逐列计算累积和
    for y in range(1, h+1):
        row_sum = 0.0
        for x in range(1, w+1):
            row_sum += img[y-1, x-1]
            ii[y, x] = ii[y-1, x] + row_sum
    return ii

# 一个简单的5x5图像
small_img = np.array([
    [1, 2, 3, 4, 5],
    [6, 7, 8, 9, 10],
    [11, 12, 13, 14, 15],
    [16, 17, 18, 19, 20],
    [21, 22, 23, 24, 25]
], dtype=np.float32)

integral_img = compute_integral_image(small_img)
print("积分图 (仅显示部分,格式为[y,x]):")
print(integral_img[1:6, 1:6]) # 显示与原图对应的部分

# 2. 使用积分图快速计算矩形区域和
# 计算原图中矩形区域 (y1:1, x1:1) 到 (y2:3, x2:3) 的和,即原图[1:4, 1:4]
x1, y1 = 1, 1
x2, y2 = 3, 3
# 注意积分图坐标偏移了1
sum_rect = (integral_img[y2+1, x2+1] - integral_img[y1, x2+1] -
            integral_img[y2+1, x1] + integral_img[y1, x1])
print(f"\n矩形区域({y1}:{y2}, {x1}:{x2})的像素和 (通过积分图): {sum_rect}")

# 3. 验证:直接求和
direct_sum = np.sum(small_img[y1:y2+1, x1:x2+1])
print(f"矩形区域({y1}:{y2}, {x1}:{x2})的像素和 (直接计算): {direct_sum}")
print(f"两者是否一致? {np.isclose(sum_rect, direct_sum)}")
积分图 (仅显示部分,格式为[y,x]):
[[  1.   3.   6.  10.  15.]
 [  7.  16.  27.  40.  55.]
 [ 18.  39.  63.  90. 120.]
 [ 34.  72. 114. 160. 210.]
 [ 55. 115. 180. 250. 325.]]

矩形区域(1:3, 1:3)的像素和 (通过积分图): 117.0
矩形区域(1:3, 1:3)的像素和 (直接计算): 117.0
两者是否一致? True

可以看到,无论矩形多大,通过积分图计算像素和都只需要四次加减运算。对于Haar特征(由几个矩形组成),计算其特征值也只需要常数次积分图查询,从而实现了海量特征的高效计算。

从弱分类器到强分类器:AdaBoost的集成艺术

单个Haar特征(即一个特定位置、特定尺度的模板)的判别能力非常有限,可能只比随机猜测(50%正确率)好一点点,因此它被称为一个弱分类器。弱分类器h(x)可以表示为: h(x) = +1, if f(x) < θ = -1, otherwise 其中f(x)是该Haar特征在样本x上的特征值,θ是决策阈值。

如何将成千上万个“弱”的Haar特征组合成一个“强”的人脸检测器?答案就是AdaBoost(Adaptive Boosting)算法。

AdaBoost的核心思想是:

  1. 迭代训练:每一轮都在当前训练样本的权重分布下,从海量Haar特征中挑选出一个分类误差最低的弱分类器。
  2. 更新权重:增加被当前弱分类器分错的样本的权重,减少分对样本的权重。这样,下一轮训练就会更关注那些“难分”的样本。
  3. 加权组合:将每一轮选出的弱分类器,根据其分类准确率赋予一个权重α,然后线性组合起来,形成最终的强分类器H(x): H(x) = sign( Σ_{t=1}^{T} α_t * h_t(x) )
import numpy as np

# 模拟AdaBoost的核心思想:通过调整样本权重,聚焦于难分样本
np.random.seed(42)

# 假设有10个训练样本的特征值(已归一化)和标签(1为人脸,-1为非人脸)
# 特征值可以理解为某个Haar特征的响应
feature_values = np.array([0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5, 1.7, 1.9])
# 样本0–3为负样本、4–7和9为正样本;样本8(特征值1.7)故意标为-1,模拟一个被标错的噪声样本
labels = np.array([-1, -1, -1, -1, 1, 1, 1, 1, -1, 1])

# 初始样本权重:均匀分布
sample_weights = np.ones(len(labels)) / len(labels)

# 第一轮:寻找最佳阈值(简化版,遍历所有可能的分割点)
best_error = 1.0
best_threshold = 0.0
best_polarity = 1  # 1表示小于阈值为正,-1表示大于阈值为正

for threshold in np.arange(0.0, 2.0, 0.1):
    for polarity in [1, -1]:
        if polarity == 1:
            predictions = np.where(feature_values < threshold, 1, -1)
        else:
            predictions = np.where(feature_values > threshold, 1, -1)
        # 计算加权错误率
        error = np.sum(sample_weights * (predictions != labels))
        if error < best_error:
            best_error = error
            best_threshold = threshold
            best_polarity = polarity

print(f"第一轮最佳弱分类器: 阈值={best_threshold:.1f}, 极性={best_polarity}")
print(f"第一轮加权错误率: {best_error:.3f}")

# 计算该弱分类器的权重 alpha
alpha = 0.5 * np.log((1.0 - best_error) / (best_error + 1e-10))
print(f"该弱分类器的权重 alpha: {alpha:.3f}")

# 更新样本权重(重点:增加错分样本的权重)
predictions = np.where(feature_values < best_threshold, 1, -1) if best_polarity == 1 else np.where(feature_values > best_threshold, 1, -1)
is_correct = (predictions == labels)

# 权重更新公式: new_weight = old_weight * exp(-alpha * label * prediction)
# 对于正确样本,label*prediction=1,权重减小;对于错误样本,label*prediction=-1,权重增大。
sample_weights *= np.exp(-alpha * labels * predictions)
# 归一化权重,使其和为1
sample_weights /= np.sum(sample_weights)

print("\n更新后的样本权重:")
for i, (val, lbl, w) in enumerate(zip(feature_values, labels, sample_weights)):
    status = "正确" if is_correct[i] else "错误"
    print(f"样本{i}: 特征值={val:.1f}, 标签={lbl:2d}, 预测={predictions[i]:2d}, {status}, 新权重={w:.3f}")
第一轮最佳弱分类器: 阈值=0.7, 极性=-1
第一轮加权错误率: 0.100
该弱分类器的权重 alpha: 1.099

更新后的样本权重:
样本0: 特征值=0.1, 标签=-1, 预测=-1, 正确, 新权重=0.056
样本1: 特征值=0.3, 标签=-1, 预测=-1, 正确, 新权重=0.056
样本2: 特征值=0.5, 标签=-1, 预测=-1, 正确, 新权重=0.056
样本3: 特征值=0.7, 标签=-1, 预测=-1, 正确, 新权重=0.056
样本4: 特征值=0.9, 标签= 1, 预测= 1, 正确, 新权重=0.056
样本5: 特征值=1.1, 标签= 1, 预测= 1, 正确, 新权重=0.056
样本6: 特征值=1.3, 标签= 1, 预测= 1, 正确, 新权重=0.056
样本7: 特征值=1.5, 标签= 1, 预测= 1, 正确, 新权重=0.056
样本8: 特征值=1.7, 标签=-1, 预测= 1, 错误, 新权重=0.500
样本9: 特征值=1.9, 标签= 1, 预测= 1, 正确, 新权重=0.056

注:样本8(特征值1.7)被故意标错,因此第一轮的加权错误率为 0.1;更新后,被错分的样本8权重从 0.1 升到 0.5,其余正确样本降到约 0.056。下一轮弱分类器就会被强制去关注这个“难分样本”,这正是 AdaBoost 的核心机制。

通过多轮这样的迭代,AdaBoost将一系列“眼光独特”(每一轮关注不同的难分样本)的弱分类器组合起来,形成了一个具有很强泛化能力的强分类器。

级联分类器:构建高效的检测流水线

一个足够强的分类器虽然准确,但计算所有特征并做出判断依然很慢。Viola和Jones提出的级联分类器结构,是解决实时性问题的关键创新。

级联分类器的核心思想是由粗到精,快速拒绝:

  1. 结构:将多个强分类器(C1, C2, ..., Cn)串联起来。
  2. 机制:每个强分类器都被设计为具有极高的召回率(几乎不放过任何人脸)和一定的判别能力。也就是说,它宁可放过一些非人脸,也尽量不误杀真人脸;反过来,一旦某个窗口被它拒绝,那么它几乎一定不是人脸。
  3. 流程:待检测的图像子窗口需要依次通过所有强分类器。如果在任何一个阶段被拒绝(判定为“非人脸”),则立即终止后续计算。只有通过所有关卡的子窗口,才被最终判定为“人脸”。
输入图像子窗口
      |
      v
   [强分类器 C1]
      | (通过?)
      v
   [强分类器 C2] ---(拒绝)---> [输出:非人脸]
      | (通过?)
      v
      ...
      | (通过?)
      v
   [强分类器 Cn]
      | (通过?)
      v
[输出:人脸]

这种设计的优势在于:

  • 速度极快:图像中大部分区域都是背景(非人脸),它们往往在第一个或前几个强分类器就被快速拒绝,无需进行后续更复杂的计算。只有少数可能包含人脸的区域会走完全程。
  • 资源节约:越靠后的强分类器通常包含更多的弱分类器(特征),计算越复杂。级联结构确保了复杂计算只用在“疑似人脸”的区域上。
  • 易于训练:可以通过调整每个强分类器的阈值,在检测率和误检率之间进行权衡,从而构建出满足不同应用需求的检测器。

正是凭借Haar-like特征、积分图、AdaBoost和级联结构这四大支柱,Viola-Jones人脸检测框架在深度学习时代之前统治了该领域近十年,并被集成到OpenCV等开源库中,成为无数应用的基础。

📝 动手练一练

  1. 概念辨析:人脸检测(Face Detection)与人脸识别(Face Recognition)的核心任务有何不同?级联分类器中的“级联”一词,具体指的是什么样的结构和工作流程?
👉 点击查看参考答案
  • 任务区别:
    • 人脸检测:属于目标检测任务,目标是定位图像中所有人脸的位置(通常用矩形框标出),并判断“这里是否有一张脸”。它不关心这是谁的脸。
    • 人脸识别:属于身份识别任务,目标是在检测到人脸的基础上,辨别这张脸属于哪个特定的个体(如张三或李四)。它核心是“这是谁的脸”。
  • 级联结构: “级联”指的是一种串联的、由粗到精的过滤结构。多个强分类器(C1, C2, ..., Cn)像关卡一样依次排列。待检测的图像区域必须依次通过所有关卡才能被判定为人脸。任何一关将其拒绝(判定为非人脸),检测流程立即终止。这种结构优先使用简单的规则快速排除大量负样本,从而极大提升整体检测速度。
  1. 积分图计算:给定一个4x4的灰度图像I和其对应的积分图II(已计算好)。请写出仅使用II中四个点的值,计算图像I中左上角坐标为(1,1),右下角坐标为(3,3)的矩形区域(即3x3区域)像素和的公式。假设积分图II的索引从0开始,且II(0, y) = II(x, 0) = 0。
👉 点击查看参考答案

设矩形区域左上角为(x1, y1) = (1,1),右下角为(x2, y2) = (3,3)。 根据积分图性质,该矩形区域的像素和S为: S = II(x2+1, y2+1) - II(x1, y2+1) - II(x2+1, y1) + II(x1, y1) 代入坐标: S = II(4, 4) - II(1, 4) - II(4, 1) + II(1, 1) 注意:公式中的坐标是积分图II的坐标,它比原图I在行和列上都多了一维(0行0列)。原图I的像素(i,j)对应积分图II的(i+1, j+1)。

本章小结

本节我们穿越回深度学习时代之前,剖析了经典人脸检测算法Viola-Jones框架的精妙之处。我们从最基本的Haar-like特征出发,理解了其如何通过矩形区域的像素差来捕捉人脸的结构性明暗对比。为了解决海量特征的计算瓶颈,我们引入了积分图这一关键技术,实现了任意矩形区域求和的常数时间复杂度。随后,我们探讨了如何利用AdaBoost集成学习算法,将成千上万个判别力微弱但计算快速的Haar特征(弱分类器),组合成一个判别力强大的强分类器。最后,我们揭示了级联分类器的核心智慧:通过串联多个强分类器构建一个由粗到精的检测流水线,早期快速拒绝大量非人脸区域,从而在保证高检测率的同时实现了惊人的实时性能。

这一整套方法体现了传统计算机视觉中特征工程与模型集成的巅峰设计,其思想至今仍在许多轻量级或对实时性要求极高的场景中发挥着作用。

📋 行动清单

  • 在OpenCV中尝试调用cv2.CascadeClassifier加载预训练的人脸检测器(haarcascade_frontalface_default.xml),并对一张图片进行检测,直观感受其效果和速度。
  • 思考:Haar特征主要捕捉的是图像的什么信息(亮度、纹理、边缘)?为什么它对光照变化有一定鲁棒性,但又可能受强烈侧光影响?
  • 对比思考:级联分类器“快速拒绝”的思想,与深度学习目标检测模型(如单阶段检测器YOLO)中使用的“置信度阈值过滤”和“非极大值抑制”在目的和机制上有何异同?

—— 小象教研组