目录
第七步:Multi-Scale Object Localisation
下面所有这种类型框内的文字可以先不看,这种框内文字仅仅是帮助你解决一些乱七八糟的想法用的,等看完全文一遍后,再看这种类型框内文字。
特征描述子(Feature Descriptor)
特征描述子就是图像的表示,抽取了有用的信息,丢掉了不相关的信息。通常特征描述子会把一个w*h*3(宽高3,3个channel)的图像转换成一个长度为n的向量/矩阵。比如一副64*128*3的图像,经过转换后输出的图像向量长度可以是3780。
什么样子的特征是有用的呢?假设我们想要预测一张图片里面衣服上面的扣子,扣子通常是圆的,而且上面有几个洞,那你就可以用边缘检测(edge detector),把图片变成只有边缘的图像,然后就可以很容易的分辨了,那么对于这张图边缘信息就是有用的,颜色信息就是没有用的。而且好的特征应该能够区分纽扣和其它圆形的东西的区别。
方向梯度直方图(HOG)中,梯度的方向分布被用作特征。沿着一张图片X和Y轴的方向上的梯度是很有用的,因为在边缘和角点的梯度值是很大的,我们知道边缘和角点包含了很多物体的形状信息。
(HOG特征描述子可以不局限于一个长度,也可以用很多其他的长度,这里只记录一种计算方法。)
方向梯度直方图总体流程
下面这张图就是方向梯度直方图的总体流程图,这里仅仅是让读者能有一个总体概念,不懂没关系,只需要将下面黑色方框中的文字记录在纸上,然后结合处理过程“怎么计算方向梯度直方图呢?”一一印证即可。
需要注意的是:
在“怎么计算方向梯度直方图呢?”中每一个cell的大小为8*8,每一个block大小为2*2,然后一幅图片由很多个block组成。这里也是就需要有概念,仅需要将这几个数值记录在纸上,然后与下面的“怎么计算方向梯度直方图呢?”一一印证即可。
总流程图
怎么计算方向梯度直方图呢?
我们会先用图像的一个patch来解释。
第一步:预处理
Patch可以是任意的尺寸,但是有一个固定的比例,比如当patch长宽比1:2,那patch大小可以是100*200, 128*256或者1000*2000但不可以是101*205。
这里有张图是720*475的,我们选100*200大小的patch来计算HOG特征,把这个patch从图片里面抠出来,然后再把大小调整成64*128。(总流程图中的“检测窗口”)
为何选择64*128的检测窗口大小?
我们用的64*128大小的检测窗口在人体周围会产生大约16个像素的空白边缘。因为空白边缘增加了有助于检测的上下文信息。将空白边缘从16像素减少到8像素(48*112大小的检测窗口)会在每个窗口的误报率(FPPW False Positives Per Window)时导致6%的性能下降。保持窗口大小为64*128不变,增加人体的尺寸(同样会使空白边缘减少),虽然使得人体的解析度变高,但也会导致性能下降。
还有原文中提到了标准化gamma空间,但是对人体检测作用好像不大,就不加了。
为了减少光照因素的影响,首先需要将整个图像进行规范化(归一化),这种处理能够有效地降低图像局部的阴影和光照变化。
Gamma压缩公式:
比如可以取Gamma=1/2;
hog_preprocess
第二步:计算梯度图像
数学原理
在计算梯度之前,需了解图像梯度的计算方法。首先看看数学上的梯度求法,
其实呢,是可以直接用x和y方向的梯度的,但是这样不够直观,不能直接感受梯度具体是朝哪个方向,因此就将x和y方向的梯度合并起来,其中梯度的模,也就是幅值是
梯度的方向为
但是我们不妨关注下求模的公式,emmm,是不是感觉计算稍微有点繁琐,又是平方,又是开根号的。不妨将他简化一下,反正只要能表示他的长度就行了,这就变为
你看这样求起来多方便,就一个绝对值以及求和。
到这里数学阶段的理论就讲完了。
实际图像上的操作
但到图像上时,该怎么一一对应上面的公式呢,图像的像素点都是离散的,怎么求梯度呢?其实也很简单,你想梯度不就是该点的变化方向嘛,既然不连续,那就简化点,直接用该像素点的左右像素点的差作为x方向的梯度,上下像素点的差作为y方向的梯度,这样就OK了(虽然我感觉粗糙了点,但离散情况下好像也只能这么搞了,既反映了该像素点的变化情况,还计算简单,完美)。那么就可以得到水平x,垂直y方向的梯度,如下面公式所示。
这时有人就问了,为啥就选择左右像素的差值也就是[-1,0,1]的模板呢,我也可以使用其他方式,只要能反应梯度就行了呀?
其实作者也做了实验的:5个因子的一维模版[1,-8,0,8,-1]比[-1,0,1]模版有1%的性能下降,2*2的对角线模版有1.5%的性能下降,无中心的[-1,1]模版也会导致1.5%的性能下降。就问你满不满足。。。
既然梯度得到了,就可以将其转化为模值和方向了,具体如下公式
补充一点
上面的梯度的角度范围可以是0-360,但原文作者做实验表明,在人体检测这个任务上,0-360不合适,效果不如0-180。那为什么可以从0-360转成0-180,其实就是将一个有符号的向量变成了一个无符号的向量。我感觉这样变相增加了该方法的鲁棒性。
首先我们计算水平和垂直方向的梯度,再来计算梯度的直方图。可以用下面的两个kernel来计算,也可以直接用OpenCV里面的kernel大小为1的Sobel算子来计算。
horizontal_vertical_gradient_kernel
下面代码中的img.convertTo(img, CV_32F, 1/255.0)就是在做总流程图中的“归一化”。
Sobel(img, gx, CV_32F, 1, 0, 1)和Sobel(img, gy, CV_32F, 0, 1, 1)在做总流程图中的“计算梯度”。
调用OpenCV代码如下:
-
// C++ gradient calculation.
-
// Read image
-
Mat img = imread("bolt.png");
-
img.convertTo(img, CV_32F, 1/255.0);
-
// Calculate gradients gx, gy
-
Mat gx, gy;
-
Sobel(img, gx, CV_32F, 1, 0, 1);
-
Sobel(img, gy, CV_32F, 0, 1, 1);
-
# Python gradient calculation
-
# Read image
-
img = cv2.imread('bolt.png')
-
img = np.float32(img) / 255.0
-
# Calculate gradient
-
gx = cv2.Sobel(img, cv2.CV_32F, 1, 0, ksize=1)
-
gy = cv2.Sobel(img, cv2.CV_32F, 0, 1, ksize=1)
接着,用下面的公式来计算梯度的幅值g和方向theta:
gradient_direction_formula
可以用OpenCV的cartToPolar函数来计算:
-
// C++ Calculate gradient magnitude and direction (in degrees)
-
Mat mag, angle;
-
cartToPolar(gx, gy, mag, angle, 1);
-
# Python Calculate gradient magnitude and direction ( in degrees )
-
mag, angle = cv2.cartToPolar(gx, gy, angleInDegrees=True)
计算得到的gradient图如下:
左边:x轴的梯度绝对值 中间:y轴的梯度绝对值 右边:梯度幅值
从上面的图像中可以看到x轴方向的梯度主要凸显了垂直方向的线条,y轴方向的梯度凸显了水平方向的梯度,梯度幅值凸显了像素值有剧烈变化的地方。(注意:图像的原点是图片的左上角,x轴是水平的,y轴是垂直的)
图像的梯度去掉了很多不必要的信息(比如不变的背景色),加重了轮廓。换句话说,你可以从梯度的图像中轻而易举的发现有个人。
在每个像素点,都有一个幅值(magnitude)和方向,对于有颜色的图片,会在三个channel上都计算梯度。那么相应的幅值就是三个channel上最大的幅值,角度(方向)是最大幅值所对应的角。
第三步:在8*8的网格中计算梯度直方图
在这一步,上面的patch图像会被分割成8*8大小的网格(如下图),每个网格都会计算一个梯度直方图(这就是总流程图中的“每个cell块对梯度直方图进行梯度投影”)。那为什么要分成8*8的呢?用特征描述子的一个主要原因是它提供了一个紧凑(compact)/压缩的表示。一个8*8的图像有8*8*3=192个像素值,每个像素有两个值(幅值magnitude和方向direction,三个channel取最大magnitude那个),加起来就是8*8*2=128,后面我们会看到这128个数如何用一个9个bin的直方图来表示成9个数的数组。不仅仅是可以有紧凑的表示,用直方图来表示一个patch也可以更加抗噪,一个gradient可能会有噪音,但是用直方图来表示后就不会对噪音那么敏感了。
这里为什么需要使用方块网格,而不是圆或者环呢?
因为原文作者做了实验,就圆和方块效果好,且结果较为接近。为方便说明,就用了方块。
为啥不直接用梯度的幅值然后进行bin的统计->归一化->分类,而是先在cell中进行bin统计->在block中进行归一化->最终分类?(想不通对照本文最后一幅图像思考)
我陷入了个误区,认为反正都是提取特征,然后送入分类器中进行分类,那干嘛不用全局统计呢。这里我又犯了我的老问题,在举例时不加限定条件。现在我们从现实情况出发,你看这个输入图像尺寸为64*128,图像中包含人和背景,那么直接用整幅图像的梯度的幅值统计出的bin能代表什么呢,你设计的这个特征并没有表示出人体的某部分特点,是一个失败的特征。
这时不妨从另一个角度来看,什么样的特征适合描述人呢?那么不妨假设人的边缘与背景还是有较大差异的,那么就需要找出一种特征能区分人体和背景,那么这时就考虑到了用梯度,梯度能表示颜色变化程度和方向。但是如果使用单个像素点的梯度好像对噪声太过敏感了,我们需要缓解这种敏感情况,那么自然会想到在局部区域内做统计不就行了,如果你是一两个小噪声对我们的影响就不是很大,还有就是你在一个合适的区域内做统计,能够忽略掉一些不需要的细节部分,比如说人的衣着等一些变化;另外单个像素还存在一个问题,不能表示空间结构关系,这点很致命,而区域统计则有空间关系,表示了该区域内所有像素整体的一个变化趋势。所以最终使用了一个8*8的区域进行一个统计,既能忽略小噪声,也能忽略掉一些无关的人体信息,同时还能结合部分的空间信息。
为什么选择cell的尺寸为8*8?
当块过大时,对局部图像的适应性变差;当块过小时,有价值的空间信息减少。但我感觉主要是试出来的,说不定对于你的问题其他的尺寸更适合。
这个patch的大小是64*128,分割成8*8的cell,那么一共有64/8 * 128/8 = 8*16=128个网格
对于64*128的这幅patch来说,8*8的网格已经足够大来表示有趣的特征比如脸,头等等。
直方图是有9个bin的向量,代表的是角度0,20,40,60.....160。
我们先来看看每个8*8的cell的梯度都是什么样子:
中间: 一个网格用箭头表示梯度 右边: 这个网格用数字表示的梯度
中间这个图的箭头是梯度的方向,长度是梯度的大小,可以发现箭头的指向方向是像素强度变化方向,幅值是强度变化的大小。
右边的梯度方向矩阵中可以看到角度是0-180度,不是0-360度,这种被称之为"无符号"梯度("unsigned" gradients),因为一个梯度和它的负数是用同一个数字表示的,也就是说一个梯度的箭头以及它旋转180度之后的箭头方向被认为是一样的。那为什么不用0-360度的表示呢?在事件中发现unsigned gradients比signed gradients在行人检测任务中效果更好。一些HOG的实现中可以让你指定signed gradients。
下一步就是为这些8*8的网格创建直方图,直方图包含了9个bin来对应0,20,40,...160这些角度。
下面这张图解释了这个过程。我们用了上一张图里面的那个网格的梯度幅值和方向。根据方向选择用哪个bin, 根据副值来确定这个bin的大小。先来看蓝色圆圈圈出来的像素点,它的角度是80,副值是2,所以它在第五个bin里面加了2,再来看红色的圈圆圈圈出来的像素点,它的角度是10,副值是4,因为角度10介于0-20度的中间(正好一半),所以把幅值一分为二地放到0和20两个bin里面去。
梯度直方图
这里有个细节要注意,如果一个角度大于160度,也就是在160-180度之间,我们知道这里角度0,180度是一样的,所以在下面这个例子里,像素的角度为165度的时候,要把幅值按照比例放到0和160的bin里面去。
角度大于160的情况
值得注意:
这里我要说明一下,上面这个bin的统计计算和平常不太一样,本文的bin的计算方法是将角度与规定bin(0,20,40,60。。。)之间的距离作为权重,距离越近权重越大,距离越远权重越小,比如上面的角度165,幅值85,角度165离bin160只相差5,离bin0也就是bin180相差15,所以幅值具体分配方法是:bin160=(1-(165-160)/20)*85,bin0=85-bin160.
但还有另外一种计算方式:
如果这个像素的梯度方向是20-40度,直方图第2个bin的计数就加一,这样,对cell内每个像素用梯度方向在直方图中进行加权投影(映射到固定的角度范围),就可以得到这个cell的梯度方向直方图了,就是该cell对应的9维特征向量(因为有9个bin)。
像素梯度方向用到了,那么梯度大小呢?梯度大小就是作为投影的权值的。例如说:这个像素的梯度方向是20-40度,然后它的梯度大小是2(假设啊),那么直方图第2个bin的计数就不是加一了,而是加二(假设啊)。
如果继续用上面的角度165和幅值85举例,那么具体的分配方法为:bin160=1*85
把这8*8的cell里面所有的像素点都分别加到这9个bin里面去,就构建了一个9-bin的直方图,上面的网格对应的直方图如下:
8*8网格直方图
这里,在我们的表示中,Y轴是0度(从上往下)。你可以看到有很多值分布在0,180的bin里面,这其实也就是说明这个网格中的梯度方向很多都是要么朝上,要么朝下。
第四步:16*16块归一化
上面的步骤中,我们创建了基于图片的梯度直方图,但是一个图片的梯度对于整张图片的光线会很敏感。如果你把所有的像素点都除以2,那么梯度的幅值也会减半,那么直方图里面的值也会减半,所以这样并不能消除光线的影响。所以理想情况下,我们希望我们的特征描述子可以和光线变换无关,所以我们就想让我们的直方图归一化从而不受光线变化影响。
先考虑对向量用l2归一化的步骤是:
v = [128, 64, 32]
[(128^2) + (64^2) + (32^2) ]^0.5=146.64
把v中每一个元素除以146.64得到[0.87,0.43,0.22]
考虑另一个向量2*v,归一化后可以得到向量依旧是[0.87, 0.43, 0.22]。你可以明白归一化是把scale给移除了。
明明我用了cell,有一定的鲁棒性呀,为什么还是需要归一化呢?(想不通对照本文最后一幅图像思考)
由于局部光照的变化,以及前景背景对比度的变化,使得梯度强度的变化范围非常大,这就需要对梯度做局部对比度归一化。正如上面这个例子讲解的那样,在比cell大一点的局部区域内,光照对比度的变化会导致该区域bin的统计偏大或偏小,这对提取到的特征来说就不是很鲁棒,为了缓解局部光照变化给特征带来的影响就使用了比cell更大的一个区域block。那有人会说干嘛不用全部的cell做归一化,你想到这个问题是好的,但是如果想不通我反手就是一巴掌(等等我脸有点疼o_O),每个统计量除以一个常数你告诉我这个局部光照带来的影响消除了没有?答案显然是没有呀,只是进行了简单的同比缩小而已,484傻。
请注意本文我用了一个词“缓解”,也就是只解决了部分问题,但还有部分问题解决不了。
你也许想到直接在我们得到的9*1的直方图上面做归一化,这也可以,但是更好的方法是从一个16*16的块上做归一化,也就是4个9*1的直方图组合成一个36*1的向量,然后做归一化,接着,窗口再朝后面挪8个像素(看动图)。重复这个过程把整张图遍历一遍。(总流程图中的“对于每个重叠block内的cell进行对比度归一化”,其中每个cell的像素是8*8,每个block的cell是2*2)
Block中各个参数的最终选取:
对于人体对象检测,块的大小为2×2个单元格,单元格的大小为8×8个象素时,检测效果是最好的,错误率约为10%左右。块的大小为3×3个单元格,单元格大小为6×6个象素时,也相差无几。6-8个象素宽的单元格,2-3个单元格宽的块,其错误率都在最低的一个平面上。块的尺寸太大时标准化的作用被削弱了从而导致错误率上升,而如果块的尺寸太小时,有用的信息反而会被过滤掉。
在实际应用中,在Block和Cell划分之后,对于得到各个像区域中,有时候还会为了进行一次高斯平滑,但是对于人体目标检测等问题,该步骤往往可以忽略,实际应用效果不大,估计在主要还是去除区域中噪点,因为梯度对于噪点相当敏感。
Block为啥要重叠滑动呢?
这主要是为了增强块与块之间的关联性,也相当于空间关联性的增强。
上面这个问题提到了关联性,其实重叠滑动只是其中一种解决方式,还有另外一种方法:
线性插值权重分配
这种方法的主要思想是每个Block都对临近的Block都有影响,这种影响,我们可以以一种加权方式附加上去。
基于线性插值的基本思想,对于上图四个方向(横纵两个45度斜角方向)个进行一次线性插值就可以达到权重分配目的。下面介绍一维线性插值。假设x1和x2是x块相邻两块的中心,且x1<x<x2。对w(即权重,一般可直接采用该block的直方图值即h(x))进行线性插值的方法如下式:
![]()
其中b在横纵方向取块间隔,而在斜45度方向则可采用sqrt(2)倍的块间隔。
hog-16x16-block-normalization
第五步:计算HOG特征向量
为了计算这整个patch的特征向量,需要把36*1的向量全部合并组成一个巨大的向量。向量的大小可以这么计算:
-
我们有多少个16*16的块?水平7个,垂直15个,总共有7*15=105次移动。
-
每个16*16的块代表了36*1的向量。所以把他们放在一起也就是36*105=3780维向量。
可视化HOG
通常HOG特征描述子是画出8*8网格中9*1归一化的直方图,见下图。你可以发现直方图的主要方向捕捉了这个人的外形,特别是躯干和腿。
visualizing_histogram
第六步:分类器
有了特征向量,就像固定维度的特征向量送入到线性SVM中进行训练分类。
为什么选择线性SVM?
我们默认使用带有松弛变量(C=0.01)的线性SVM分类器SVMLight(在原版SVM上稍作改动使得处理大规模特征向量时可减少内存占用)。如果使用高斯核函数SVM可以在10-4FPPW时提高大约3%的性能,但需要以更多的运行时间为代价。
第七步:Multi-Scale Object Localisation
上图右上角是对SVM分类结果的置信度做个映射得到检测评分。检测过程就是用固定大小的窗口对多个尺度的图像进行滑窗检测,将多个尺度计算得到的矩形框都还原成原图尺寸(这就相当于把图像的宽高进行放大或者缩小,然后检测框的大小是不变的,这样就变相的产生了多个尺度的检测框),再进行非极大值抑制(NMS,Non-maximum Suppression)处理(不懂可以自行百度,或者参考这篇博客)。在物体检测非极大值抑制应用十分广泛,主要目的是为了消除多余的框,找到最佳的检测框的位置。
HOG的应用
主要用在object detection 领域,特别是行人检测,智能交通系统,当然也有文章提到把HOG用在手势识别,人脸识别等方面。
HOG与SIFT区别
HOG和SIFT都属于描述子,以及由于在具体操作上有很多相似的步骤,所以致使很多人误认为HOG是SIFT的一种,其实两者在使用目的和具体处理细节上是有很大的区别的。HOG与SIFT的主要区别如下:
① SIFT是基于关键点特征向量的描述。
② HOG是将图像均匀的分成相邻的小块,然后在所有的小块内统计梯度直方图。
③ SIFT需要对图像尺度空间下对像素求极值点,而HOG中不需要。
④ SIFT一般有两大步骤,第一个步骤是对图像提取特征点,而HOG不会对图像提取特征点。
HOG的优点
HOG表示的是边缘(梯度)的结构特征,因此可以描述局部的形状信息;
位置和方向空间的量化一定程度上可以抑制平移和旋转带来的影响;
采取在局部区域归一化直方图,可以部分抵消光照变化带来的影响。
由于一定程度忽略了光照颜色对图像造成的影响,使得图像所需要的表征数据的维度降低了。
而且由于它这种分块分单元的处理方法,也使得图像局部像素点之间的关系可以很好得到的表征。
HOG的缺点
描述子生成过程冗长,导致速度慢,实时性差;
很难处理遮挡问题。
由于梯度的性质,该描述子对噪点相当敏感
文献
HOG的经典论文:Dalal N, Triggs B. Histograms of oriented gradients for human detection[C]//Computer Vision and Pattern Recognition, 2005. CVPR 2005. IEEE Computer Society Conference on. IEEE, 2005, 1: 886-893.(2016:Google Citation: 14046)
下载链接:https://hal.inria.fr/file/index/docid/548512/filename/hog_cvpr2005.pdf
较为详细的博士论文:Dalal N. Finding people in images and videos[D]. Institut National Polytechnique de Grenoble-INPG, 2006.(Google Citation: 337)
下载链接:https://tel.archives-ouvertes.fr/tel-00390303/document
采用级联结构对HOG检测进行加速:Zhu Q, Yeh M C, Cheng K T, et al. Fast human detection using a cascade of histograms of oriented gradients[C]//Computer Vision and Pattern Recognition, 2006 IEEE Computer Society Conference on. IEEE, 2006, 2: 1491-1498.(Google Citation: 1270)
参考
https://www.leiphone.com/news/201708/ZKsGd2JRKr766wEd.html
https://www.cnblogs.com/wjgaas/p/3597248.html
https://www.cnblogs.com/wyuzl/p/6792216.html
NMS:https://www.cnblogs.com/makefile/p/nms.html
对经典文章的翻译:http://blog.csdn.net/masibuaa/article/details/14056807
从理论到OpenCV实现:http://blog.csdn.net/zhazhiqiang/article/details/21047207