本文翻譯自 Histogram of Oriented Gradients"
在這篇文章中,我們將會學習 HOG (Histogram of Oriented Gradients,方向梯度直方圖)特征描述子 的詳細內容。
我們將學習 HOG 算法是如何實現的,以及在 OpenCv / MATLAB 或者其他工具里面如何計算特征子。
這篇文章是我正在寫的,關於 Image Recognition / 圖像識別 和 Object Detection / 目標檢測 系列文章中的一部分。
很多事情看起來困難又神秘,但是你一旦花時間去了解,揭開神秘面紗,你就會發現神奇之處。
如果你是一個初學者,覺得計算機視覺又難又神秘,請記住一句話:
問:如何吃掉一個大象?
答:一口一口吃
什么是 Feature Desciptor / 特征描述子
Feature Desciptor / 特征描述子 從圖像中提取有用信息,剔除無關信息;
典型的,特征描述子從將一張 寬度 * 高度 * 3 ( 通道數 ) 大小的圖像,提取出長度為 n 的 Feature Vector / 特征向量 或者 Feature Array / 特征矩陣;
比如 HOG 特征描述子會從一張 64 * 128 * 3 的圖像中提取出長度為 3780 的特征向量;
請記住, HOG 的特征描述子也可以計算其他尺寸,但是這篇文章中,我使用上述尺寸,以便你能夠輕松的理解概念。
這些概念聽起來都挺不錯,但是哪些是“有用的信息”,有些又是“無用的信息"?
定義“有用的信息”,我們需要知道有用的信息用來干什么的;
很明顯,通過特征向量用來瀏覽圖像是沒用的,但是在圖像識別或者目標檢測中,特征向量會變得很有用;
在一些圖像分類算法中比如 SVM,Support Vector Machine,支持向量機 中,用特征向量進行分類會達到很好的結果。
但是在分類任務中,哪些特征是有用的呢?
我們借助下面的例子來討論,比如現在我們想通過一個目標檢測器,可以檢測襯衫和大衣的紐扣;
一個紐扣是一個圓形(圖片中也有可能看起來像是橢圓),一般來說有幾個孔,用於縫到衣服上面;
你可以在紐扣的圖像上使用一個 Edge detector / 邊緣檢測器,可以輕松通過檢測邊緣來辨別它是不是一個紐扣;
這個例子中,邊緣信息是“有用的”而顏色信息是 ”無用的“;
除此之外,特征也需要有足夠特殊的地方。比如一個好的特征,應該能夠讓你辨別出紐扣和其他圓形的物體,比如硬幣和汽車輪胎。
如何計算 Histogram of Oriented Gradients / 方向梯度直方圖?
在這一節,我們會繼續深入學習如何計算 HOG 特征描述子。
步驟1:預加工
之前提到用於行人檢測的 HOG 特征描述子,是基於 64×128 大小的圖像。當然,圖像可能是任何尺寸的;
對於這些之后用於分析的圖像,唯一需要進行的處理是調整縱橫比圖像大小;
在我們的例子中,需要調整縱橫比為1:2,比如圖像可以被調整為 100×200, 128×256, 或者 1000×2000,但是不能是 101×205;
原始圖像大小是 720×475,我們截切出來 100×200 大小圖像用來計算 HOG 特征描述子,然后重新調整大小到 64×128;
現在我們就做完了計算 HOG 特征描述子准備工作。

Dalal 和 Triggs 的論文也提到了 Gamma Correction / 伽馬校正 作為預處理步驟,但性能提升很小,因此我們選擇預處理中跳過這一步。
步驟 2 :計算梯度圖像
為了計算 HOG 特征描述子,我們第一步需要計算水平和垂直方向的梯度。我們通過下面的 Kernel / 核 來處理圖像,很容易計算出梯度的直方圖。

我們可以使用核大小為 1 的 OpenCv 的 Sobel 算子:
1 // C++ gradient calculation.
2 // Read image
3 Mat img = imread("bolt.png"); 4 img.convertTo(img, CV_32F, 1/255.0); 5 6 // Calculate gradients gx, gy
7 Mat gx, gy; 8 Sobel(img, gx, CV_32F, 1, 0, 1); 9 Sobel(img, gy, CV_32F, 0, 1, 1);
1 # Python gradient calculation
2 3 # Read image
4 im = cv2.imread('bolt.png') 5 im = np.float32(im) / 255.0
6 7 # Calculate gradient
8 gx = cv2.Sobel(img, cv2.CV_32F, 1, 0, ksize=1) 9 gy = cv2.Sobel(img, cv2.CV_32F, 0, 1, ksize=1)
接下來,我們通過下面的公式來計算梯度的幅值和方向:

在 OpenCv 中,我們可以使用 cartToPolar 函數來計算上述數值:
// C++ Calculate gradient magnitude and direction (in degrees)
Mat mag, angle; cartToPolar(gx, gy, mag, angle, 1); The same code in python looks like this.
# Python Calculate gradient magnitude and direction ( in degrees )
mag, angle = cv2.cartToPolar(gx, gy, angleInDegrees=True)
下圖展示了梯度計算結果:
左邊:x 方向梯度的絕對值
中間:y 方向梯度的絕對值
右邊:梯度的幅值
注意 x 方向的梯度代表垂直方向的變化趨勢,而 y 方向代表的是水平方向的變化;
如果圖像像素變換迅速的話,可以在梯度圖中明顯看出,而當區域內變化緩慢時,不會出現梯度幅值;
梯度圖像去除了很多不必要的信息,保留了關鍵信息。換句話說,你可以看着梯度圖,然后輕松的辨別出來照片里的人;
每一個像素點,都有一個 Magnitude / 幅值 和 Direction / 方向;
對於彩色的圖像,三種通道的梯度都會被評估計算,取的是最大的梯度。
步驟 3:在 8*8 cells / 網格 中計算梯度直方圖
這一步,圖像會被分割成 8*8 大小的單獨 cells / 小格子,然后對於每個 8*8 的小格子,分別計算梯度直方圖;
我們先來了解下為什么要把圖像分割為 8*8 的小格子;
有一個重要的原因是使用特征描述子來描述一幅 image / 圖像 的一個 patch / 子圖像 的話,網格分割會提供了一個 compact / 緊湊 的表示方式;
一個 8*8 的子圖像包含 8*8*3 = 192 個像素值。每個像素梯度有兩個值( Magnitude / 幅值 和 Direction / 方向 ),所以每個子圖像會有 8*8*2=128 個數值;
在這節結束之前,我們會看到這 128 個數值如何使用 9 位的數組來 存儲在 9 位的直方圖中。經過壓縮處理之后的數據具有更好的 抗噪性。
但是為什么取得是 8*8 的子圖像而不是 32*32? 這是根據我們所要檢測的目標來決定的;
對於 HOG 行人檢測, 從 64*128 的行人圖像中提取出的 8*8 子圖像,已經足夠用來提取出有用的信息(比如臉部,頭的頂部等等)
直方圖有必要是 9 位的向量,與 0,20,40,60… 160 度對應;
讓我們來看看在一個 8*8 的子圖像中,梯度是什么樣的:

中間:用箭頭來代表顏色和梯度的變化;
右邊:用數字來代表子圖像中的梯度;
如果你是一個計算機視覺的初學者,中間的圖像會很有幫助很形象;
通過箭頭來表示圖像中梯度的變化,箭頭的方向表示着像素強度變化的方向,幅值表示變化的緩慢;
通過右邊的圖,我們可以看到 8*8 子圖像中提取出來的代表梯度的數值,這些角度從 0~180 度而不是 0~360 度,這些被稱之為 unsigned gradients / 無符號梯度,因為一個梯度和它取負之后得到的是同樣的數值;
換句話說,一個梯度箭頭旋轉180度之后被認為是一樣的;
但是為什么我們不使用 0-360 度呢?經驗告訴我們使用無符號的梯度,比使用有符號的梯度在行人檢測中性能更好。不過一些 HOG 的實現中也可以允許你使用有符號的梯度。
接下來就是為這些 8*8 的子圖像,建立一個梯度直方圖。直方圖有 9 位,來與 0, 20, 40…160 度相對應;
下面的圖像向我們展示了操作過程,我們關注從 8*8 子圖像中提取出來的幅值和方向;
* 根據梯度的方向來選擇使用填充到哪一位,然后根據梯度的幅值來填充數值;
我們先來看看 藍圈的數值,角度為 80,幅值為2,所以在直方圖第五位加 2;
再來看看 紅圈的數值,角度為 10,幅值為 4,角度 10 的話在 0 和 20 之間,所以將它的幅值 4 被一分為 2 ,分別在直方圖的 "0 位" 和 "20 位" 里面放 2 。

還需要注意的一點是,如果 角度比 160 大,在 160 和 180 之間。我們知道在這里 0 度和 180 度一樣,所以下面這個例子,角度 165 度被分到了 0 度和 160 度 兩個位里面。

8*8 子圖像提取出來的數值,經過處理,可以得到一個 9 位的直方圖,對於上面的子圖像,我們可以得到如下的直方圖:

在我們的表示中,y 軸默認為 0 度。你可以從直方圖中看到,在 0~180 度之間有很多分布,這也表明子圖像中的梯度方向要么朝上要么朝下。
步驟 4:16*16 塊歸一化
在之前的步驟中,我們根據圖像的梯度制作了直方圖。但是對於亮度不同的圖像,梯度很敏感。
如果你讓所有像素點的數值除 2 來讓圖像變暗,梯度幅值也會相應的減半,因此直方圖也會對應着減半。
理想情況下,我們希望我們的描述器是不隨着亮度變化而變化的,換句話說,我們想要歸一化直方圖,所以讓它不受亮度影響;

在我說明直方圖如何被歸一化之前,讓我們來看看,一個長度為 3 的向量是如何被歸一化的;
比如我們有個 RGB 顏色向量為 [ 128, 64, 32 ],計算出長度為:
這也被稱為這個向量的 L2 范數;
對向量的每個元素除以 146.64,得到歸一化之后的向量 [ 0.87, 0.43, 0.22 ]。
現在考慮另一個向量,它的數值是之前向量的兩倍,2 x [ 128, 64, 32 ] = [ 256, 128, 64 ];
通過同樣的計算方式,你可以得到同樣的歸一化向量 [ 0.87, 0.43, 0.22 ],這就可以解決之前提到的亮度的影響問題。
現在我們知道了如何去歸一化向量,也許你會認為,歸一化 9*1 的直方圖和上面介紹的 3*1 的向量歸一化一樣。這想法並沒有錯,但是更好的方式是用一個更大尺寸 16*16 的塊去歸一化;
也就是 36*1 的直方圖可以看成 4 個 9*1 的直方圖構成,然后窗口以 8 像素移動(見上圖),計算出歸一化的 36*1 大小的向量然后重復這個過程遍歷圖像。
可視化 HOG
通過在 8*8 子圖像里面進行 9*1 歸一化的直方圖,我們可以可視化子圖像的 HOG 的描述子。
在下圖中你會發現,直方圖的 Dominant direction / 主要方向 捕獲了這個人的外形,尤其在軀干和腿。
不幸的是,在 OpenCv 中進行 HOG 的特征描述子的可視化比較困難。

# 英文版權 @
# 翻譯中文版權 @ coneypo
# 轉載請注明出處