本文最后更新于:2020年7月6日 凌晨

3.1 算法由来

  Haar-like特征最早是由Papageorgiou等应用于人脸表示,2001年,Viola和Jones两位大牛发表了今典的《Rapid Object Detection using a Boosted Cascade of Simple Features》和《Robust Real-Time Face Detection》,在AdaBoost算法基础上,使用Haar-like小波特征和积分图方法进行人脸监测,并对AdaBoost训练出的强分类器进行级联。
  这两个大咖不是最早提出使用小波特征的,单他们设计了针对人脸监测更有效的特征,可以说是人脸检测史上里程碑式的一笔了,当时这个算法被称为Viola-Jones检测器。又过了一段时间,Rainer Liehart和Jochen Maydt两位大咖把这个检测器进行扩展,最终形成了OpenCV现在的Haar分类器。

  AdaBoost是Freund和Schapire在1995年提出的算法,是对传统Boosting算法的一大提升。Boosting算法的核心思想,是将弱学习方法提升成强学习算法,也就是“三个臭皮匠顶一个诸葛亮”。

Haar分类器 = Haar-like特征 + 积分图方法 + AdaBoost + 级联

Haar分类器算法的要点如下:

  1. 使用Haar-like特征特征做监测
  2. 使用积分图(Integral Image)对Haar-like特征求值进行加速。
  3. 使用AdaBoost算法训练区分人脸和非人脸的强分类器。
  4. 使用筛选式级联把强分类器级联到一起,提高准确率。

3.2 算法理解

一、Haar-like特征

viola牛们提出的Haar-like特征:

img

Lienhart等牛们提出的Haar-like特征:

img

  Haar特征分为四类:边缘特征、线性特征、中心特征和对角线特征,组合成特征模板。特征模板内有白色和黑色两种矩形。并定义该模板的特征值为白色矩形像素和减去黑色矩形像素和。Haar特征值反映了图形的灰度变化情况。例如:脸部的一些特征能由矩形特征简单的描述,单矩形特征只对一些简单的图形结构,如边缘、线段较敏感,所以只能描述特定走向(水平、垂直、对角)的结构。

img

  图中的A, B和D这类特征,特征数值计算公式为:v=Σ白-Σ黑。图C中,计算公式:v=Σ白-2*Σ黑。将黑色区域像素和乘以2,是为了使两种矩形区域中像素数目一致。我们希望当把矩形放到人脸区域计算出来的特征值和放到非人脸区域计算出来的特征值差别越大越好,这样就可以用来区分人脸和非人脸。
  OpenCV(2.4.11版本)所使用的共计14种Haar特征,包括5种Basic特征、3种Core特征和6种Title(即$45^\circ$旋转)特征。mode为BASIC,使用前五个。mode为CORE,使用前8个。mode为ALL,使用全部14种。

img

  通过改变特征模板的大小和位置,可在图像子窗口中穷举出大量的特征。上图的特征模板称为“特征原型”;特征原型在图像子窗口中扩展(平移伸缩)得到的特征称为“矩形特征”;矩形特征的值称为“特征值”。

img

  上图中两个矩形特征,表示出人脸的某些特征。比如中间一幅表示眼睛区域的颜色比脸颊区域的颜色深,右边一幅表示鼻梁两侧比鼻梁的颜色要深。同样,其他目标,如眼睛等,也可以用一些矩形特征来表示。使用矩形特征比单纯地使用像素点具有很大的优越性,并且速度更快。
  矩形特征可位于图像任意位置,大小也可以任意改变,所以矩形特征值是矩形模版类别、矩形位置和矩形大小这三个因素的函数。故类别、大小和位置的变化,使得很小的检测窗口含有非常多的矩形特征,如:在24*24像素大小的检测窗口内矩形特征数量可以达到16万个。这样就有两个问题需要解决了:

(1)如何快速计算那么多的特征?—-积分图大显神通;
(2)哪些矩形特征才是对分类器分类最有效的?—-如通过AdaBoost算法来训练。

二、Haar-like特征计算-积分图

  积分图是只遍历一次图像就可求出图像中所有区域像素和的快速算法,大大提高了图像特征值计算的效率。
  积分图的主要思想是将图像从起点开始到各个点形成的矩形区域像素之和作为一个数组的元素提前保存在数组中。当要计算某个区域的像素和时,直接索引数组的元素进行线性计算即可,从而加快了计算速度。
  积分图是一种能够描述全局信息的举证表示方法。积分图的构造方式是位置位置(𝑖,𝑗)处的值𝑖𝑖(𝑖,𝑗)是原图像(𝑖,𝑗)左上角方向所有像素𝑓(𝑘,𝑙)的和:

积分图构建算法:
1、用𝑠(𝑖,𝑗)表示行方向的累加和,初始化𝑠(𝑖,−1)=0;
2、使用𝑖𝑖(𝑖,𝑗)表示一个积分图像,初始化𝑖𝑖(−1,𝑖)=0;
3、逐行扫描图像,递归计算每个像素(𝑖,𝑗)行方向的累加和𝑠(𝑖,𝑗)和积分图像𝑖𝑖(𝑖,𝑗)的值:

4、扫描图像一遍,当到达图像右下角像素时,积分图像𝑖𝑖就构建好了。
积分图构造好之后,图像中任何矩阵区域像素累加和都可以通过简单运算得到如图所示:

img

设D的四个顶点分别为$\alpha,\beta,\gamma,\delta$,则D的像素和可以表示为:

Haar-like特征值是两个矩阵像素和的差,同样可以在常数时间内完成。

三、计算Haar特征值

  由二已知,一个区域的像素值的和,可以由该区域的端点的积分图计算。由前面特征模板的特征值的定义可推出,矩形特征的特征值由特征端点的积分图计算得到。以A矩形特征为例,如下图,使用积分图计算该特征值:

img

区域A的像素值:$𝑖𝑖(5)+𝑖𝑖(1)−𝑖𝑖(2)−𝑖𝑖(4)$

区域B的像素值:$𝑖𝑖(6)+𝑖𝑖(2)−𝑖𝑖(5)−𝑖𝑖(3)$

所以:该矩形特征的特征值为:

  所以,矩形特征的特征值,只与特征矩形的端点的积分图有关。利用矩形特征的端点的积分图,再进行简单的加减运算,即可得到特征值,如此一来,特征的计算速度大大提高,也提高了目标的检测速度。
了解特征值计算后,接下来展示不同特征值的含义。

  下图展示了20X20子窗口里面全部78,460个矩形特征对在全部2,706个人脸样本和4,381个非人脸样本的特征值平均值的分布图。由分布看出,特征的绝大部分的特征值平均值都是分布在0前后的范围内。
  出乎意料的是,人脸样本与非人脸样本的分布曲线差别并不大,不过注意到特征值大于或小于某个值后,分布曲线出现了一致性的差别。说明了绝大部分特征对于识别人脸和非人脸的能力是很微小的,但是存在一些特征及相应的阈值,可以有效第区别人脸样本和非人脸样本。image-20200704225306093

  为了更好地说明部分特征对于人脸和非人脸的区分作用,从78,460个矩形特征中随机抽取了两个特征A和B,这两个特征遍历2,706 个人脸样本和4,381 个非人脸样本,计算了每张图像对应的特征值,最后将特征值进行了从小到大的排序,并按照这个新的顺序表绘制了分布图如下所示:

  可以看出,矩形特征A在人脸样本和非人脸样本中的特征值分布相似,所以区分人脸和非人脸的能力很差。下面看矩形特征B在人脸样本和非人脸样本中特征值的分布:

  可以看出,矩形特征B的特征值分布,尤其是0点的位置,在人脸样本和非人脸样本中差别比较大,所以可以更好地实现对人脸分类。
  特征 A 和特征 B 的表现大相径庭。
  特征 A 对人脸和非人脸样本的特征值为0的点几乎处于相同位置(46.5%,51.5%),且都在所有特征的中间范围 (-5%)。这说明矩形特征 A对于人脸和非人脸几乎没有分辨能力。
  特征 B 对非人脸样本的分布,符合我们的预想,特征值为 0的点处于所有特征的中间范围(59.4%),这说明特征B也“ 看不到” 非人脸的特点。但是对于人脸样本,特征 B 表现了很一致的倾向性,93.4%的特征在 0 点的一侧,与非人脸样本的相差 34%。这说明特征 B 能够相当可靠地分辨人脸和非人脸。
  上述的分析说明,确实存在优势的矩形特征,能够在一定的置信范围内区分人脸和非人脸。由于是使用统计的方法计算人脸图像和非人脸图像的差别,因此最后得到的区分阈值,是在某个概率范围内准确地进行区分。
上述总结如下:
(1)在检测窗口通过平移+缩放可以产生一系列Haar特征,这些特征由于位置和大小不同,分类效果也不同;
(2)通过计算Haar特征的特征值,可以有将图像矩阵映射为1维特征值,有效实现了降维。

Haar特征值归一化(也可以采取标准归一化)

  从上图中可发现,仅仅在正样本组中12*8大小的Haar特征计算出的特征值变化范围就达到了-2000~+6000,跨度非常大。这种跨度大的特征不利于量化评定特征值,这时候归一化就排上用场了。对特征值进行归一化,压缩特征值范围。
  假设当前检测窗口中的图像像素为$i(x,y)$,当前检测窗口为$w\times h$大小(例如上图中20x20大小)。
OpenCV采用如下方式归一化:
1、计算检测窗口中图像的灰度值和灰度值平方和:(这里的$i^2$怎么用积分图表示??)

2、计算平均值:

3、计算归一化因子:

4、归一化特征值:

之后使用归一化的特征值𝑛𝑜𝑟𝑚𝑉𝑎𝑙𝑢𝑒与阈值对比。

四、Adaboost级联分类器

OpenCV中的Adaboost级联分类器的结构,弱分类器->强分类器->级联分类器;

1.级联分类器

Adaboost级联分类器:
  级联分类模型是树状结构,每一层—强分类器是树状结构,强分类器中的每一个弱分类器也是树状结构。如下图所示,每一个stage都代表一级强分类器:
img
  当检测窗口通过所有的强分类器时被认为是正样本。每一个强分类器对负样本的判别准确率高,任一级强分类器检测为负样本,就不再继续调用下面的强分类器,直接丢弃,减少了很多的检测时间。
  一副图像中待检测的区域中很多是负样本,级联分类器在初期就会抛弃很多负样本的复杂检测,所以级联分类器的速度是非常快的,只有正样本才会送到下一个强分类器再次进行检验,这样就保证了最后输出的正样本的伪正(false positive)的可能性非常低。

2.级联分类器的训练

1)首先需要训练出m个弱分类器,然后把m个弱分类器按照一定的组合策略,得到一个强分类器,如下图所示:
2)重复1步骤n次得到n个强分类器,按照级联的方式组合起来,得到最终的Haar分类器。
img

  一个弱分类器是和上图类似的决策树,最基本的弱分类器只包含一个Haar-like特征,即只包含一层决策树(树桩stump)。
  以20*20图像为例,78,460个特征,若直接利用AdaBoost训练,78,460个弱分类器组合成强分类器,工作量是极其巨大的。所以必须要有个筛选过程。

首先筛选出T个优秀的最优弱分类器(特征值),然后把T个最优弱分类器传给AdaBoost进行训练。
训练最优弱分类器
  人脸样本2000张,非人脸样本4000张,这些样本都经过了归一化,大小都是20X20的图像。那么,对于78,460中的任一特征$f_i$,我们计算该特征在这2000人脸样本,4000非人脸样本上的值,得到6000个特征值。将这些特征值排序,然后选取一个最佳的特征值。在该特征值下,对于特征$f_i$而言,样本的加权错误率最低。
  在确定了训练子窗口(20x20的图像)的矩形特征数量(78,460)和特征值后,需要对每一个特征$f$,训练一个弱分类器$h(x,f,\rho,\theta)$:

  其中$𝑥$代表一个检测子窗口,$𝑓$为矩形特征,$\theta$为阈值,$\rho$指示不等号的方向。对每个特征$𝑓$,训练一个弱分类器$ℎ(𝑥,𝑓,\rho,\theta)$,就是确定$𝑓$的最优阈值,使得这个弱分类器对所有的训练样本分类误差最小。
这里的最优不是指强分类器,只是一个误差相对较低的弱分类器。
弱分类器训练的具体步骤:
1、对于每个特征$f$,计算所有训练样本的特征值,并将其排序:
2、扫描一遍排好序的特征值,对排好序的表中的每个元素,计算下面四个值:
计算全部正例的权重和$𝑇^+$;
计算全部负例的权重和$T^-$;
计算该元素前之前的正例的权重和$𝑆^+$;
计算该元素前之前的负例的权重和$𝑆^−$;

这里写图片描述

3、选取当前元素的特征值$F_{k,j}$和它前面的一个特征值$F_{k,j-1}$之间的数作为阈值,所得到的弱分类器就在当前元素处把样本分开 —— 也就是说这个阈值对应的弱分类器将当前元素前的所有元素分为非人脸(或人脸),而把当前元素后(含)的所有元素分为人脸(或非人脸)。该阈值的分类误差为:

公式说明:前一个分类误差计算前提为阈值前为非人脸,后一个分类误差计算前提为阈值前为人脸。
  把排序表从头到尾扫描一遍就可以为弱分类器选择使分类误差最小的阈值(最优阈值),即选取了一个最优弱分类器。在本例中,一共有78,460个特征,因此会得到78,460个最优分类器。
结合三,可得出以下分析:
  阈值$\theta$的含义就清晰可见了。方向指示符$p$ 用以改变不等号的方向。一个弱学习器(一个特征)的要求仅仅是:它能够以稍低于50%的错误率来区分人脸和非人脸图像,因此上面提到只能在某个概率范围内准确地进行区分就已经完全足够。按照这个要求,可以把所有错误率低于50% 的矩形特征都找到(适当地选择阈值,对于固定的训练集,几乎所有的矩形特征都可以满足上述要求)。每轮训练,将选取当轮中的最佳弱分类器(在算法中,迭代$T$次即是选择$T$个最佳弱分类器),最后将每轮得到的最佳弱分类器按照一定方法提升(Boosting)为强分类器。
AdaBoost算法的目的是从训练数据中学习一系列弱分类器,然后把这些弱分类器组合成一个强分类器。
AdaBoost强分类器的训练步骤:参考AdaBoost人脸监测(3)

这里的公式还没有仔细厘清,先无脑抄了,整理好了再把这里删掉
给定训练数据集$T=\left(x_{i}, y_{i}\right), i=1,2,3, \ldots N$,共$N$个样本,其中样本$x \in X$,实例空间$x \in R^n$,$y_i \in Y=\{-1, +1\}$。$T$为迭代次数。
步骤1:初始化训练数据的权值分布。每个训练数据初始都被赋予相同的权值:$\frac{1}{N}$

步骤2:进行T轮迭代,用$t=1,2,3, \ldots T$表示迭代的次数。
①使用具有权值分布$D_m$的训练数据集测试,得到基本分类器(选取分类误差最低的阈值设计基本分类器):$h_m(x):X \rightarrow \{−1, +1 \}$
②计算$h_m(x)$在训练数据集上的分类误差率:

由上述式子可知,$h_m(x)$在训练数据集上的误差$e_m$就是被$h_m(x)$误分类样本的权值之和。

②对每个(种)特征$f_j$,训练一个弱分类器$h_j$(如上),每个分类器只使用一种Haar特征进行训练。分类误差为:

③从②确定的弱分类器中,找出一个具有最小分类误差的弱分类器$h_t$;
④更新每个样本对应的权重:

这里,如果样本$x_i$被正确分类,则$e_i=0$,否则$e_i=1$,而

4、最终形成的强分类器组成为:

其中:

  在使用Adaboost算法训练分类器之前,需要准备好正、负样本,根据样本特点选择和构造特征集。
  由算法的训练过程可知,当弱分类器对样本分类正确,样本的权重会减小;而分类错误时,样本的权重会增加。这样,后面的分类器会加强对错分样本的训练。最后,组合所有的弱分类器形成强分类器,通过比较这些弱分类器投票的加权和与平均投票结果来检测图像。
  OpenCV中,强分类器是由多个弱分类器“并列”构成,即强分类器中的弱分类器是两两相互独立的。在检测目标时,每个弱分类器独立运行并输出结果,然后把当前强分类器中每一个弱分类器的输出值相加,与本级强分类器的阈值相比,当且仅当结果大于阈值,认为当前检测窗口通过了该级强分类器。

3.级联分类器的检测

  整合好了级联分类器,接下来就是高光时刻:检测!
  检测是以显示中的一副图片作为输入,然后对图片进行多区域、多尺度的检测。

  • 多区域检测是指遍历整张图片。
  • 多尺度检测有两种策略
    1)搜索窗口的大小不变,不断缩放图片,在这种情况下需要对每个缩放后的图片进行区域特征值的运算,效率较低。
    2)不断扩大搜索窗口,对图片进行特征值运算,效率较高。因为1)需要计算大小不同的图片的积分图,而2)只需对原图进行一次积分图即可。不同的搜索窗口的特征值计算在线性时间内即可完成。

  放大+平移获得的子特征有多少个呢?Rainer Lienhart在其论文中做出了解释,假设检测窗口大小为$WH$,矩形特征大小为$wh$,X和Y表示矩形特征在水平和垂直方向上能放大的最大比例系数:

img

  如图5,在检测窗口window中,一般矩形特征的数量为:$XY(W+1-w\frac{X+1}{2})( H+1-h\frac{Y+1}{2})$。对于之前x3特征在24*24大小的检测窗口中(W=H=24,w=3,h=1,X=8,Y=24),一共能产生27600个子特征。具体参考Haar特征与积分图
  无论哪种搜索方法,都会向级联分类器输入大量的子窗口图像,子窗口图像在筛选式级联分类器中依次被每一个节点筛选,丢弃或通过。最后利用并查集合并检测结果窗口。重叠窗口个数大于阈值时,选取其中之一作为检测结果,其余丢弃。

4.总结

总结Haar分类器的五大训练步骤:

  1. 准备人脸、非人脸样本集
  2. 计算特征值和积分图
  3. 筛选出T个优秀的最优弱分类器(特征值)
  4. 把T个最优弱分类器传给AdaBoost进行训练,得到一个强分类器
  5. 重复3. 4.,将得到的n个强分类器级联成一个Adaboost级联分类器。

以20*20窗口为例,有78,460个特征,筛选出T个优秀的最优弱分类器,然后把T个最优弱分类器传给AdaBoost进行训练得到一个强分类器,最后将n个强分类器级联成一个Adaboost级联分类器。

五、分类器的检测

OpenCV自带了训练器和检测器。如果想从头训练一个分类器检测汽车、飞机等其他物体,可以使用OpenCV构建。细节参考这里:Cascade Classifier Training
OpenCV自带的检测器在OpenCV库文件\haarcascades文件\xx.xml文件中。在我的电脑里,具体路径为C:\Users\susu\AppData\Roaming\Python\Python37\site-packages\cv2\data\中。这个文件夹下包含了检测人脸、眼睛、鼻子和嘴等部位的检测器。均需要正面、直立的人体图像。
xml文件包含了检测器的相关信息。具体可以参考haar+adaboost代码讲解(OpenCV)

3.3 人脸检测(OpenCV简单实现)

一、静态图片中的人脸检测

import cv2
# 加载图像
img = cv2.imread('image2.jpg', 1)
# 创建人脸和人眼的级联分类器,加载.xml分类器文件,即Haar特征的分类器(上一篇使用LBP特征的分类器)
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades+'haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades+'haarcascade_eye.xml')
# 进行人脸检测,scaleFactor表示图像的压缩率,minNeighbors表示每个人脸矩形的邻近数目
faces = face_cascade.detectMultiScale(img, scaleFactor=1.3, minNeighbors=5)
for (x, y, w, h) in faces:
    # 使用矩形框出人脸
    img = cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)
    face_area = img[y:y+h, x:x+w]
    # 在人脸上检测人眼
    eyes = eye_cascade.detectMultiScale(face_area)
    # 使用矩形框出人眼
    for (ex, ey, ew, eh) in eyes:
        cv2.rectangle(face_area, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 1)
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite('output.jpg', img)

二、实时人脸监测

# 调用电脑摄像头进行实时人脸+眼睛识别
import cv2

face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades+'haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades+'haarcascade_eye.xml')
smile_cascade = cv2.CascadeClassifier(cv2.data.haarcascades+'haarcascade_smile.xml')
# 调用摄像头
cap = cv2.VideoCapture(0)
cv2.namedWindow('Dynamic')
while(True):
    # 获取摄像头拍摄到的画面
    ret, frame = cap.read()
    # ret为True表明图片读取成功。
    faces = face_cascade.detectMultiScale(frame, 1.3, 2)
    img = frame
    for (x, y, w, h) in faces:
        # 画出人脸框,蓝色,画笔宽度
        img = cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)
        # 框选出人脸区域,在人脸区域而不是全图中进行人眼检测,节省计算资源
        face_area = img[y:y+h, x:x+w]
        eyes = eye_cascade.detectMultiScale(face_area, 1.3, 10)
        # 画出人眼框,绿色,画笔宽度为1
        for (ex, ey, ew, eh) in eyes:
            eye_area = cv2.rectangle(face_area, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 1)
        smile = smile_cascade.detectMultiScale(face_area, scaleFactor=1.16, minNeighbors=65, minSize=(25, 25), flags=cv2.CASCADE_SCALE_IMAGE)
        for (ex, ey, ew, eh) in smile:
            cv2.rectangle(face_area, (ex, ey, ex+ew, ey+eh), (0, 0, 255), 1)
            cv2.putText(img, 'Smile', (x, y-7), 3, 1.2, (0, 0, 255), 2, cv2.LINE_AA)
    cv2.imshow('frame2', img)
    # 按下q键退出
    if cv2.waitKey(5) & 0xFF == ord('q'):
        break
cap.release()
cv2.destroyAllWindows()

简单分析上面的代码:
  创建一个级联分类器对象,加载xml检测器,进行人脸监测。

face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades+'haarcascade_frontalface_default.xml')

  将人脸图像放入监测。不同于LBP检测器,这里彩色的图像也是可以的。

detectMultiScale(image[,scaleFactor[,minNeighbors[,flags,[minSize,[maxSize]]]]])

1)image:待检测的输入图像
2)scaleFactor:每一个图像的尺度参数。默认值为1.1。scaleFactor参数控制两个不同大小窗口扫描的间距。参数过大,可能会错过正确的人脸区域。
3)minNeighbors:每一个级联矩形应该保留的邻近个数,默认为3.minNeighbors控制与检测率。默认值为3,表明至少有3次重叠监测,认为人脸确实存在。
4)minSize:目标最小尺寸
5)maxSize:目标最大尺寸

这次想写得太多了,啥都想加,加到最后好累。。。sad,不知道应该挑几个重点还是都来。awsl

参考资料

人脸监测之Haar分类器
Haar特征与积分图(推荐)
AdaBoost人脸检测介绍(3)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

LSTM_Elmo 上一篇
pandas下综合练习 下一篇