前言
- HOG(Histogram of Oriented Gradients)最早由是Dadal博士在CVPR 2005年的論文中提出,用以解決道路行人的識別問題。后來逐漸成為計算機視覺、模式識別領(lǐng)域很常用的一種描述圖像局部紋理的特征。顧名思義,就是先計算圖片某一區(qū)域中不同方向上梯度的值,然后進行累積,得到直方圖,再將直方圖進行一定的處理得到不同維數(shù)的特征。之后即可將特征可以輸入到分類器里面了。
- 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之后,便會對它在識別上產(chǎn)生的提升作用嘆為觀止。拜讀這篇博士論文的過程之中,也讓人收獲到了一些科研過程中有益的思路。
- Dalal N. Finding people in images and videos[D]. Institut National Polytechnique de Grenoble-INPG, 2006.
- python的skimage庫當中已經(jīng)有了對HOG較為完善的實現(xiàn)了,那么就讓我們立足HOG的skimage實現(xiàn),將其與論文步驟一一對應(yīng),深入探究一下此算法。
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)
圖像標準化
在這一步,我們的主要目的是為了預(yù)處理圖像,減少光照等帶來的影響。
- 此處我們選擇
值小于1便會使圖像整體灰度變大,如果我們選擇
值大于1便會使圖像整體灰度變小?;叶鹊拇笮∧撤N程度上決定了圖片的亮暗,灰度越小,圖片越發(fā)昏暗,反之亦然。
if transform_sqrt:
image = np.sqrt(image)
- skimage在此處的實現(xiàn)極為簡潔,直接使用了開方來對圖片進行處理。
圖像平滑
- 去除灰度圖像的噪點,一般選取離散高斯平滑模板進行平滑,高斯函數(shù)在不同平滑的尺度下進行對灰度圖像進行平滑操作。Dalal的實驗中moving from σ=0 to σ=2 reduces the recall rate from 89% to 80% at FPPW,反應(yīng)給出做了圖像平滑之后HOG效果反而變差。我們在實驗過程中也得出了相似的結(jié)論,很容易讓人想到,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的實現(xiàn)中我們可以看到,這里是先將圖片以float的形式讀入,防止出現(xiàn)uint(小) - uint(大) 越界出現(xiàn)正數(shù)的情況。
- 接著是對于多信道的一個判斷,如果圖像是多信道的話,我們會分信道進行梯度統(tǒng)計,如果是灰度圖片,會直接只進行一次梯度統(tǒng)計處理。梯度統(tǒng)計的代碼如下:
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結(jié)構(gòu)。
下一步便是pixels per cell參數(shù)的選取,此處我們?nèi)绻x擇(4x4)作為參數(shù),那么就代表由4x4個像素構(gòu)成一個cell,這時要對每一個cell當中的各個像素進行梯度向量的統(tǒng)計,此處我們選擇使用直方圖來進行統(tǒng)計,對應(yīng)的橫軸坐標就是向量的角度。這里簡單起見會考慮用若干個區(qū)間來覆蓋向量角度,Dadal論文當中采用的是9份,skimage官方的demo中采用的是8份,這里我們不妨選取9份作為例子。

這樣一來從0°到180°(如果是0°到360°則需考慮方向的正負)即可以分為20°的每份來作為梯度向量統(tǒng)計直方圖的橫軸,對應(yīng)的縱軸方向則填充像素點對應(yīng)的梯度的幅度值。
同理,我們選擇cell per block參數(shù),例如也選取(4x4)。那么對于每一個block,都由對應(yīng)數(shù)量的cell合成。此時我們得到的塊特征向量長度應(yīng)該是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的函數(shù),我們無法看到它內(nèi)部的實現(xiàn),但我們猜測應(yīng)該是通過移動掃描窗口來實現(xiàn)直方圖的cell統(tǒng)計。為了保證效率,采取了c實現(xiàn)。
- 這里還有一個visualize的實現(xiàn),是在之前詢問我們是否返回一個hog的可視圖。如果選擇是是,這里就會根據(jù)之前統(tǒng)計值引入draw作圖。
歸一化
- 使局部光照對比度歸一化,壓縮光照,明暗,邊緣對比度對圖片帶來的影響。這一步是基于block進行的,也就是說每一個cell,可能同時屬于不同的block,那么它就會在不同的block被分別均一化。
- 設(shè)
為沒有歸一化的feature vector,此處的均一化,我們通常有以下四種方式可選:
-
:加一個極小的
以防止分母為0
-
:在
的基礎(chǔ)上限制
的最大值為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
- 再來看一下具體的實現(xiàn)過程,n_blocks_row 對應(yīng)的是block的行數(shù),需要對應(yīng)的cell在行上平均分布開的數(shù)目減去對應(yīng)的cells_per_block的行數(shù)再加上1。列的計算依然。由此,我們可以推斷出對應(yīng)的特征向量維數(shù)應(yīng)該是之前每一個block對應(yīng)的維數(shù)4x4x9再乘上對應(yīng)的block數(shù)目(8-4+1)x(8-4+1),最終等于3600維。選取了不同的參數(shù)也可以根據(jù)此判據(jù)來進行計算。
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)