【Datawhale】计算机视觉下 —— HOG特征描述算子

前言

概念介绍

HOG特征:方向梯度直方图(Histogram of Oriented Gradient,HOG)特征是一种进行物体检测时的特征描述子,它是一种用于表征图像局部梯度方向和梯度强度分布特性的描述符。

特征描述子:计算机不能直接识别图像,所以特征描述子实际上就是图像的数字表示,但它抽取了有用的信息,且丢掉了不相关的信息。通常特征描述子会把一个\(W \times H \times 3\)的图像转换成一个一维的、长度为\(N\)的向量表示。

适用场景

首先单独说HOG特征的用处:计算图像梯度后,把图片变成只有边缘的图像,如下图所示。在一些颜色信息显然不起作用的图像处理任务中,我们就可以借助HOG特征将颜色信息剔除,留下边缘信息做进一步的处理。

HOG特征能够很好地反映人体或汽车的轮廓,而且对整体光照、亮度等不敏感。

现在比较流行HOG和SVM组合使用,在行人检测、车辆检测、跟踪方面有比较广泛的运用。

传统的SVM可以利用训练数据生成非常精确的二分类器,也广泛用于解决一些计算机视觉方面的任务。因此两者结合之后,在检测方面具有良好的性能和鲁棒性。

具体两者是怎么结合的,在之后会详细进行介绍。

图像梯度

在介绍HOG特征之前,我们应该先对图像梯度有所了解。

图像梯度计算的是图像变化的速度,对于图像的边缘部分,其灰度值变化较大,梯度值也较大;相反,对于图像中比较平滑的部分,其灰度值变化较小,相应的梯度值也较小。一般情况下,图像梯度计算的是图像的边缘信息。

严格地说,图像梯度计算需要求导数,但是图像梯度一般通过计算像素值的差来计算梯度的近似值。

比如下面这张图像边界示意图所示:

针对左图,通过垂直方向的线条A和线条B的位置,可以计算图像水平方向的边界:

  • 对于线条A和线条B,它们的右侧像素值和左侧像素值的差值不为0,所以它们属于边界。
  • 对于其余位置的线条而言,它们的左右两侧的像素值差值为0,所以不是边界。

针对右图,通过水平方向的线条A和线条B的位置,可以计算图像垂直方向的边界:

  • 对于线条A和线条B,它们的上下两侧的像素差值为零,因此是边界。
  • 对于其他位置的线条而言,它们的上下两侧的像素值差值为0,所以不是边界。

根据大学数学基础可以知道,图像的梯度也有自己的方向,比如上面这张图包含的垂直、水平方向。

所以如果我们现在要计算某个像素点的方向梯度,可以先计算它在垂直和水平方向的梯度,进而得到它的最终梯度值。

HOG特征算法

img

上图为HOG特征算法的基本流程,其具体过程如下:

  1. 输入待处理图像,进行标准化操作,包括图像缩放、图像灰度化、Gamma校正。
  2. 预处理完毕后,便可以计算每个像素点的图像梯度。
  3. 确定图像分割单位Cell大小,为每个细胞单元构建梯度方向直方图。
  4. 把细胞单元组合成大的块(block),块内归一化梯度直方图。
  5. 收集图像的HOG特征,得到最终的特征向量,并进行之后的分类使用。

下面,将给出算法流程中的每个步骤,结合实例进行具体介绍:

图像灰度化

由于颜色信息作用不大,通常转化为灰度图。 对于彩色图像,将RGB分量转化成灰度图像,其转化公式为:

\[gray = 0.3 * R + 0.59 * G + 0.11 * B \]

其实图像灰度化是可选操作,因为灰度图像和彩色图像都可以用于计算梯度图。

对于彩色图像而言,先对三通道颜色值分别计算梯度,然后取梯度值最大的那个作为该像素的梯度

所以,首先我们读取图片,并将图片进行灰度处理。(由于原图太大了,原图缩小成了原来的20%)

import cv2 as cv
imgpath = '../img/cv_5.jpeg'  # 图片路径

img = cv.imread(imgpath)
scale_percent = 20  # 缩小成20%
width = int(img.shape[1] * scale_percent / 100)
height = int(img.shape[0] * scale_percent / 100)
dim = (width, height)
img = cv.resize(img, dim, interpolation=cv.INTER_LINEAR)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()

Gamma校正

Gamma变换就是用来图像增强,其提升了暗部细节,简单来说就是通过非线性变换,让图像从暴光强度的线性响应变得更接近人眼感受的响应,即将漂白(相机曝光)或过暗(曝光不足)的图片,进行矫正。

其输出图像灰度值与输入图像灰度值呈指数关系:

\[V_{out} = CV_{in}^\gamma \]

这个指数即为Gamma。

注意这个\(V_{in}\) 的取值范围为0~1。(之前以为是0-255,纠结了好久)

经过Gamma变换后的输入和输出图像灰度值关系如图1所示:

横坐标是输入灰度值,纵坐标是输出灰度值,蓝色曲线是gamma值小于1时的输入输出关系,红色曲线是gamma值大于1时的输入输出关系。

可以观察到,当gamma值小于1时(蓝色曲线),图像的整体亮度值得到提升,同时低灰度处的对比度得到增加,更利于分辩低灰度值时的图像细节。

在这里插入图片描述

⚠️ : 所以可以总结如下:
\(\gamma > 1\),较亮的区域灰度被拉伸,较暗的区域灰度被压缩的更暗,图像整体变暗;
$ \gamma<1$,较亮的区域灰度被压缩,较暗的区域灰度被拉伸的较亮,图像整体变亮;

所以,在HOG特征计算中,当图像光照不均匀时,可以通过Gamma校正,将图像整体亮度提高或降低。

\[Y(x, y) = I(x, y) ^ \gamma \]

通常我们取\(\gamma = 0.5\)

所以上述的灰度图进行Gamma校正后,图片在亮度上明显发生了变化——亮度提升:

img = np.power(np.float32(gray) / 255.0, 1/2)

计算每个像素点的梯度值

先分别计算每个像素点的横坐标和纵坐标方向上的梯度值,并据此结果计算出最终梯度方向值。

求导操作不仅能够捕获轮廓,人影和一些纹理信息,还能进一步弱化光照的影响。

图像中像素点(x,y)的梯度为:

\[G_x(x, y) = H(x+1, y) - H(x-1, y) \\ G_y(x, y) = H(x, y + 1) - H(x, y-1) \]

式中的\(G_x(x,y), G_y(x, y)\)分别为图像在水平方向和垂直方向上的梯度值,而\(H(x,y)\)表示的是位置\((x, y)\)上的像素值。

在这里我们可以使用Sobel算子来计算图像在水平方向和垂直方向上的偏导数近似值,滤波核处理图像的速度会加快。下图为Sobel算子的示例。

⚠️: 之前问过助教,据说Sobel算子对X轴、Y轴实际上是不做要求的,而是注重于计算水平或者是垂直方向的梯度值。所以左侧的\(3 \times 3\)矩阵,是计算水平方向的梯度(理论上可以理解为是X轴),而右侧的计算的是垂直方向上的梯度。

现计算水平方向偏导数的近似值:

将Sobel算子与原始图像img进行卷积操作,可以计算水平方向上的像素值变化情况。例如,当Sobel算子的大小为\(3 \times 3\)时,水平方向偏导数\(G_x\)的计算方式为

\[G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} \times img \]

上式中,img是原始图像,假设其中有9个像素点,如下图所示:

如果要计算像素点P5的水平方向偏导数\(P5_x\),则需要利用Sobel算子及P5邻域点,所使用的公式为

\[P5_x = (P3 - P1) + 2 \times (P6 - P4) + (P9 - P7) \]

即用像素点\(P_5\)右侧像素点的像素值减去其左侧像素点的像素值,这符合公式(4)的计算。

其中,中间像素点P4和P6距离像素点P5比较近,因此它俩占的权重会更高一些,值为2,其他权重差值为1。

那么,我们使用cv.Sobel()方法求得水平方向的梯度:

grad_x = cv.Sobel(img, cv.CV_32F, 1, 0, ksize = 3)
# cv.CV_32F 可以防止因为相减后的值为负数造成的影响
# 1, 0 表示计算的是水平方向梯度; 0, 1表示垂直
# ksize是Sobel核的大小

关于该方法的具体参数含义将另行介绍,此处不做过多说明。

得到图像如下:

那么同样的,计算垂直方向上的梯度时,可以得到结果:

从上面的图像中可以看到x轴方向的梯度主要凸显了垂直方向的线条,y轴方向的梯度凸显了水平方向的梯度,梯度幅值凸显了像素值有剧烈变化的地方。

(⚠️:图像的原点是图片的左上角,x轴是水平的,y轴是垂直的)

图像的梯度去掉了很多不必要的信息(比如不变的背景色),加重了轮廓。换句话说,你可以从梯度的图像中还是可以轻而易举的发现有个人。

最后将两方向梯度进行平方和计算后再开方,得到最后梯度结果,并另外计算其梯度方向,公式如下:

\[G(x,y) = \sqrt{G_x(x, y)^2 + G_y(x, y)^2} \\ \alpha(x, y) = arctan(\frac{G_y(x,y)}{G_x(x,y)}) \]

然后使用cv.cartToPolar()来计算合梯度的幅值和方向(角度)。

# 计算合梯度的幅值和方向
grad_xy, angle = cv.cartToPolar(grad_x, grad_y, angleInDegrees=True)
cv.imshow(grad_xy)

可以发现方向梯度结合后,得到图像为:

也可以使用其他梯度算子来替换Sobel算子,比如大部分博客写的是:水平边缘算子\([-1, 0, 1]\) ;垂直边缘算子\([-1, 0, 1]^T\)

为每个单元格构建梯度方向直方图

首先明白几个在HOG特征求取过程中需要用到的单位,根据下图具体解释:

  • 我们使用一个滑动窗口(window)按照从左到右、从上至下的顺序对给定的待检测图片(img)进行处理。而window需要包含你要检测的整个目标的一个窗口。

    假如现在要检测行人,你就需要用这个window把行人给框住。因为window是整个HOG计算的最顶层,也就是说我们每次计算HOG特征,计算的并不是整幅图像的,而是一个window范围内的HOG特征。

    其实window可以是任意尺寸的(arbitrary的),这里使用官方推荐的 64 x 128。

  • 设定block是window中的一个滑框。

    window的长和宽最好是block长宽的整数倍, 这里依旧使用官方推荐的16 x 16。

  • 设定最小单位cell,它是block的下一级了,其中cell是不可滑动的。

    cell的单位依旧是官方推荐的8 x 8。

所以,在一个滑动窗口中,最小单位是Cell,4个Cell组成了一个Block。

  • 设定直方图的区间数为9,将0-180度分成9等份,称为9个bins,分别是0,20,40...160。

    ⚠️ :角度的范围介于0到180度之间,而不是0到360度, 这被称为“无符号”梯度,因为两个完全相反的方向被认为是相同的。

img

那么,我们再对一张图像阐述详细处理过程吧:

以预先设定的Cell和Block来划分window,那么整个window最后就被划分为\(8 \times 16\)\(8 \times 8\)的Cell单元,并为每个Cell计算梯度直方图。在计算Cell的梯度过程中,总共包含了\(8 \times 8 \times 2 = 128\)个值,因为每个像素包括梯度的大小和方向。

那么,我们先来看看每个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里面去。

把这8*8的cell里面所有的像素点都分别加到这9个bin里面去,就构建了一个9-bin的直方图,上面的网格对应的直方图如下:

疑问:为什么我们要分Cell呢?

答:这是因为如果对一整张梯度图逐像素计算,其中的有效特征是非常稀疏的,不但运算量大,而且会受到一些噪声干扰。于是我们就使用局部特征描述符来表示一个更紧凑的特征,计算这种局部cell上的梯度直方图更具鲁棒性。

Block归一化

上面的步骤中,我们创建了基于图片的梯度直方图,但是一个图片的梯度对于整张图片的光线会很敏感。如果你把所有的像素点都除以2,那么梯度的幅值也会减半,那么直方图里面的值也会减半,所以这样并不能消除光线的影响。

所以理想情况下,我们希望我们的特征描述子可以和光线变换无关,所以我们就想让我们的直方图归一化从而不受光线变化影响,能够进一步地对光照、阴影和边缘进行压缩。

我们知道,block是由多个cell所组成的,典型的组合方式是 2x2 个 cell 组成成一个 block,每个 cell 上面都有一个 9 维的表示直方图大小的向量,那么一个block的拼接向量上就有 2x2x9 = 36维的向量。

疑问:为什么我们要分Block呢?

答:这是因为,虽然我们已经为图像的8×8单元创建了HOG特征,但是图像的梯度对整体光照很敏感。这意味着对于特定的图像,图像的某些部分与其他部分相比会非常明亮。

⚠️ 由于图像中光照情况和背景的变化多样,梯度值的变化范围会比较大,因而良好的特征标准化对于检测率的提高相当重要。

⚠️ 相邻block之间是有重叠的,这样有效的利用了相邻像素信息,对检测结果有很大的帮助。

规范化的方法有多种可选:

先考虑对向量用L2归一化的步骤是:

\[V = [128, 64, 32] \\ [(128^2) + (64^2) + (32^2) ]^{0.5}=146.64 \]

再把\(V\)中每一个元素除以146.64得到\([0.87,0.43,0.22]\),得到最后结果。

所以,经过上述步骤,我们成功将4个Cell的直方图进行拼接,形成了一个Block归一化后的直方图。

计算HOG特征向量

最后一步就是将检测窗口中所有重叠的块进行HOG特征的收集,那么为了计算这整个window的特征向量,需要把36*1的向量全部合并组成一个巨大的向量。向量的大小可以这么计算:

  1. 我们有多少个\(16 \times 16\) 的块?水平7个,垂直15个,总共有\(7 \times 15=105\)次移动。
  2. 每个\(16 \times 16\) 的块代表了\(36 \times 1\) 的向量。所以把他们放在一起也就是$ 36 \times 105=3780$维向量。

再将最终的特征向量供分类器使用。

openCV实现与可视化

# coding:utf-8
"""
@Author  : sonata
@time    : 2020-07-04 11:34
@File    : HOG.py
@Software: PyCharm
@Role    : task04 HOG特征描述算子
"""

import cv2 as cv
import numpy as np

imgpath = '../img/cv_5.jpeg'

img = cv.imread(imgpath)
hog = cv.HOGDescriptor()
hog.setSVMDetector(cv.HOGDescriptor_getDefaultPeopleDetector())

(rects, weights) = hog.detectMultiScale(img, winStride=(2, 4), padding=(8, 8), scale=1.2, useMeanshiftGrouping=False)
for (x, y, w, h) in rects:
        cv.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)

cv.imwrite("image", img)
cv.imshow('image', img)
cv.waitKey(0)
cv.destroyAllWindows()

可以得到最后结果如下图所示:

学习总结

这是我第二次参加Datawhale的组队学习活动,这次任务结束后,CV下的学习也就彻底结束了。

这16天的学习让我感到充实,并且又探索出了新的学习方法。这次加入的队伍也依旧优秀,每天能在群里唠唠嗑,感觉真的很好~

希望16期的组队活动能够如约而至,而到了那时,我能比现在更进步一点点!

参考资料

  1. https://blog.csdn.net/hujingshuang/article/details/47337707
  2. https://blog.csdn.net/coming_is_winter/article/details/72850511
  3. https://blog.csdn.net/Pierce_KK/article/details/89501308
  4. https://blog.csdn.net/zhazhiqiang/article/details/21047207
  5. https://www.cnblogs.com/tornadomeet/archive/2012/08/15/2640754.html
  6. https://blog.csdn.net/zouxy09/article/details/7929348
  7. https://blog.csdn.net/krais_wk/article/details/81119237
  8. https://www.jianshu.com/p/395f0582c5f7
posted @ 2020-07-05 14:53  司念  阅读(1283)  评论(0编辑  收藏  举报