前言
- HOG(Histogram of Oriented Gradients)最早由是Dadal博士在CVPR 2005年的論文中提出碉哑,用以解決道路行人的識(shí)別問(wèn)題。后來(lái)逐漸成為計(jì)算機(jī)視覺痊银、模式識(shí)別領(lǐng)域很常用的一種描述圖像局部紋理的特征奠滑。顧名思義鸟雏,就是先計(jì)算圖片某一區(qū)域中不同方向上梯度的值,然后進(jìn)行累積惧财,得到直方圖巡扇,再將直方圖進(jìn)行一定的處理得到不同維數(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.
- 在其博士論文中厅翔,有更詳細(xì)的描述及拓展。在使用過(guò)HOG之后帘靡,便會(huì)對(duì)它在識(shí)別上產(chǎn)生的提升作用嘆為觀止知给。拜讀這篇博士論文的過(guò)程之中,也讓人收獲到了一些科研過(guò)程中有益的思路。
- Dalal N. Finding people in images and videos[D]. Institut National Polytechnique de Grenoble-INPG, 2006.
- python的skimage庫(kù)當(dāng)中已經(jīng)有了對(duì)HOG較為完善的實(shí)現(xiàn)了涩赢,那么就讓我們立足HOG的skimage實(shí)現(xiàn)戈次,將其與論文步驟一一對(duì)應(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)
圖像標(biāo)準(zhǔn)化
在這一步怯邪,我們的主要目的是為了預(yù)處理圖像,減少光照等帶來(lái)的影響花墩。
- 此處我們選擇值小于1便會(huì)使圖像整體灰度變大悬秉,如果我們選擇值大于1便會(huì)使圖像整體灰度變小”ⅲ灰度的大小某種程度上決定了圖片的亮暗和泌,灰度越小,圖片越發(fā)昏暗祠肥,反之亦然武氓。
if transform_sqrt:
image = np.sqrt(image)
- skimage在此處的實(shí)現(xiàn)極為簡(jiǎn)潔,直接使用了開方來(lái)對(duì)圖片進(jìn)行處理仇箱。
圖像平滑
- 去除灰度圖像的噪點(diǎn)县恕,一般選取離散高斯平滑模板進(jìn)行平滑,高斯函數(shù)在不同平滑的尺度下進(jìn)行對(duì)灰度圖像進(jìn)行平滑操作剂桥。Dalal的實(shí)驗(yàn)中moving from σ=0 to σ=2 reduces the recall rate from 89% to 80% at FPPW忠烛,反應(yīng)給出做了圖像平滑之后HOG效果反而變差。我們?cè)趯?shí)驗(yàn)過(guò)程中也得出了相似的結(jié)論权逗,很容易讓人想到美尸,HOG是基于圖像邊緣梯度的算法,但平滑過(guò)程有可能破壞邊緣的梯度信息旬迹,從而影響HOG的效果火惊。
梯度計(jì)算
- 首先是像素點(diǎn)梯度的計(jì)算,我們使用來(lái)表示圖像上(x, y)像素點(diǎn)的像素值奔垦。那么每個(gè)像素點(diǎn)的水平和豎直方向的梯度(Gradient)可以分別被表示為:
- 那么顯然,作為兩個(gè)梯度矢量尸疆,它們的幅度值和角度也可以分別表示為:
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的實(shí)現(xiàn)中我們可以看到椿猎,這里是先將圖片以float的形式讀入,防止出現(xiàn)uint(小) - uint(大) 越界出現(xiàn)正數(shù)的情況寿弱。
- 接著是對(duì)于多信道的一個(gè)判斷犯眠,如果圖像是多信道的話,我們會(huì)分信道進(jìn)行梯度統(tǒng)計(jì)症革,如果是灰度圖片筐咧,會(huì)直接只進(jìn)行一次梯度統(tǒng)計(jì)處理。梯度統(tǒng)計(jì)的代碼如下:
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
- 接著,我們要將這些像素點(diǎn)整合為一個(gè)個(gè)的cell量蕊,選取的方式有正方形取點(diǎn)R-HOG铺罢,圓形取點(diǎn)C-HOG,和中心切割型取點(diǎn)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韭赘,但效果卻基本與圓形取點(diǎn)C-HOG相吻合,所以我們通常選用R-HOG和C-HOG二者之一势就。此處我們選擇R-HOG這一常用的HOG結(jié)構(gòu)泉瞻。
下一步便是pixels per cell參數(shù)的選取,此處我們?nèi)绻x擇(4x4)作為參數(shù)苞冯,那么就代表由4x4個(gè)像素構(gòu)成一個(gè)cell袖牙,這時(shí)要對(duì)每一個(gè)cell當(dāng)中的各個(gè)像素進(jìn)行梯度向量的統(tǒng)計(jì),此處我們選擇使用直方圖來(lái)進(jìn)行統(tǒng)計(jì)舅锄,對(duì)應(yīng)的橫軸坐標(biāo)就是向量的角度贼陶。這里簡(jiǎn)單起見會(huì)考慮用若干個(gè)區(qū)間來(lái)覆蓋向量角度,Dadal論文當(dāng)中采用的是9份巧娱,skimage官方的demo中采用的是8份碉怔,這里我們不妨選取9份作為例子。
這樣一來(lái)從0°到180°(如果是0°到360°則需考慮方向的正負(fù))即可以分為20°的每份來(lái)作為梯度向量統(tǒng)計(jì)直方圖的橫軸禁添,對(duì)應(yīng)的縱軸方向則填充像素點(diǎn)對(duì)應(yīng)的梯度的幅度值撮胧。
同理,我們選擇cell per block參數(shù)老翘,例如也選取(4x4)芹啥。那么對(duì)于每一個(gè)block,都由對(duì)應(yīng)數(shù)量的cell合成铺峭。此時(shí)我們得到的塊特征向量長(zhǎng)度應(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是一個(gè)bultins的函數(shù),我們無(wú)法看到它內(nèi)部的實(shí)現(xiàn)卫键,但我們猜測(cè)應(yīng)該是通過(guò)移動(dòng)掃描窗口來(lái)實(shí)現(xiàn)直方圖的cell統(tǒng)計(jì)傀履。為了保證效率,采取了c實(shí)現(xiàn)莉炉。
- 這里還有一個(gè)visualize的實(shí)現(xiàn)钓账,是在之前詢問(wèn)我們是否返回一個(gè)hog的可視圖。如果選擇是是絮宁,這里就會(huì)根據(jù)之前統(tǒng)計(jì)值引入draw作圖梆暮。
歸一化
- 使局部光照對(duì)比度歸一化,壓縮光照绍昂,明暗啦粹,邊緣對(duì)比度對(duì)圖片帶來(lái)的影響偿荷。這一步是基于block進(jìn)行的,也就是說(shuō)每一個(gè)cell唠椭,可能同時(shí)屬于不同的block跳纳,那么它就會(huì)在不同的block被分別均一化。
- 設(shè)為沒有歸一化的feature vector泪蔫,此處的均一化棒旗,我們通常有以下四種方式可選:
- :加一個(gè)極小的以防止分母為0
- :在的基礎(chǔ)上限制的最大值為0.2,再歸一化撩荣。
- 這里的塊均一化方法同時(shí)支持了我們上面所描述的四種方法铣揉。
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
- 再來(lái)看一下具體的實(shí)現(xiàn)過(guò)程,n_blocks_row 對(duì)應(yīng)的是block的行數(shù)餐曹,需要對(duì)應(yīng)的cell在行上平均分布開的數(shù)目減去對(duì)應(yīng)的cells_per_block的行數(shù)再加上1逛拱。列的計(jì)算依然。由此台猴,我們可以推斷出對(duì)應(yīng)的特征向量維數(shù)應(yīng)該是之前每一個(gè)block對(duì)應(yīng)的維數(shù)4x4x9再乘上對(duì)應(yīng)的block數(shù)目(8-4+1)x(8-4+1)朽合,最終等于3600維。選取了不同的參數(shù)也可以根據(jù)此判據(jù)來(lái)進(jìn)行計(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)