80行Python實現-HOG梯度特徵提取

本文用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特徵提取算法的整個實現過程大致如下:

  1. 讀入所需要的檢測目標即輸入的image
  2. 將圖像進行灰度化(將輸入的彩色的圖像的r,g,b值通過特定公式轉換爲灰度值)
  3. 採用Gamma校正法對輸入圖像進行顏色空間的標準化(歸一化)
  4. 計算圖像每個像素的梯度(包括大小和方向),捕獲輪廓信息
  5. 統計每個cell的梯度直方圖(不同梯度的個數),形成每個cell的descriptor
  6. 將每幾個cell組成一個block(以3*3爲例),一個block內所有cell的特徵串聯起來得到該block的HOG特徵descriptor
  7. 將圖像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

發佈了46 篇原創文章 · 獲贊 132 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章