参考链接:
(1) https://blog.csdn.net/JasonDing1354/article/details/37558287
(2) https://blog.csdn.net/zouxy09/article/details/7922923
(3) https://blog.csdn.net/zouxy09/article/details/7929570

1. 前言

我们要探讨的Haar分类器实际上是Boosting算法的一个应用,Haar分类器用到了Boosting算法中的AdaBoost算法,只是把AdaBoost算法训练出的强分类器进行了级联,并且在底层的特征提取中采用了高效率的矩形特征和积分图方法,这里涉及到的几个名词接下来会具体讨论。
而AdaBoost算法是Freund和Schapire在1995年提出的算法,是对传统Boosting算法的一大提升。Boosting算法的核心思想,是将弱学习方法提升成强学习算法。
Haar分类器算法的要点如下:

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

2. Haar-like特征

最原始的Haar-like特征在2002年的《A general framework for object detection》提出,它定义了四个基本特征结构,如下A,B,C,D所示,可以将它们理解成为一个窗口,这个窗口将在图像中做步长为1的滑动,最终遍历整个图像。

在这里插入图片描述

比较特殊的一点是,当一次遍历结束后,窗口将在宽度或长度上成比例的放大,再重复之前遍历的步骤,直到放大到最后一个比例后结束。

那么可以放大的比例系数如何确定呢?设在宽度上可以放大的最大倍数为K w ,高度上可以放大的最大倍数为K h ,计算公式如下:

其中w I 和h I 是整个图像的宽高,Wwin和h w i n  是Haar窗口的初始宽、高,可以放大的倍数为K w ⋅ K h  。
Haar-like特征提取过程就是利用上面定义的窗口在图像中滑动,滑动到一个位置的时候,将窗口覆盖住的区域中的白色位置对应的像素值的和减去黑色位置对应的像素值的和,得到的一个数值就是Haar特征中一个维度。
其中对于窗口C,黑色区域的像素值加和要乘以2,是为了像素点个数相同而增加的权重。
在基本的四个Haar特征基础上,文章《An extended set of Haar-like features for rapid object detection》对其做了扩展,将原来的4个扩展为14个。这些扩展特征主要增加了旋转性,能够提取到更丰富的边缘信息。
在这里插入图片描述

如何理解特征呢?以人脸识别为例,我们将上面的任意一个矩形放到人脸区域上,然后将白色区域的像素和减去黑色区域的像素和,得到的值我们暂且称之为人脸特征值,如果你把这个矩形放到一个非人脸区域,那么计算出的特征值应该和人脸特征值是不一样的,而且越不一样越好,所以这些方块的目的就是把人脸特征量化,以区分人脸和非人脸。
为了增加区分度,可以对多个矩形特征计算得到一个区分度更大的特征值,那么什么样的矩形特征怎么样的组合到一块可以更好的区分出人脸和非人脸呢,这就是AdaBoost算法要做的事了。

3. AdaBoost算法

AdaBoost算法的老祖宗可以说是机器学习的一个模型,它的名字叫PAC(Probably Approximately Correct)。
PAC模型是计算学习理论中常用的模型,由Leslie Valiant在1984年提出,他认为“学习”是模式明显清晰或模式不存在时仍能获取知识的一种“过程”,并给出了一个从计算角度来获得这种“过程"的方法,这种方法包括:

  • 适当信息收集机制的选择;
  • 学习的协定;
  • 对能在合理步骤内完成学习的概念的分类。

PAC学习模型的实质就是在样本训练的基础上,使算法的输出以概率接近未知的目标概念。PAC学习模型是考虑样本复杂度(指学习器收敛到成功假设时至少所需的训练样本数)和计算复杂度(指学习器收敛到成功假设时所需的计算量)的一个基本框架,成功的学习被定义为形式化的概率理论。简单说来,PAC学习模型不要求你每次都正确,只要能在多项式样本和多项式时间内得到满足需求的正确率,就算是一个成功的学习。
基于PAC学习模型的理论分析,Valiant提出了Boosting算法。Boosting算法涉及到两个重要的概念就是弱学习和强学习,所谓的弱学习,就是指一个学习算法对一组概念的识别率只比随机识别好一点,所谓强学习,就是指一个学习算法对一组概率的识别率很高。Kearns和Valiant提出了弱学习和强学习等价的问题,并证明了只要有足够的数据,弱学习算法就能通过集成的方式生成任意高精度的强学习方法。
针对Boosting的若干缺陷,Freund和Schapire于1996年前后提出了一个实际可用的自适应Boosting算法AdaBoost,AdaBoost目前已发展出了大概四种形式的算法:Discrete AdaBoost、Real AdaBoost、LogitBoost、gentle AdaBoost。

3.1 弱分类器

最初的弱分类器可能只是一个最基本的Haar-like特征,计算输入图像的Haar-like特征值,和最初的弱分类器的特征值比较,以此来判断输入图像是不是人脸,然而这个弱分类器太简陋了,可能并不比随机判断的效果好,对弱分类器的孵化就是训练弱分类器成为最优弱分类器,注意这里的最优不是指强分类器,只是一个误差相对稍低的弱分类器,训练弱分类器实际上是为分类器进行设置的过程。我们首先分别看下弱分类器的数学结构和代码结构。
① 数学结构:

在这里插入图片描述

其中,h j ( x ) 为弱分类器的判断值,值为1则说明图片为人脸,否则非人脸;x 表示输入的图片子窗口,f j ( x ) 为x图像上第j个特征的值;θ j 为分类器阈值;p j 为不等号方向,若分类结果大于阈值,则为−1,否则为+1,以保证不等号<方向不变。
注意:输入x xx为一幅数字图像,特征f j 与弱分类器h j ( x ) 是一一对应的关系;一个弱分类器的训练就是找到最优阈值θj的过程,一轮分类器的训练过程就是找到分类效果最好的弱分类器的过程。
② 代码结构:
/*
    
    * CART classifier
    
*/
    
typedef struct CvCARTHaarClassifier
    
{
    
    CV_INT_HAAR_CLASSIFIER_FIELDS()
    
    int count;
    
    int* compidx;
    
    CvTHaarFeature* feature;
    
    CvFastHaarFeature* fastfeature;
    
    float* threshold;
    
    int* left;
    
    int* right;
    
    float* val;
    
} CvCARTHaarClassifier;

其中,代码结构中的threshold即代表数学结构中的θ阈值。
这个阈值究竟是干什么的?我们先了解下CvCARTHaarClassifier这个结构,注意CART这个词,它是一种二叉决策树,它的提出者Leo Breiman等称其为“分类和回归树(CART)”。
在机器学习中,决策树是一个预测模型,它代表的是对象属性与对象值之间的一种映射关系。树中每个节点表示某个对象,而每个分叉路径则代表的某个可能的属性值,而每个叶结点则对应从根节点到该叶节点所经历的路径所表示的对象的值。决策树仅有单一输出,若欲有复数输出,可以建立独立的决策树以处理不同输出。从数据产生决策树的机器学习技术叫做决策树学习,通俗说就是决策树。
决策树用途很广可以分析因素对事件结果的影响,同时也是很常用的分类方法,我举个最简单的决策树例子,假设我们使用三个Haar-like特征f 1 ,f 2,f 3来判断输入数据是否为人脸,可以建立如下决策树:

在这里插入图片描述

可以看出,在分类的应用中,每个非叶子节点都表示一种判断,每个路径代表一种判断的输出,每个叶子节点代表一种类别,并作为最终判断的结果。
一个弱分类器就是一个基本和上图类似的决策树,最基本的弱分类器只包含一个Haar-like特征,也就是它的决策树只有一层,被称为树桩(stump)。

3.2 弱分类器的训练

最重要的是如何决定每个节点判断的输出,要比较输入图片的特征值和弱分类器中特征,一定需要一个阈值,当输入图片的特征值大于该阈值时才判定其为人脸。训练最优弱分类器的过程实际上就是在寻找合适的分类器阈值,使该分类器对所有样本的判读误差最低。
具体操作过程如下:

对于每个特征f,计算所有训练样本的特征值,并根据特征值大小对特征升序排序;
扫描一遍排好序的特征值,对排好序的表中的每个元素,计算下面四个值:
全部人脸样本的权重的和T1;
全部非人脸样本的权重的和T0;
在此元素之前的人脸样本的权重的和S1;
在此元素之前的非人脸样本的权重的和S0;

通过把这个排序的表从头到尾扫描一遍可以为弱分类器选择使分类误差最小的阈值(即最优阈值),也就是得到一个最佳弱分类器。

3.3 强分类器

AdaBoost训练出来的强分类器一般具有较小的误识率,但检测率并不很高,一般情况下,高检测率会导致高误识率,这是强分类阈值的划分导致的,要提高强分类器的检测率要降低阈值,要降低强分类器的误识率就要提高阈值,这是个矛盾的事情。增加分类器个数可以在提高强分类器检测率的同时降低误识率,所以级联分类器在训练时要考虑如下平衡,一是弱分类器的个数和计算时间的平衡,二是强分类器检测率和误识率之间的平衡。
首先看一下强分类器的代码结构:
/* internal stage classifier */
    
typedef struct CvStageHaarClassifier
    
{
    
    CV_INT_HAAR_CLASSIFIER_FIELDS()
    
    int count;
    
    float threshold;
    
    CvIntHaarClassifier** classifier;
    
}CvStageHaarClassifier;
    
/* internal weak classifier*/
    
typedef struct CvIntHaarClassifier
    
{
    
    CV_INT_HAAR_CLASSIFIER_FIELDS()
    
} CvIntHaarClassifier;

这里要提到的是CvIntHaarClassifier结构:它就相当于一个接口类,当然是用C语言模拟的面向对象思想,利用CV_INT_HAAR_CLASSIFIER_FIELDS()这个宏让弱分类器CvCARTHaarClassifier和强分类器CvStageHaarClassifier继承于CvIntHaarClassifier。
强分类器的诞生需要T轮的迭代,具体操作如下:

给定训练样本集S,共N(N=X+Y)个样本,其中X和Y分别对应于正样本和负样本的数量,T为训练的最大循环次数;
初始化样本权重为1/N,即为训练样本的初始概率分布;
第一次迭代训练N个样本,得到第一个最优弱分类器;
提高上一轮中被误判的样本的权重;
将新的样本和上次区分错误的样本放在一起进行新一轮的训练;
循环执行4-5步骤,T轮后得到T个最优弱分类器;
组合T个最优弱分类器得到强分类器,组合方式如下:
在这里插入图片描述

其中,h t ( x ) 为第t个弱分类器的输出值,α t 为弱分类器的权重,它的数学公式为α t = [ l o g ( ( 1 − e t ) / e t ) ] ,e t为弱分类器对训练样本集的错误率。
下图是强分类器的逻辑结构:
在这里插入图片描述

在OpenCV中,强分类器是由多个弱分类器“并列”构成,即强分类器中的弱分类器是两两相互独立的。在检测目标时,每个弱分类器独立运行并输出值cascadeLeaves[leafOfs - idx],然后把当前强分类器中每一个弱分类器的输出值相加求和。因此强分类器的决策相当于让所有弱分类器投票,再对投票结果按照弱分类器的错误率加权求和,将投票加权求和的结果与平均投票结果比较得出最终的结果。

3.4 强分类器的级联

我们通过AdaBoost算法训练出了强分类器,然而在现实的人脸检测中,只靠一个强分类器还是难以保证检测的正确率,这个时候,需要一个豪华的阵容,来训练出多个强分类器将它们强强联手,最终形成正确率很高的级联分类器这就是我们最终的目标Haar分类器。
在一个级联分类系统中,对于每一个输入图片,对图片进行多区域,多尺度的检测,所谓多区域,是要对图片划分多块,对每个块进行检测,由于训练的时候用的照片一般都是20*20左右的小图片,所以对于大的人脸,还需要进行多尺度的检测,多尺度检测机制一般有两种策略,一种是不改变搜索窗口的大小,而不断缩放图片,这种方法显然需要对每个缩放后的图片进行区域特征值的运算,效率不高。而另一种方法,是不断初始化搜索窗口size为训练时的图片大小,不断扩大搜索窗口,进行搜索,解决了第一种方法的弱势。在区域放大的过程中会出现同一个人脸被多次检测,这需要进行区域的合并,这里不作探讨。
无论哪一种搜索方法,都会为输入图片输出大量的子窗口图像,这些子窗口图像经过筛选式级联分类器会不断地被每一个节点筛选,抛弃或通过。
另外前面的强分类器相对简单,其包含的弱分类器也相对较少,后面的强分类器逐级复杂,只有通过前面的强分类检测后的图片才能送入后面的强分类器检测,比较靠前的几级分类器可以过滤掉大部分的不合格图片,只有通过了所有强分类器检测的图片区域才是有效的人脸区域。
在这里插入图片描述

4. 积分图

在上面Haar-like特征这节说到,将模板的特征值定义为白色矩形像素和减去黑色矩形像素和。而积分图就是只遍历一次图像就可以求出图像中所有区域像素和的快速算法,大大的提高了图像特征值计算的效率。
积分图的构造方式是位置( i , j )处的值i i ( i , j )是原图像( i , j ) 左上角方向所有像素的和:
在这里插入图片描述

积分图构建算法:

用s ( i , j ) 表示行方向的累加和,初始化s ( i , − 1 ) = 0 ;

用i i ( i , j )表示一个积分图像,初始化i i ( − 1 , i ) = 0;
逐行扫描图像,递归计算每个像素f ( i , j )行方向的累加和s ( i , j ) 和积分图像i i ( i , j )的值;
在这里插入图片描述

扫描图像一遍,当到达图像右下角像素时,积分图像i ii就构造好了。
在这里插入图片描述

上面这幅图中有四个区域A,B,C,D。我们将左上角的点记为0,区域A的所有点像素值的和记为sum_A,点0与点1之间的积分图记为i n t e g r a l 0 , 1  ,那么根据积分图的定义:
在这里插入图片描述

在这里插入图片描述

区域D的像素值和s u m D 就应该为i n t e g r a l 1 , 4 ,但是注意,积分图中是没有从点1到点4的概念的,它所有的起点都应该是点0,所以:

在这里插入图片描述

转化一下就是

在这里插入图片描述

上面的内容就是积分图,比如说我们要求s u m D,并不需要从点1到点4做行列的遍历,因为这个遍历过程的时间复杂度是O ( m n )的。我们只需要先存在下来从0到点1,2,3,4的积分图,然后做一个简单的加减法就好了,这个时间复杂度仅仅为O ( 1 )。当然了,存储的过程是消耗空间复杂度的,这是很典型的空间换时间的套路。这就是上面提到的积分图加速计算的过程。