前言
- HOG(Histogram of Oriented Gradients)最早由是Dadal博士在CVPR 2005年的论文中提出,用以解决道路行人的识别问题。后来逐渐成为计算机视觉、模式识别领域很常用的一种描述图像局部纹理的特征。顾名思义,就是先计算图片某一区域中不同方向上梯度的值,然后进行累积,得到直方图,再将直方图进行一定的处理得到不同维数的特征。之后即可将特征可以输入到分类器里面了。
- 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.
- 在其博士论文中,有更详细的描述及拓展。在使用过HOG之后,便会对它在识别上产生的提升作用叹为观止。拜读这篇博士论文的过程之中,也让人收获到了一些科研过程中有益的思路。
- Dalal N. Finding people in images and videos[D]. Institut National Polytechnique de Grenoble-INPG, 2006.
- python的skimage库当中已经有了对HOG较为完善的实现了,那么就让我们立足HOG的skimage实现,将其与论文步骤一一对应,深入探究一下此算法。
hog(image, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(3, 3),
block_norm='L2-Hys', visualize=False, transform_sqrt=False,
feature_vector=True, multichannel=None)
图像标准化
在这一步,我们的主要目的是为了预处理图像,减少光照等带来的影响。
- 此处我们选择值小于1便会使图像整体灰度变大,如果我们选择值大于1便会使图像整体灰度变小。灰度的大小某种程度上决定了图片的亮暗,灰度越小,图片越发昏暗,反之亦然。
if transform_sqrt:
image = np.sqrt(image)
- skimage在此处的实现极为简洁,直接使用了开方来对图片进行处理。
图像平滑
- 去除灰度图像的噪点,一般选取离散高斯平滑模板进行平滑,高斯函数在不同平滑的尺度下进行对灰度图像进行平滑操作。Dalal的实验中moving from σ=0 to σ=2 reduces the recall rate from 89% to 80% at FPPW,反应给出做了图像平滑之后HOG效果反而变差。我们在实验过程中也得出了相似的结论,很容易让人想到,HOG是基于图像边缘梯度的算法,但平滑过程有可能破坏边缘的梯度信息,从而影响HOG的效果。
梯度计算
- 首先是像素点梯度的计算,我们使用来表示图像上(x, y)像素点的像素值。那么每个像素点的水平和竖直方向的梯度(Gradient)可以分别被表示为:
- 那么显然,作为两个梯度矢量,它们的幅度值和角度也可以分别表示为:
if image.dtype.kind == 'u':
# convert uint image to float
# to avoid problems with subtracting unsigned numbers
image = image.astype('float')
if multichannel:
g_row_by_ch = np.empty_like(image, dtype=np.double)
g_col_by_ch = np.empty_like(image, dtype=np.double)
g_magn = np.empty_like(image, dtype=np.double)
for idx_ch in range(image.shape[2]):
g_row_by_ch[:, :, idx_ch], g_col_by_ch[:, :, idx_ch] = \
_hog_channel_gradient(image[:, :, idx_ch])
g_magn[:, :, idx_ch] = np.hypot(g_row_by_ch[:, :, idx_ch],
g_col_by_ch[:, :, idx_ch])
# For each pixel select the channel with the highest gradient magnitude
idcs_max = g_magn.argmax(axis=2)
rr, cc = np.meshgrid(np.arange(image.shape[0]),
np.arange(image.shape[1]),
indexing='ij',
sparse=True)
g_row = g_row_by_ch[rr, cc, idcs_max]
g_col = g_col_by_ch[rr, cc, idcs_max]
else:
g_row, g_col = _hog_channel_gradient(image)
- 从HOG的实现中我们可以看到,这里是先将图片以float的形式读入,防止出现uint(小) - uint(大) 越界出现正数的情况。
- 接着是对于多信道的一个判断,如果图像是多信道的话,我们会分信道进行梯度统计,如果是灰度图片,会直接只进行一次梯度统计处理。梯度统计的代码如下:
def _hog_channel_gradient(channel):
"""Compute unnormalized gradient image along `row` and `col` axes.
Parameters
----------
channel : (M, N) ndarray
Grayscale image or one of image channel.
Returns
-------
g_row, g_col : channel gradient along `row` and `col` axes correspondingly.
"""
g_row = np.empty(channel.shape, dtype=np.double)
g_row[0, :] = 0
g_row[-1, :] = 0
g_row[1:-1, :] = channel[2:, :] - channel[:-2, :]
g_col = np.empty(channel.shape, dtype=np.double)
g_col[:, 0] = 0
g_col[:, -1] = 0
g_col[:, 1:-1] = channel[:, 2:] - channel[:, :-2]
return g_row, g_col
- 接着,我们要将这些像素点整合为一个个的cell,选取的方式有正方形取点R-HOG,圆形取点C-HOG,和中心切割型取点Single centre C-HOG,而Dadel的论文指出:
We evaluated two variants of the C-HOG geometry, ones with a single circular central cell (similar to the GLOH feature), and ones whose cen-tralcellis divided into angular sectors as in shape contexts.We present results only for the circular-centrevariants, as these have fewer spatial cells than the divided centre ones and give the same per-formance in practice.
-
由于中心切割型要消耗更多的4cell,但效果却基本与圆形取点C-HOG相吻合,所以我们通常选用R-HOG和C-HOG二者之一。此处我们选择R-HOG这一常用的HOG结构。
-
下一步便是pixels per cell参数的选取,此处我们如果选择(4x4)作为参数,那么就代表由4x4个像素构成一个cell,这时要对每一个cell当中的各个像素进行梯度向量的统计,此处我们选择使用直方图来进行统计,对应的横轴坐标就是向量的角度。这里简单起见会考虑用若干个区间来覆盖向量角度,Dadal论文当中采用的是9份,skimage官方的demo中采用的是8份,这里我们不妨选取9份作为例子。
-
这样一来从0°到180°(如果是0°到360°则需考虑方向的正负)即可以分为20°的每份来作为梯度向量统计直方图的横轴,对应的纵轴方向则填充像素点对应的梯度的幅度值。
-
同理,我们选择cell per block参数,例如也选取(4x4)。那么对于每一个block,都由对应数量的cell合成。此时我们得到的块特征向量长度应该是4x4x9
s_row, s_col = image.shape[:2]
c_row, c_col = pixels_per_cell
b_row, b_col = cells_per_block
n_cells_row = int(s_row // c_row) # number of cells along row-axis
n_cells_col = int(s_col // c_col) # number of cells along col-axis
# compute orientations integral images
orientation_histogram = np.zeros((n_cells_row, n_cells_col, orientations))
_hoghistogram.hog_histograms(g_col, g_row, c_col, c_row, s_col, s_row,
n_cells_col, n_cells_row,
orientations, orientation_histogram)
# now compute the histogram for each cell
hog_image = None
if visualize:
from .. import draw
radius = min(c_row, c_col) // 2 - 1
orientations_arr = np.arange(orientations)
# set dr_arr, dc_arr to correspond to midpoints of orientation bins
orientation_bin_midpoints = (
np.pi * (orientations_arr + .5) / orientations)
dr_arr = radius * np.sin(orientation_bin_midpoints)
dc_arr = radius * np.cos(orientation_bin_midpoints)
hog_image = np.zeros((s_row, s_col), dtype=float)
for r in range(n_cells_row):
for c in range(n_cells_col):
for o, dr, dc in zip(orientations_arr, dr_arr, dc_arr):
centre = tuple([r * c_row + c_row // 2,
c * c_col + c_col // 2])
rr, cc = draw.line(int(centre[0] - dc),
int(centre[1] + dr),
int(centre[0] + dc),
int(centre[1] - dr))
hog_image[rr, cc] += orientation_histogram[r, c, o]
- 这里我们可以看到hog_histograms是一个bultins的函数,我们无法看到它内部的实现,但我们猜测应该是通过移动扫描窗口来实现直方图的cell统计。为了保证效率,采取了c实现。
- 这里还有一个visualize的实现,是在之前询问我们是否返回一个hog的可视图。如果选择是是,这里就会根据之前统计值引入draw作图。
归一化
- 使局部光照对比度归一化,压缩光照,明暗,边缘对比度对图片带来的影响。这一步是基于block进行的,也就是说每一个cell,可能同时属于不同的block,那么它就会在不同的block被分别均一化。
- 设为没有归一化的feature vector,此处的均一化,我们通常有以下四种方式可选:
- :加一个极小的以防止分母为0
- :在的基础上限制的最大值为0.2,再归一化。
- 这里的块均一化方法同时支持了我们上面所描述的四种方法。
def _hog_normalize_block(block, method, eps=1e-5):
if method == 'L1':
out = block / (np.sum(np.abs(block)) + eps)
elif method == 'L1-sqrt':
out = np.sqrt(block / (np.sum(np.abs(block)) + eps))
elif method == 'L2':
out = block / np.sqrt(np.sum(block ** 2) + eps ** 2)
elif method == 'L2-Hys':
out = block / np.sqrt(np.sum(block ** 2) + eps ** 2)
out = np.minimum(out, 0.2)
out = out / np.sqrt(np.sum(out ** 2) + eps ** 2)
else:
raise ValueError('Selected block normalization method is invalid.')
return out
- 再来看一下具体的实现过程,n_blocks_row 对应的是block的行数,需要对应的cell在行上平均分布开的数目减去对应的cells_per_block的行数再加上1。列的计算依然。由此,我们可以推断出对应的特征向量维数应该是之前每一个block对应的维数4x4x9再乘上对应的block数目(8-4+1)x(8-4+1),最终等于3600维。选取了不同的参数也可以根据此判据来进行计算。
n_blocks_row = (n_cells_row - b_row) + 1
n_blocks_col = (n_cells_col - b_col) + 1
normalized_blocks = np.zeros((n_blocks_row, n_blocks_col,
b_row, b_col, orientations))
for r in range(n_blocks_row):
for c in range(n_blocks_col):
block = orientation_histogram[r:r + b_row, c:c + b_col, :]
normalized_blocks[r, c, :] = \
_hog_normalize_block(block, method=block_norm)
网友评论