图像处理Task03——Haar特征描述算子-人脸检测
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 Lienhart和Jochen Maydt两位大牛将这个检测器进行了扩展,最终形成了OpenCV现在的Haar分类器。
3.2 学习目标
- 理解Haar-like特征
- 理解使用积分图来计算Haar特征值算法
- 理解Haar特征归一化算法
- Haar分类器学习
- 学会使用OpenCV自带的Haar分类器进行人脸检测
3.3 学习内容
3.3.1 Haar-like 特征
Haar特征是基于“块”的特征,也被称为矩形特征。Haar特征(模板)分为三类:边缘特征、线性特征、中心特征和对角线特征。特征模板内有白色和黑色两种矩形,并定义该模板的特征值为白色矩形像素和减去黑色矩形像素和。Haar特征值反映了图像的灰度变化情况。例如:脸部的一些特征能由矩形特征简单的描述,如:眼睛要比脸颊颜色要深,鼻梁两侧比鼻梁颜色要深,嘴巴比周围颜色要深等。但矩形特征只对一些简单的图形结构,如边缘、线段较敏感,所以只能描述特定走向(水平、垂直、对角)的结构。
可以将它们理解成为一个窗口,这个窗口将在图像中做步长为1的滑动,最终遍历整个图像。 当一次遍历结束后,窗口将在宽度或长度上成比例的放大,再重复之前遍历的步骤,直到放大到最后一个比例后结束。
可以放大的比例系数由haar窗口的初始宽高以及整个图像的宽高确定:设在宽度上可以放大的最大倍数为Kw,高度上可以放大的最大倍数为Kh,计算公式如下:
Kw=wI/wwin
Kh=hI/hwin
其中wI和hI是整个图像的宽高,wwin和hwin是haar窗口的初始宽高,可以放大的倍数为Kw⋅Kh。
Haar-like特征提取过程就是利用上面定义的窗口在图像中滑动,滑动到一个位置的时候,将窗口覆盖住的区域中的白色位置对应的像素值的和减去黑色位置对应的像素值的和,得到的一个数值就是haar特征中一个维度。
矩形特征可位于图像任意位置,大小也可以任意改变,所以矩形特征值是矩形模版类别、矩形位置和矩形大小这三个因素的函数。故类别、大小和位置的变化,使得很小的检测窗口含有非常多的矩形特征,如:在24*24像素大小的检测窗口内矩形特征数量可以达到16万个。这样就有两个问题需要解决了:
(1)如何快速计算那么多的特征?—积分图大显神通;
(2)哪些矩形特征才是对分类器分类最有效的?—如通过AdaBoost算法来训练。
3.3.2 使用积分图来计算Haar特征值
积分图是(Integral Image)类似动态规划的方法,主要的思想是将图像从起点开始到各个点所形成的矩形区域像素之存在数组中,当要计算某个区域的像素和时可以直接从数组中索引,不需要重新计算这个区域的像素和,从而加快了计算。
上面这幅图中有四个区域,A,B,C,D。我们将左上角的点记为0,区域AA的所有点像素值的和记为sumAsumA,点0与点1之间的积分图记为integral(0,1)那么根据积分图的定义:
sumA=integral(0,1)
sumA+B+C+D=integral(0,4)
区域D的像素值和sumD就应该为integral(1,4),但是注意,自积分图中是没有从点1到点4的概念的,它所有的起点都应该是点0,所以:
sumD=integral(1,4)=integral(0,4)−integral(0,2)−integral(0,3)+integral(0,1)
转化一下就是
sumD=sumA+B+C+D−sumA+B−sumA+C+sumA
通过一次遍历图像得到“积分图”(Integralimage),也叫Summed Area Table,之后任何一个Haar矩形特征都可以通过查表的方法(Look Up Table)和有限次简单运算得到,大大减少了运算次数。
原图像A经过“积分”后得到相同大小的图像B,B中(x,y)处的值B(x,y)是原图像(x,y)位置左上角方向所有像素的和,即:
举例:
得到图像的积分图后,特征模板的Haar特征值就很好求了。举例如下:
图中红色方框为特征模板所处位置,特征值即为红色方框中白色区域减去灰色区域,
灰色区域可表示为:B(4)-B(5)-B(3)+B(1)
白色区域可表示为:B(5)-B(6)-B(2)+B(1)
则特征值为:
B(5)-B(6)-B(2)+B(1) - ( B(4)-B(5)-B(3)+B(1) )=B(4)-B(5)-B(3)+B(2)
3.3.3 Haar特征归一化算法
从上图我们可以发现,仅仅一个128大小的Haar特征计算出的特征值变化范围从-2000~+6000,跨度非常大。这种跨度大的特性不利于量化评定特征值,所以需要进行“归一化”,压缩特征值范围。假设当前检测窗口中的图像像素为𝑖(𝑥,𝑦)i(x,y),当前检测窗口为𝑤∗ℎw∗h大小(例如上图中为2020大小),OpenCV采用如下方式“归一化”:
1、计算检测窗口中图像的灰度值和灰度值平方和: s u m = ∑ i ( x , y ) sum=\sum i(x,y) sum=∑i(x,y)
s q s u m = ∑ i 2 ( x , y ) sq_{sum}=\sum i^2(x,y) sqsum=∑i2(x,y)
2、计算平均值: m e a n = s u m w ∗ h mean = \frac{sum}{w*h} mean=w∗hsum
s q m e a n = s q s u m w ∗ h sq_{mean}=\frac{sq_{sum}}{w*h} sqmean=w∗hsqsum
3、计算归一化因子: v a r N o r m F a c t o r = s q m e a n − m e a n 2 varNormFactor=\sqrt{sq_{mean}-mean^2} varNormFactor=sqmean−mean2 4、归一化特征值: n o r m V a l u e = f e a t u r e V a l u e v a r N o r m F a c t o r normValue=\frac{featureValue}{varNormFactor} normValue=varNormFactorfeatureValue 之后使用归一化的特征值𝑛𝑜𝑟𝑚𝑉𝑎𝑙𝑢𝑒与阈值对比。
3.3.4 Haar分类器学习
Haar分类器 = Haar-like特征 + 积分图(Integral Image)方法 + AdaBoost + 级联
Haar分类器算法的要点如下:
1)使用Haar-like特征做检测
2)使用积分图(Integral Image)对Haar-like特征求值进行加速
3)使用AdaBoost算法训练区分人脸和非人脸的强分类器
4)使用筛选式级联把强分类器级联到一起,提高准确率
级联分类器是如何训练的呢?首先需要训练出每一个弱分类器,然后把每个弱分类器按照一定的组合策略,得到一个强分类器,我们训练出多个强分类器,然后按照级联的方式把它们组合在一块,就会得到我们最终想要的Haar分类器。
-
弱分类器训练
在弱分类器训练的过程中,训练采用的照片一般都是20*20左右的小图片,弱分类器训练的具体步骤:1、对于每个特征 𝑓,计算所有训练样本的特征值,并将其排序:
2、扫描一遍排好序的特征值,对排好序的表中的每个元素,计算下面四个值:
计算全部正例的权重和𝑇+;
计算全部负例的权重和𝑇−;
计算该元素前之前的正例的权重和𝑆+;
计算该元素前之前的负例的权重和𝑆−;
3、选取当前元素的特征值 F k , j F_{k,j} Fk,j和它前面的一个特征值 F k , j − 1 F_{k,j-1} Fk,j−1之间的数作为阈值,所得到的弱分类器就在当前元素处把样本分开 —— 也就是说这个阈值对应的弱分类器将当前元素前的所有元素分为人脸(或非人脸),而把当前元素后(含)的所有元素分为非人脸(或人脸)。该阈值的分类误差为: e = m i n ( S + + ( T − − S − ) , S − + ( T + − S + ) ) e=min(S^++(T^--S^-),S^-+(T^+-S^+)) e=min(S++(T−−S−),S−+(T+−S+)) 于是,通过把这个排序表从头到尾扫描一遍就可以为弱分类器选择使分类误差最小的阈值(最优阈值),也就是选取了一个最佳弱分类器。
-
训练强分类器
1.、给定训练样本集 ( x i , y i ) , i = 1 , 2 , 3 , … N \left(x_{i}, y_{i}\right), i=1,2,3, \ldots N (xi,yi),i=1,2,3,…N,共N个样本, y i y_i yi取值为0(负样本)或者1(正样本);设人脸正样本的数量为 n 1 n_1 n1,负样本数量为 n 2 n_2 n2; T为训练的最大循环次数;2.、初始化样本权重为 1 n 1 + n 2 \frac{1}{n_1+n_2} n1+n21,即为训练样本的初始概率分布;
3、 f o r t = 1 , . . . T for\ t=1,...T for t=1,...T:
①权重归一化 ω t , i = ω t , i ∑ j − 1 n ω t , j ω_{t,i}=\frac{ω_{t,i}}{\sum\limits_{j-1}^{n}ω_{t,j}} ωt,i=j−1∑nωt,jωt,i②对每个(种)特征𝑓𝑗fj,训练一个弱分类器ℎ𝑗hj(如上),每个分类器只使用一种Haar特征进行训练。分类误差为: ε j = ∑ i ω i ∣ h j ( x i ) − y i ∣ ε_j=\sum\limits_{i}ω_i|h_j(x_i)-y_i| εj=i∑ωi∣hj(xi)−yi∣ ③从②确定的弱分类器中,找出一个具有最小分类误差的弱分类器 h t h_t ht; ④更新每个样本对应的权重: ω t + 1 , i = ω t , i β t 1 − e i \omega_{t+1, i}=\omega_{t, i} \beta_{t}^{1-e_{i}} ωt+1,i=ωt,iβt1−ei
这里,如果样本 x i x_i xi被正确分类,则 e i = 0 e_i=0 ei=0,否则 e i = 1 e_i=1 ei=1,而 β t = ε t 1 − ε t \beta_t=\frac{ε_t}{1-ε_t} βt=1−εtεt
4、最终形成的强分类器组成为: KaTeX parse error: Expected '}', got '\right' at position 144: …e } \end{array}\̲r̲i̲g̲h̲t̲. 其中: α t = l o g 1 β t \alpha_t=log\frac{1}{\beta_t} αt=logβt1
在使用Adaboost算法训练分类器之前,需要准备好正、负样本,根据样本特点选择和构造特征集。由算法的训练过程可知,当弱分类器对样本分类正确,样本的权重会减小;而分类错误时,样本的权重会增加。这样,后面的分类器会加强对错分样本的训练。最后,组合所有的弱分类器形成强分类器,通过比较这些弱分类器投票的加权和与平均投票结果来检测图像。
-
训练级联分类器
训练级联分类器的目的就是为了检测的时候,更加准确,这涉及到Haar分类器的另一个体系,检测体系,检测体系是以现实中的一幅大图片作为输入,然后对图片中进行多区域,多尺度的检测,所谓多区域,是要对图片划分多块,对每个块进行检测,由于训练的时候用的照片一般都是20*20左右的小图片,所以对于大的人脸,还需要进行多尺度的检测,多尺度检测机制一般有两种策略:- 一种是不改变搜索窗口的大小,而不断缩放图片,这种方法显然需要对每个缩放后的图片进行区域特征值的运算,效率不高;
- 另一种方法,不断扩大搜索窗口,进行搜索,解决了第一种方法的弱势。
在区域放大的过程中会出现同一个人脸被多次检测,这需要进行区域的合并,这里不作探讨。
无论哪一种搜索方法,都会为输入图片输出大量的子窗口图像,这些子窗口图像经过筛选式级联分类器会不断地被每一个节点筛选,抛弃或通过。
-
总结
从上面所述内容我们可以总结Haar分类器训练的五大步骤:1、准备人脸、非人脸样本集;
2、计算特征值和积分图;
3、筛选出T个优秀的特征值(即最优弱分类器);
4、把这个T个最优弱分类器传给AdaBoost进行训练。
5、级联,也就是强分类器的强强联手。
在开始前,一定要记住,以20*20窗口为例,就有78,460的特征数量,筛选出T个优秀的特征值(即最优弱分类器),然后把这个T个最优弱分类器传给AdaBoost进行训练得到一个强分类器,最后将强分类器进行级联。
3.3.4 使用OpenCV自带的Haar分类器进行人脸检测
OpenCV 自带了训练器和检测器,这里介绍的XML文件,就是OpenCV自带的检测器。位置如下(在对应的环境的cv2库的data文件夹下):
从这些文件名可以知道这些级联适用于检测人脸、眼睛、鼻子和嘴等部位的跟踪,这些文件需要正面、直立的人体图像。
xml文件主要保存相关的特征矩阵,以及各个弱分类器相关的信息。
OpenCV中主要使用detectMultiScale函数:
detectMultiScale(image[,scaleFactor[,minNeighbors[,flags[,minSize[,maxSize]]]]])
- image:表示的是要检测的输入图像
- scaleFactor:为每一个图像尺度中的尺度参数,默认值为1.1。
- scaleFactor参数可以决定两个不同大小的窗口扫描之间有多大的跳跃,这个参数设置的大,则意味着计算会变快,但如果窗口错过了某个大小的人脸,则可能丢失物体。
- minNeighbors:参数为每一个级联矩形应该保留的邻近个数,默认为3。minNeighbors控制着误检测,默认值为3表明至少有3次重叠检测,我们才认为人脸确实存。
- flags:对于新的分类器没有用(但目前的haar分类器都是旧版的,CV_HAAR_DO_CANNY_PRUNING,这个值告诉分类器跳过平滑(无边缘区域)。利用Canny边缘检测器来排除一些边缘很少或者很多的图像区域;CV_HAAR_SCALE_IMAGE,这个值告诉分类器不要缩放分类器。而是缩放图像(处理好内存和缓存的使用问题,这可以提高性能。)就是按比例正常检测;CV_HAAR_FIND_BIGGEST_OBJECTS,告诉分类器只返回最大的目标(这样返回的物体个数只可能是0或1)只检测最大的物,CV_HAAR_DO_ROUGH_SEARCH,他只可与CV_HAAR_FIND_BIGGEST_OBJECTS一起使用,这个标志告诉分类器在任何窗口,只要第一个候选者被发现则结束寻找(当然需要足够的相邻的区域来说明真正找到了。),只做初略检测.
- minSize:为目标的最小尺寸
- maxSize:为目标的最大尺寸
实例代码:
- 我们首先把上述的data文件夹中需要的xml文件复制到当前项目路径下,然后创建py文件
import cv2
import numpy as np
# OpenCV 自带了训练器和检测器,这里介绍的XML文件,就是OpenCV自带的检测器
haar_front_face_xml = './haarcascade_frontalface_default.xml'
# 1.静态图像中的人脸检测
def StaticDetect(filename):
# 创建一个级联分类器 加载一个 .xml 分类器文件. 它既可以是Haar特征也可以是LBP特征的分类器.
face_cascade = cv2.CascadeClassifier(haar_front_face_xml)
# 加载图像
img = cv2.imread(filename)
# 转换为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 进行人脸检测,传入scaleFactor,minNegihbors,分别表示人脸检测过程中每次迭代时图像的压缩率以及
# 每个人脸矩形保留近似数目的最小值
# 返回人脸矩形数组
faces = face_cascade.detectMultiScale(gray_img, 1.3, 5)
for (x, y, w, h) in faces:
# 在原图像上绘制矩形
img = cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 5)
cv2.namedWindow('Face Detected!', 0)
cv2.imshow('Face Detected!', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 摄像头中的人脸检测
def DynamicDetect():
'''
打开摄像头,读取帧,检测帧中的人脸,扫描检测到的人脸中的眼睛,对人脸绘制蓝色的矩形框,对人眼绘制绿色的矩形框
'''
# 创建一个级联分类器 加载一个 .xml 分类器文件. 它既可以是Haar特征也可以是LBP特征的分类器.
face_cascade = cv2.CascadeClassifier(haar_front_face_xml)
# 打开摄像头
camera = cv2.VideoCapture(0)
cv2.namedWindow('Dynamic')
while True:
# 读取一帧图像
ret, frame = camera.read()
# 判断图片读取成功?
if ret:
gray_img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 人脸检测
faces = face_cascade.detectMultiScale(gray_img, 1.3, 5)
for (x, y, w, h) in faces:
# 在原图像上绘制矩形
cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 2)
roi_gray = gray_img[y:y + h, x:x + w]
cv2.imshow('Dynamic', frame)
# 如果按下q键则退出
if cv2.waitKey(100) & 0xff == ord('q'):
break
camera.release()
cv2.destroyAllWindows()
#视频文件中的人脸检测
def videodetect(filename):
face_cascade = cv2.CascadeClassifier(haar_front_face_xml)
'#从文件播放视频'
cap = cv2.VideoCapture(filename)
while cap.isOpened():
'# 逐帧捕获'
ret, frame = cap.read()
'#如果正确读取帧,ret为true'
if not ret:
print('cannot receive')
break
if ret:
gray_img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 人脸检测
faces = face_cascade.detectMultiScale(gray_img, 1.3, 5)
for (x, y, w, h) in faces:
# 在原图像上绘制矩形
cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 2)
roi_gray = gray_img[y:y + h, x:x + w]
cv2.imshow('Dynamic', frame)
# 如果按下q键则退出
if cv2.waitKey(100) & 0xff == ord('q'): # 设置视频播放速度
break
'#完成所有操作后释放捕获器'
cap.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
filename = '4.jpeg'
filename2 = ''
# StaticDetect(filename)
# DynamicDetect()
videodetect(filename2)
图片结果: