【opencv】传统目标检测:Haar检测器实现人脸检测
阅读原文时间:2023年08月16日阅读:1

传统目标分类器主要包括Viola Jones Detector、HOG Detector、DPM Detector,本文主要介绍VJ检测器,在VJ检测器基础上发展出了Haar检测器,Haar检测器也是更为全面、使用更为广泛的检测器。

Viola Jones Detector是作为人脸检测器被Viola和Jones提出来的,后来Rainer Lienhart和Jochen Maydt将这个检测器进行了扩展,opencv中的haar检测器就是以此为基础的。既然提到了人脸检测,那就简单了解一下相关内容吧。
目前人脸检测的方法主要有两大类:基于知识和基于统计。

  • 基于知识的方法:主要利用先验知识将人脸看作器官特征的组合,根据眼睛、眉毛、嘴巴、鼻子等器官的特征以及相互之间的几何位置关系来检测人脸。主要包括模板匹配、人脸特征、形状与边缘、纹理特性、颜色特征等方法。
  • 基于统计的方法:将人脸看作一个整体的模式——二维像素矩阵,从统计的观点通过大量人脸图像样本构造人脸模式空间,根据相似度量来判断人脸是否存在。主要包括主成分分析与特征脸、神经网络方法、支持向量机、隐马尔可夫模型、Adaboost算法等。

VJ检测器就是基于统计的方法设计的,内部包含了Adaboost算法。由于Haar检测器更为完善、应用范围广,因此本文的VJ检测器特指利用Haar分类器实现的检测算法。
Haar检测器是利用Haar特征、积分图、AdaBoost算法、级联分类器来实现的,实现过程可总结为:

  • 使用Haar特征做检测;
  • 使用积分图对Haar特征求值进行加速;
  • 使用AdaBoost算法训练区分人脸和非人脸的强分类器;
  • 使用筛选式级联把强分类器级联到一起,提高准确率和速度。

Haar是一种特征描述,随着时代的进步Haar特征也从Haar Basic简单特征扩展到了Haar-Like以及到现在的Haar Extended比较复杂的特征。在opencv中共分为了三类BASIC(边缘特征、线性特征、对角特征)、CORE(中心特征)、Titled(旋转45°),如下图所示,它们共同组成了特征模板。
特征模板内有白色和黑色两种矩形,并定义该模板的特征值为白色矩形像素和减去黑色矩形像素和。Haar特征值反映了图像的灰度变化情况。如:眼睛要比脸颊颜色要深,鼻梁两侧比鼻梁颜色要深,嘴巴比周围颜色要深等。

上图的特征模板称为“特征原型”;特征原型在图像子窗口中扩展(平移、伸缩)得到的特征称为“矩形特征”;矩形特征的值称为“特征值”。从下图可以看到,矩形特征可用于表示人脸的某些特征,如中间一幅表示眼睛区域的颜色比脸颊区域的颜色深,右边一幅表示鼻梁两侧比鼻梁的颜色要深。

由于特征原型可以平移伸缩,因此矩形特征值和特征模板、矩形位置和矩形大小这三个因素有关。故类别、大小、位置的变化使得很小的检测窗口含有非常多的矩形特征,在24*24像素大小的检测窗口内矩形特征值就达16万之多。为提高计算速度引入了积分图,为了选取高效率分类器引入了AdaBoost级联分类器。

2.1. 积分图

积分图就是只遍历一次图像就可以求出图像中所有区域像素和的快速算法,大大的提高了图像特征值计算的效率。积分图主要的思想是图像某点积分图的值,是某点与起点作为对角点所形成的矩形区域像素之和,它作为一个数组的元素保存在内存中,当要计算某个区域的像素和时可以直接索引数组的元素进行计算,不用重新计算这个区域的像素和,从而加快了计算(这有个相应的称呼,叫做动态规划算法)。积分图能够在多种尺度下,使用相同的时间(常数时间)来计算不同的特征,因此大大提高了检测速度。
积分图是一种能够描述全局信息的矩阵表示方法,其构造方式是位置(i, j)处的值ii(i, j)是原图(i, j)左上角方向所有像素灰度值f(k, l)的和,即

积分图的构建步骤:

  1. 用s(i, j)表示行方向的累加和,初始化s(i,−1)=0;
  2. 使用ii(i, j)表示一个积分图像,初始化ii(−1, i)=0;
  3. 逐行扫描图像,递归计算每个像素(i, j)行方向的累加和s(i, j)和积分图像ii(i, j)的值:

s(i,j)=s(i,j−1)+f(i,j)
ii(i,j)=ii(i−1,j)+s(i,j)

  1. 扫描图像一遍,当到达图像右下角像素时,图像积分图ii就构建好了。

2.2. 计算Haar特征值

上面已经对积分图做了介绍,那么让我们利用积分图计算下面的边缘矩形特征:

区域A的像素值之和为:Sum(A) = ii(5)+ii(1)−ii(2)−ii(4)
区域B的像素值之和为:Sum(B) = ii(6)+ii(2)−ii(5)−ii(3)
该矩形特征值为:Sum(A)-Sum(B) = ii(5)+ii(1)−ii(2)−ii(4)−[ii(6)+ii(2)−ii(5)−ii(3)]
积分图的出现就使Haar特征值的相关变量由三个 (特征类型、大小、位置) 变成了显性的积分图的值,而且只需要遍历一次图像,因此特征值的计算速度得到了极大的提升。

2.3. Haar特征值的含义

不同的矩阵特征有不同的意义,在人脸检测中下图左侧特征可对应人眼区域,右边则无意义。

我们选取MIT人脸库中2706个大小为20*20的人脸正样本图像计算上图所示的Haar特征,可以绘制如下的函数图。可以看出,图中2个不同Haar特征在同一组样本中具有不同的特征值分布,左边特征计算出的特征值基本都大于0(对样本的区分度大),而右边特征的特征值基本均匀分布于0两侧(对样本的区分度小)。所以,正是由于样本中Haar特征值分布不均匀,导致了不同Haar特征分类效果不同。显而易见,对正负样本区分度越大的特征分类效果越好,即红色曲线对应图中的的左边Haar特征分类效果好于右边Haar特征。

那么看到这里,应该理解了下面2个问题:

  • 在检测窗口通过平移+缩放可以产生一系列Haar特征,这些特征由于位置和大小不同,分类效果也不同;
  • 通过计算Haar特征的特征值,可以有将图像矩阵映射为1维特征值,有效实现了降维。

2.4. Haar特征值归一化

从上图我们可以发现,仅仅一个12*8大小的Haar特征计算出的特征值变化范围从-2000~+6000,跨度非常大。这种跨度大的特性不利于量化评定特征值,所以需要进行“归一化”,压缩特征值范围。假设当前检测窗口中的图像像素值函数为i(x,y),当前检测窗口为w∗h大小,OpenCV采用如下方式“归一化”:

  1. 计算检测窗口中图像的灰度值和灰度值平方和:

sum=∑i(x,y)、sqsum=∑i2(x,y)

  1. 计算平均值:

mean=sum/w∗h、sqmean=sqsum/w∗h

  1. 计算归一化因子:

varNormFactor = √sqmean−mean2

  1. 归一化特征值:

normValue = featureValue/varNormFactor
之后使用归一化的特征值normValue与(归一化的)阈值对比。

3.1. AdaBoost算法概述

AdaBoost算法是Boosting系列算法中最有名的算法之一,Boosting是集成学习中个体学习器之间存在强依赖关系的代表。AdaBoost算法既可以用作分类,也可以用作回归。
Boosting算法是将“弱学习算法“提升为“强学习算法”的过程,主要思想是“三个臭皮匠顶个诸葛亮”。因此,AdaBoost算法也具有这种特性。
AdaBoost算法的主要优点:

  • AdaBoost作为分类器时,分类精度很高。
  • 在AdaBoost的框架下,可以使用个各种回归分类模型来构建弱学习器,非常灵活。
  • 作为简单的二元分类器时,构造简单,结果可理解。
  • 不容易发生过拟合。

AdaBoost算法的主要缺点:

  • 对异常样本敏感,异常样本在迭代中可能会获得较高的权重,影响最终的强学习器的预测准确性。

3.2. 级联(Cascade)分类器


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

3.3. AdaBoost级联分类器


级联分类器是如何训练的呢?首先需要训练出每一个弱分类器,然后把每个弱分类器按照一定的组合策略(并联组合),得到一个强分类器,我们训练出多个强分类器,然后按照级联(串联组合)的方式把它们组合在一块,就会得到我们最终想要的Haar分类器。
一个弱分类器就是一个基本和上图类似的决策树,最基本的弱分类器只包含一个Haar特征,也就是它的决策树只有一层,被称为树桩(stump)。以20*20图像为例,78 460个特征,如果直接利用AdaBoost训练,那么工作量是极其极其巨大的。所以必须有个筛选的过程,筛选出T个优秀的特征值(即最优弱分类器),然后把这个T个最优弱分类器传给AdaBoost进行训练称为多个强分类器。
那么,如何训练弱分类器呢?
最初的弱分类器可能只是一个最基本的Haar特征,然而这个弱分类器太简陋了,可能并不比随机判断的效果好。那么就需要训练弱分类器成为最优弱分类器,注意这里的最优不是指强分类器,只是一个误差相对稍低的弱分类器,训练弱分类器实际上是为分类器进行设置的过程。至于如何设置分类器,设置什么,我们首先分别看下弱分类器的数学结构。

其中f为特征,θ为阈值,p指示不等号的方向,x表示一个检测子窗口。对每个特征f,训练一个弱分类器 f(x)=h(x, f, p, θ),就是确定特征f的最优阈值,使得这个弱分类器对所有的训练样本分类误差最小。
弱分类器的训练过程大致分为以下几步 :

  1. 对每个特征f计算所有训练样本的特征值;
  2. 将特征值排序;
  3. 对排好序的每个元素计算,1)该特征f全部正例的权重和T+;2)该特征f全部负例的权重和T-;3)该元素前正例的权重和S+;4)该元素前负例的权重和S-;
  4. 选取当前元素的特征值F(kj) 和它前面的一个特征值Fkj−1之间的数作为阈值,所得到的弱分类器就在当前元素处把样本分开 —— 也就是说这个阈值对应的弱分类器将当前元素前的所有元素分为人脸(或非人脸),而把当前元素后(含)的所有元素分为非人脸(或人脸)。该阈值的分类误差为:

e=min(S++(T−−S−), S−+(T+−S+))
于是,通过把这个排序表从头到尾扫描一遍就可以为弱分类器选择使分类误差最小的阈值(最优阈值),也就是选取了一个最佳弱分类器。

最优弱分类器有了,强分类器呢?
实际应用中,对分类产生较大影响的往往只有少数的特征,比如传统的主成分分析方法(PCA)就是用来寻找这些对分类影响较大的特征。而将所有最优弱分类器传给AdaBoost进行训练时,AdaBoost算法可以对最优弱分类器做出选择,选出最主要的特征。这里的AdaBoost每一轮只选出一个特征。通过迭代一定轮数,就可以构建出一个AdaBoost的强分类器,同时也自动做了特征选择。

  1. 给定训练样本集(xi, yi),i=1,2,3,…N,共N个样本,yi取值为0(负样本)或者1(正样本);设人脸正样本的数量为n1,负样本数量为n2; T为训练的最大循环次数;
  2. 初始化样本权重为1/(n1+n2),即为训练样本的初始概率分布;
  3. fort=1,…T:

  1. 对每个(种)特征fj,训练一个弱分类器hj(如上),每个分类器只使用一种Haar特征进行训练。分类误差为:

  1. 从b确定的弱分类器中,找出一个具有最小分类误差的弱分类器ht;
  2. 更新每个样本对应的权重:


这里,如果样本xi被正确分类,则ei=0,否则ei=1,而
βt=εt/(1−εt)

  1. 最终形成的强分类器组成为:


其中:αt=log(1/βt)
在使用AdaBoost算法训练分类器之前,需要准备好正、负样本,根据样本特点选择和构造特征集。由算法的训练过程可知,当弱分类器对样本分类正确,样本的权重会减小;而分类错误时,样本的权重会增加。这样,后面的分类器会加强对错分样本的训练。最后,组合所有的弱分类器形成强分类器,通过比较这些弱分类器投票的加权和与平均投票结果来检测图像。
AdaBoost级联分类器
为了提高人脸检测的速度和精度,最终的分类器还需要通过几个强分类器级联得到。在一个级联分类系统中,对于每一个输入图片,顺序通过每个强分类器,前面的强分类器相对简单,其包含的弱分类器也相对较少,后面的强分类器逐级复杂,只有通过前面的强分类检测后的图片才能送入后面的强分类器检测,比较靠前的几级分类器可以过滤掉大部分的不合格图片,只有通过了所有强分类器检测的图片区域才是有效人脸区域。

在这里我们要引入两个概念,真阳率(True Positive Rate, TPR)和假阳率(False Positive Rate, FPR)。真阳率,是检测出来的真阳性样本数除以所有真实阳性样本数;假阳率,是检测出来的假阳性样本数除以所有真实阴性样本数。
AdaBoost训练后的强分类器,检测结果的高真阳率会伴随者高假阳率,这是强分类阈值的划分导致的。增加分类器个数可以在提高强分类器真阳率的同时降低假阳率,所以级联分类器在训练时要考虑如下平衡,一是强分类器的个数和计算时间的平衡,二是强分类器真阳率和假阳率之间的平衡。

opencv中内置了已经训练好的级联人脸、眼睛、嘴部等检测器,以.XML格式存储,可以将它们应用于图片及实时视频流的检测。
opencv的人脸检测级联检测器是最稳定和准确的,但在许多情况下眼睛检测和嘴巴检测的效果要差上许多。如果要对眼睛和嘴巴进行检测,可以尝试python、dlib、opencv工作流,它的效果更好、速度更快。
Haar级联算法是OpenCV最流行的目标检测算法,主要优点是速度快,尽管许多算法(如HOG+线性SVM、SSDs、更快的R-CNN、YOLO等等)比Haar级联算法更精确。但如果需要纯粹的速度,就是无法打败OpenCV的Haar cascades。Haar级联的缺点是容易出现假阳性检测,应用于推理/检测时需要进行参数调整。

4.1. 内置的haar级联模型

haarcascade_frontalface_default.xml:检测面部
haarcascade_eye.xml:检测左眼和右眼
haarcascade_smile.xml:检测面部是否存在嘴部
haarcascade_eye_tree_eyeglasses.xml:检测是否带墨镜
haarcascade_frontalcatface.xml:检测猫脸
haarcascade_frontalcatface_extended.xml:检测猫脸延伸
haarcascade_frontalface_alt.xml:检测人脸属性
haarcascade_frontalface_alt_tree.xml
haarcascade_frontalface_alt2.xml
haarcascade_fullbody.xml:检测全身
haarcascade_lefteye_2splits.xml:检测左眼
haarcascade_licence_plate_rus_16stages.xml:检测证件
haarcascade_lowerbody.xml:检测下半身
haarcascade_profileface.xml
haarcascade_righteye_2splits.xml:检测右眼
haarcascade_russian_plate_number.xml:检测俄罗斯字母车牌号
haarcascade_upperbody.xml:检测上半身

4.2. 图像检测

由于opencv中已经内置了训练好的haar级联模型,我们可以直接调用。


首先,实例化CascadeClassifier类。
语法:cv2.CascadeClassifier(builtins.object)
参数:builtins.object---内置.XML模型的位置(一般位于….\Library\etc\haarcascades文件夹中)


实例化后,我们便可以调用检测器的内部属性和方法,下面介绍比较常用的函数方法。
detectMultiScale()
描述:检测输入图像中不同大小的对象。若检测到对象,每个对象以行向量(x, y, w, h)的形式返回,函数的返回结果是一个矩阵。
语法:detectMultiScale(imge[, scaleFactor[, minNeighbors[, flags[, minSize[, maxSize]]]]])--->matrix
参数:
image---待检测灰度图片。
scaleFactor---表示在前后两次相继的扫描中,搜索窗口的比例系数。默认为1.1即每次搜索窗口依次扩大10%。
minNeighbors---表示构成检测目标的相邻矩形的最小个数(默认为3个)。如果组成检测目标的小矩形的个数和小于 min_neighbors - 1 都会被排除。如果min_neighbors 为 0, 则函数不做任何操作就返回所有的被检候选矩形框,这种设定值一般用在用户自定义对检测结果的组合程序上。
flags---CASCADE_DO_CANNY_PRUNING = 1,这个值告诉分类器跳过平滑(无边缘区域),利用Canny边缘检测器来排除一些边缘很少或者很多的图像区域;CASCADE_SCALE_IMAGE = 2,这个值告诉分类器不要缩放分类器,而是缩放图像(处理好内存和缓存的使用问题,这可以提高性能)就是按比例正常检测;CASCADE_FIND_BIGGEST_OBJECT = 4,告诉分类器只返回最大的目标(这样返回的物体个数只可能是0或1)只检测最大的物;CASCADE_DO_ROUGH_SEARCH = 8,只可与CASCADE_FIND_BIGGEST_OBJECT一起使用,这个标志告诉分类器在任何窗口,只要第一个候选者被发现则结束寻找(当然需要足够的相邻的区域来说明真正找到了),只做初略检测。
minSize---目标(例如人脸)对象的最小尺寸,小于该尺寸的对象将不被检测到。
maxSize---目标(例如人脸)对象的最大尺寸,大于该尺寸的对象将不被检测到。

import cv2
import tkinter as tk
from tkinter import filedialog

def img_test():
    # 获取选择文件路径
    # 实例化
    root = tk.Tk()
    root.withdraw()
    # 获取文件或文件夹的绝对路径路径
    return filedialog.askopenfilename()

def haar_detection():
    face_path = 'haarcascades\\haarcascade_frontalface_default.xml'
    eye_path = 'haarcascades\\haarcascade_eye.xml'
    smile_path = 'haarcascades\\haarcascade_smile.xml'
    diction = {'face': face_path, 'eye': eye_path, 'smile': smile_path}
    for i in diction:
        diction[i] = cv2.CascadeClassifier(diction[i])
    return diction

def image_detection(img, detection):
    image_gray = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)
    face_ret = detection['face'].detectMultiScale(image_gray, scaleFactor=1.02, minNeighbors=5, minSize=(15, 15),
                                                  maxSize=(50, 50), flags=cv2.CASCADE_DO_CANNY_PRUNING)
    if len(face_ret) != 0:
        for (x, y, w, h) in face_ret:
            face_roi = image_gray[y:y + h, x:x + w]
            eye_ret = detection['eye'].detectMultiScale(face_roi, scaleFactor=1.1, minNeighbors=3, minSize=(15, 15),
                                                        flags=cv2.CASCADE_SCALE_IMAGE)
            smile_ret = detection['smile'].detectMultiScale(face_roi, scaleFactor=1.1, minNeighbors=3, minSize=(15, 15),
                                                            flags=cv2.CASCADE_SCALE_IMAGE)
            for (xx, yy, ww, hh) in eye_ret:
                pt1 = (x + xx, y + yy)
                pt2 = (pt1[0]+ww, pt1[1]+hh)
                cv2.rectangle(img, pt1, pt2, (255, 0, 0), 2)
            for (xx, yy, ww, hh) in smile_ret:
                pt1 = (x + xx, y + yy)
                pt2 = (pt1[0]+ww, pt1[1]+hh)
                cv2.rectangle(img, pt1, pt2, (0, 255, 0), 2)
            cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2)
        print('图像中有人脸')
    else:
        print('图像中无人脸')

    return img

if __name__ == '__main__':
    image = cv2.imread('image\\icon.jpg')
    cv2.namedWindow('Press q to exit and n to load the next picture', cv2.WINDOW_NORMAL)
    cv2.imshow('Press q to exit and n to load the next picture', image)
    while 1:
        k = cv2.waitKey()
        if k == ord('n'):
            image = cv2.imread(img_test())
            detector = haar_detection()
            image = image_detection(image, detector)
            cv2.imshow('Press q to exit and n to load the next picture', image)
        elif k == ord('q'):
            break
    cv2.destroyAllWindows()

测试样张:

  1. 第九节、人脸检测之Haar分类器 - 大奥特曼打小怪兽 - 博客园
  2. (四十八)Haar级联检测器
  3. AdaBoost 人脸检测介绍(3) : AdaBoost算法流程_adaboost算法流程图_nk_wavelet的博客-CSDN博客
  4. 【机器学习】传统目标检测算法之级联分类器Cascade_机器学习cascade级联_zhaosarsa的博客-CSDN博客
  5. 真阳率(true positive rate)、假阳率(false positive rate),AUC,ROC_生活不只*眼前的苟且的博客-CSDN博客
  6. haar+adaboost结合讲解(偏重实际)_haar算子+adaboost分类器详解_playezio的博客-CSDN博客
  7. 使用OpenCV,Haar级联检测器进行面部、眼睛、嘴部检测_haarcascade_frontalface_alt_程序媛一枚~的博客-CSDN博客