本文用80行代碼的Python實現了HOG算法,代碼在Github Hog-feature,雖然OpenCV有實現好的Hog描述器算法,但是本文目的是完全理解HOG特徵提取的具體方法和實現原理,以及檢驗相關參數對實驗結果的影響,提升檢測到的特徵的性能以及優化代碼的運行速度。
此文由作者和ZP大兄弟共同完成
1. 方法簡介
方向梯度直方圖(Histogram of Oriented Gradient, HOG)特徵是一種在計算機視覺和圖像處理中用來進行物體檢測的描述子。通過計算和統計局部區域的梯度方向直方圖來構成特徵。Hog特徵結合SVM分類器已經被廣泛應用於圖像識別中,尤其在行人檢測中獲得了極大的成功。現如今如今雖然有很多行人檢測算法不斷提出,但基本都是以HOG+SVM的思路爲主。
主要思想:在一幅圖像中,局部目標的表象和形狀(appearance and shape)能夠被梯度或邊緣的方向密度分佈很好地描述。其本質是梯度的統計信息,而梯度主要存在於邊緣所在的地方。
實現過程:簡單來說,首先需要將圖像分成小的連通區域,稱之爲細胞單元。然後採集細胞單元中各像素點的梯度或邊緣的方向直方圖。最後把這些直方圖組合起來就可以構成特徵描述器。
算法優點:與其他的特徵描述方法相比,HOG有較多優點。由於HOG是在圖像的局部方格單元上操作,所以它對圖像幾何的和光學的形變都能保持很好的不變性,這兩種形變只會出現在更大的空間領域上。其次,在粗的空域抽樣、精細的方向抽樣以及較強的局部光學歸一化等條件下,只要行人大體上能夠保持直立的姿勢,可以容許行人有一些細微的肢體動作,這些細微的動作可以被忽略而不影響檢測效果。因此HOG特徵是特別適合於做圖像中的人體檢測的。
2. HOG流程
HOG特徵提取算法的整個實現過程大致如下:
- 讀入所需要的檢測目標即輸入的image
- 將圖像進行灰度化(將輸入的彩色的圖像的r,g,b值通過特定公式轉換爲灰度值)
- 採用Gamma校正法對輸入圖像進行顏色空間的標準化(歸一化)
- 計算圖像每個像素的梯度(包括大小和方向),捕獲輪廓信息
- 統計每個cell的梯度直方圖(不同梯度的個數),形成每個cell的descriptor
- 將每幾個cell組成一個block(以3*3爲例),一個block內所有cell的特徵串聯起來得到該block的HOG特徵descriptor
- 將圖像image內所有block的HOG特徵descriptor串聯起來得到該image(檢測目標)的HOG特徵descriptor,這就是最終分類的特徵向量
HOG參數設置是:2*2細胞/區間、8*8像素/細胞、8個直方圖通道,步長爲1。
特徵提取流程圖如下:
3. Python實現
3.1 數據準備
讀入彩色圖像,並轉換爲灰度值圖像, 獲得圖像的寬和高。採用Gamma校正法對輸入圖像進行顏色空間的標準化(歸一化),目的是調節圖像的對比度,降低圖像局部的陰影和光照變化所造成的影響,同時可以抑制噪音。採用的gamma值爲0.5。
#first part
import cv2
import numpy as np
img = cv2.imread('person_037.png', cv2.IMREAD_GRAYSCALE)
# cv2.imshow('Image', img)
# cv2.imwrite("Image-test.jpg", img)
# cv2.waitKey(0)
img = np.sqrt(img / float(np.max(img)))
# cv2.imshow('Image', img)
# cv2.imwrite("Image-test2.jpg", img)
# cv2.waitKey(0)
3.2 計算每個像素的梯度
計算圖像橫座標和縱座標方向的梯度,並據此計算每個像素位置的梯度方向值;求導操作不僅能夠捕獲輪廓,人影和一些紋理信息,還能進一步弱化光照的影響。在求出輸入圖像中像素點(x,y)處的水平方向梯度、垂直方向梯度和像素值,從而求出梯度幅值和方向。
常用的方法是:首先用[-1,0,1]梯度算子對原圖像做卷積運算,得到x方向(水平方向,以向右爲正方向)的梯度分量gradscalx,然後用[1,0,-1]T梯度算子對原圖像做卷積運算,得到y方向(豎直方向,以向上爲正方向)的梯度分量gradscaly。然後再用以上公式計算該像素點的梯度大小和方向。
# second part
height, width = img.shape
gradient_values_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
gradient_values_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)
gradient_magnitude = cv2.addWeighted(gradient_values_x, 0.5, gradient_values_y, 0.5, 0)
gradient_angle = cv2.phase(gradient_values_x, gradient_values_y, angleInDegrees=True)
print gradient_magnitude.shape, gradient_angle.shape
Out
(640, 480) (640, 480)
3.3 爲每個細胞單元構建梯度方向直方圖
我們將圖像分成若干個“單元格cell”,默認我們將cell設爲8*8個像素。假設我們採用8個bin的直方圖來統計這6*6個像素的梯度信息。也就是將cell的梯度方向360度分成8個方向塊,例如:如果這個像素的梯度方向是0-22.5度,直方圖第1個bin的計數就加一,這樣,對cell內每個像素用梯度方向在直方圖中進行加權投影(映射到固定的角度範圍),就可以得到這個cell的梯度方向直方圖了,就是該cell對應的8維特徵向量而梯度大小作爲投影的權值。
# third part
cell_size = 8
bin_size = 8
angle_unit = 360 / bin_size
gradient_magnitude = abs(gradient_magnitude)
cell_gradient_vector = np.zeros((height / cell_size, width / cell_size, bin_size))
print cell_gradient_vector.shape
def cell_gradient(cell_magnitude, cell_angle):
orientation_centers = [0] * bin_size
for k in range(cell_magnitude.shape[0]):
for l in range(cell_magnitude.shape[1]):
gradient_strength = cell_magnitude[k][l]
gradient_angle = cell_angle[k][l]
min_angle = int(gradient_angle / angle_unit)%8
max_angle = (min_angle + 1) % bin_size
mod = gradient_angle % angle_unit
orientation_centers[min_angle] += (gradient_strength * (1 - (mod / angle_unit)))
orientation_centers[max_angle] += (gradient_strength * (mod / angle_unit))
return orientation_centers
for i in range(cell_gradient_vector.shape[0]):
for j in range(cell_gradient_vector.shape[1]):
cell_magnitude = gradient_magnitude[i * cell_size:(i + 1) * cell_size,
j * cell_size:(j + 1) * cell_size]
cell_angle = gradient_angle[i * cell_size:(i + 1) * cell_size,
j * cell_size:(j + 1) * cell_size]
print cell_angle.max()
cell_gradient_vector[i][j] = cell_gradient(cell_magnitude, cell_angle)
3.4 可視化Cell梯度直方圖
將得到的每個cell的梯度方向直方圖繪出,得到特徵圖
# fourth part
import math
import matplotlib.pyplot as plt
hog_image= np.zeros([height, width])
cell_gradient = cell_gradient_vector
cell_width = cell_size / 2
max_mag = np.array(cell_gradient).max()
for x in range(cell_gradient.shape[0]):
for y in range(cell_gradient.shape[1]):
cell_grad = cell_gradient[x][y]
cell_grad /= max_mag
angle = 0
angle_gap = angle_unit
for magnitude in cell_grad:
angle_radian = math.radians(angle)
x1 = int(x * cell_size + magnitude * cell_width * math.cos(angle_radian))
y1 = int(y * cell_size + magnitude * cell_width * math.sin(angle_radian))
x2 = int(x * cell_size - magnitude * cell_width * math.cos(angle_radian))
y2 = int(y * cell_size - magnitude * cell_width * math.sin(angle_radian))
cv2.line(hog_image, (y1, x1), (y2, x2), int(255 * math.sqrt(magnitude)))
angle += angle_gap
plt.imshow(hog_image, cmap=plt.cm.gray)
plt.show()
out
3.5 統計Block的梯度信息
把細胞單元組合成大的塊(block),塊內歸一化梯度直方圖
由於局部光照的變化以及前景-背景對比度的變化,使得梯度強度的變化範圍非常大。這就需要對梯度強度做歸一化。歸一化能夠進一步地對光照、陰影和邊緣進行壓縮。
把各個細胞單元組合成大的、空間上連通的區間(blocks)。這樣,一個block內所有cell的特徵向量串聯起來便得到該block的HOG特徵。這些區間是互有重疊的,
本次實驗採用的是矩陣形區間,它可以有三個參數來表徵:每個區間中細胞單元的數目、每個細胞單元中像素點的數目、每個細胞的直方圖通道數目。
本次實驗中我們採用的參數設置是:2*2細胞/區間、8*8像素/細胞、8個直方圖通道,步長爲1。則一塊的特徵數爲2*2*8。
# fifth part
hog_vector = []
for i in range(cell_gradient_vector.shape[0] - 1):
for j in range(cell_gradient_vector.shape[1] - 1):
block_vector = []
block_vector.extend(cell_gradient_vector[i][j])
block_vector.extend(cell_gradient_vector[i][j + 1])
block_vector.extend(cell_gradient_vector[i + 1][j])
block_vector.extend(cell_gradient_vector[i + 1][j + 1])
mag = lambda vector: math.sqrt(sum(i ** 2 for i in vector))
magnitude = mag(block_vector)
if magnitude != 0:
normalize = lambda block_vector, magnitude: [element / magnitude for element in block_vector]
block_vector = normalize(block_vector, magnitude)
hog_vector.append(block_vector)
print np.array(hog_vector).shape
out
(4661, 32)
共有4661個block,每個block有32維的特徵
4. 80行代碼封裝
通過建類和封裝函數,最終代碼如下:
import cv2
import numpy as np
import math
import matplotlib.pyplot as plt
class Hog_descriptor():
def __init__(self, img, cell_size=16, bin_size=8):
self.img = img
self.img = np.sqrt(img / np.max(img))
self.img = img * 255
self.cell_size = cell_size
self.bin_size = bin_size
self.angle_unit = 360 / self.bin_size
assert type(self.bin_size) == int, "bin_size should be integer,"
assert type(self.cell_size) == int, "cell_size should be integer,"
assert type(self.angle_unit) == int, "bin_size should be divisible by 360"
def extract(self):
height, width = self.img.shape
gradient_magnitude, gradient_angle = self.global_gradient()
gradient_magnitude = abs(gradient_magnitude)
cell_gradient_vector = np.zeros((height / self.cell_size, width / self.cell_size, self.bin_size))
for i in range(cell_gradient_vector.shape[0]):
for j in range(cell_gradient_vector.shape[1]):
cell_magnitude = gradient_magnitude[i * self.cell_size:(i + 1) * self.cell_size,
j * self.cell_size:(j + 1) * self.cell_size]
cell_angle = gradient_angle[i * self.cell_size:(i + 1) * self.cell_size,
j * self.cell_size:(j + 1) * self.cell_size]
cell_gradient_vector[i][j] = self.cell_gradient(cell_magnitude, cell_angle)
hog_image = self.render_gradient(np.zeros([height, width]), cell_gradient_vector)
hog_vector = []
for i in range(cell_gradient_vector.shape[0] - 1):
for j in range(cell_gradient_vector.shape[1] - 1):
block_vector = []
block_vector.extend(cell_gradient_vector[i][j])
block_vector.extend(cell_gradient_vector[i][j + 1])
block_vector.extend(cell_gradient_vector[i + 1][j])
block_vector.extend(cell_gradient_vector[i + 1][j + 1])
mag = lambda vector: math.sqrt(sum(i ** 2 for i in vector))
magnitude = mag(block_vector)
if magnitude != 0:
normalize = lambda block_vector, magnitude: [element / magnitude for element in block_vector]
block_vector = normalize(block_vector, magnitude)
hog_vector.append(block_vector)
return hog_vector, hog_image
def global_gradient(self):
gradient_values_x = cv2.Sobel(self.img, cv2.CV_64F, 1, 0, ksize=5)
gradient_values_y = cv2.Sobel(self.img, cv2.CV_64F, 0, 1, ksize=5)
gradient_magnitude = cv2.addWeighted(gradient_values_x, 0.5, gradient_values_y, 0.5, 0)
gradient_angle = cv2.phase(gradient_values_x, gradient_values_y, angleInDegrees=True)
return gradient_magnitude, gradient_angle
def cell_gradient(self, cell_magnitude, cell_angle):
orientation_centers = [0] * self.bin_size
for i in range(cell_magnitude.shape[0]):
for j in range(cell_magnitude.shape[1]):
gradient_strength = cell_magnitude[i][j]
gradient_angle = cell_angle[i][j]
min_angle, max_angle, mod = self.get_closest_bins(gradient_angle)
orientation_centers[min_angle] += (gradient_strength * (1 - (mod / self.angle_unit)))
orientation_centers[max_angle] += (gradient_strength * (mod / self.angle_unit))
return orientation_centers
def get_closest_bins(self, gradient_angle):
idx = int(gradient_angle / self.angle_unit)
mod = gradient_angle % self.angle_unit
return idx, (idx + 1) % self.bin_size, mod
def render_gradient(self, image, cell_gradient):
cell_width = self.cell_size / 2
max_mag = np.array(cell_gradient).max()
for x in range(cell_gradient.shape[0]):
for y in range(cell_gradient.shape[1]):
cell_grad = cell_gradient[x][y]
cell_grad /= max_mag
angle = 0
angle_gap = self.angle_unit
for magnitude in cell_grad:
angle_radian = math.radians(angle)
x1 = int(x * self.cell_size + magnitude * cell_width * math.cos(angle_radian))
y1 = int(y * self.cell_size + magnitude * cell_width * math.sin(angle_radian))
x2 = int(x * self.cell_size - magnitude * cell_width * math.cos(angle_radian))
y2 = int(y * self.cell_size - magnitude * cell_width * math.sin(angle_radian))
cv2.line(image, (y1, x1), (y2, x2), int(255 * math.sqrt(magnitude)))
angle += angle_gap
return image
img = cv2.imread('person_037.png', cv2.IMREAD_GRAYSCALE)
hog = Hog_descriptor(img, cell_size=8, bin_size=8)
vector, image = hog.extract()
print np.array(vector).shape
plt.imshow(image, cmap=plt.cm.gray)
plt.show()
5. 結果分析
本文最終單幅圖像HOG特徵的求取平均時間爲1.8秒,相比最初版本所需的5.4秒有個長足的改進。
相比初期的版本hog梯度特徵圖
可見最終版本中
能夠更加有效的區分梯度顯示邊緣。這是因爲對各個像素的梯度進行了全局歸一化,並且在描繪梯度方向時加入了梯度量級的非線性映射,使得梯度方向產生明顯的深淺和長度差異,更易於區分邊緣,凸顯明顯的梯度變化。
此外在輸入圖像時,採用Gamma校正對輸入圖像進行顏色空間的標準化能夠抑制噪聲,使得產生的邊緣更加明顯,清晰。
此外改變cell的大小和直方圖方向通道的效果如下:
cell_size = 10 即 16*16個像素
可以看出增大cell的size得到的特徵圖更加註重基本輪廓和邊緣,而忽略一些細節,某種程度上降低了噪聲。
當通道數目爲16個方向
梯度特徵圖像的細節變得更加明顯,方向更多。
6. 在人臉識別,物體檢測中的應用
在提取完圖像的HOG特徵之後,可以使用SVM進行分類訓練,能完成行人檢測等任務。
未來工作可參考Github的行人檢測項目https://github.com/icsfy/Pedestrian_Detection