HOG原理與OpenCV實現
方向梯度直方圖(Histogram of Oriented Gradient, HOG)於2005年提出,是一種常用的特徵提取方法,HOG+SVM在行人檢測中有著優異的效果。
HOG特徵提取演算法原理
在一幅影象中,梯度或邊緣的方向密度分佈能夠很好地描述區域性目標區域的特徵,HOG正是利用這種思想,對梯度資訊做出統計,並生成最後的特徵描述。在HOG中,對一幅影象進行了如下劃分: 影象(image)->檢測視窗(win)->影象塊(block)->細胞單元(cell)
流程圖如下: 對於上述流程圖,有幾點需要注意的地方: 1.色彩和伽馬歸一化為了減少光照因素的影響,首先需要將整個影象進行規範化(歸一化)。在影象的紋理強度中,區域性的表層曝光貢獻的比重較大,所以,這種壓縮處理能夠有效地降低影象區域性的陰影和光照變化。 2.影象的梯度針對的是每一個畫素計算得到,然後再cell中進行方向梯度直方圖的構建,在block中進行對比度歸一化操作。 3.由於視窗的滑動性與塊的滑動行,視窗與塊都會出現不同程度的重疊(由步長決定),此時在塊內劃分出的cell就會多次出現,這就意味著:每一個細胞單元的輸出都多次作用於最終的描述器。
數字影象梯度的計算:
在二元連續函式的情形下,設函式z=f(x,y)在平面區域D內具有一階連續偏導數,則對於每一點p(x,y)⊆Dp(x,y)⊆D,都可以定出一個向量
∂f∂xi+∂f∂yj∂f∂xi+∂f∂yj
這向量稱為函式z=f(x,y)z=f(x,y)在點p(x,y)p(x,y)的梯度,記作gradf(x,y)gradf(x,y)。 而對於數字影象影象而言,相當於對二維離散函式求梯度,如下:
G(x,y)=dx(x,y)+dy(x,y)dx(x,y)=I(x+1,y)−I(x,y)dy(x,y)=I(x,y+1)−I(x,y)G(x,y)=dx(x,y)+dy(x,y)dx(x,y)=I(x+1,y)−I(x,y)dy(x,y)=I(x,y+1)−I(x,y)
其中,I(x,y)I(x,y)是影象在點(x,y)(x,y)處的畫素值。
HOG中的win ,block ,cell
HOG最先是用來做行人檢測的,顯然這是一個目標檢測的任務,當我們使用滑動窗遍歷方法實現目標檢測任務時,首先我們需要構建一個滑動窗,這個滑動窗就是HOG中win的概念。可以理解為,在HOG特徵提取時,一個視窗是最小的特徵提取單元,在目標檢測任務中,滑動窗將以一個設定的步長在整個影象中順序的滑動,每一次滑動後,都會提取視窗內的HOG特徵,提取到的特徵將送入到預先訓練好的分類器中,如果分類器模型判定其為目標,則完成目標檢測任務。 比如,在一個影象中選擇檢測視窗,依靠檢測視窗尺寸,視窗滑動步長與影象尺寸共同決定將選擇幾個檢測視窗,比如影象的尺寸為166×80166×80,檢測視窗的尺寸為64×6464×64,視窗步長為(8,8)(8,8)。那麼在影象的列中將滑動次數如下:
numcols=(166−64)8+1=13.75numcols=(166−64)8+1=13.75
顯然這並不合乎邏輯,此時將自動填充,所以列中可以滑動出14個win; 同理,行中滑動次數如下:
numrows=(80−64)8+1=3numrows=(80−64)8+1=3
所以影象中共滑動14×3=4214×3=42個視窗。 那麼對於一個視窗內選擇塊是一樣的原理,假設給出塊的尺寸為16×1616×16,塊步長為(8,8)(8,8),經過計算:檢測視窗中共滑動7×7=497×7=49個block。 在一個塊中選擇細胞單元不再滑動,給出細胞單元的尺寸為(8,8)(8,8),所以一個塊中一共有2×2=42×2=4個cell。 在這裡有一個需要注意的地方時,視窗在滑動時可以根據畫素填充方法補齊的,但是對於塊與cell來說,是不可以的。因為這是演算法本身的一個硬性要求,所在這要求我們在做視窗尺寸,塊尺寸,塊步長,單元尺寸選擇時,必須滿足一下條件: 1.一個視窗內根據塊步長與塊尺寸滑動塊時,必須可以滑動出整數個塊。 2.在塊內確定單元個數時,必須要整數個單元。
HOG構建方向梯度直方圖:
HOG構建方向梯度直方圖在cell中完成,bins的個數決定了方向的範圍。 細胞單元中的每一個畫素點都為某個基於方向的直方圖通道投票。 投票是採取加權投票的方式,即每一票都是帶有權值的,這個權值是根據該畫素點的梯度幅度計算出來。可以採用幅值本身或者它的函式來表示這個權值,實際測試表明: 使用幅值來表示權值能獲得最佳的效果,當然,也可以選擇幅值的函式來表示,比如幅值的平方根、幅值的平方、幅值的截斷形式等。細胞單元可以是矩形的,也可以是星形的。直方圖通道是平均分佈在0-180(無向)或0-360(有向)範圍內。經研究發現,採用無向的梯度和9個直方圖通道,能在行人檢測試驗中取得最佳的效果。而在這種情況下方向的範圍劃分為1809=201809=20度。
特徵向量維數
之前提到過,cell的中方向範圍的個數由bins來決定,還是以9為例:所以,一個cell中的向量為9個。以上面的例子,在一個尺寸為64×6464×64的檢測窗中,描述子的維數就應該為:9×4×49=17649×4×49=1764 。其中4為一個block中cell的個數,49為一個win中block的個數。
那麼HOG作為一種特徵提取演算法,對於影象分類問題該如何提取特徵呢?此時的視窗將是整幅影象,也就是說,視窗將不再在影象中滑動。
HOG的OpenCV實現
注意事項
在HOG的原理部分,其實我們已經提到了一些注意的事項,那就是塊尺寸,塊步長,單元尺寸,視窗步長的選擇問題。這些引數在自行設計時應該滿足滑動出整數的條件,否則程式碼會出現異常。在Opencv中,在構建類HOGDescriptor
的物件時,它是帶有初始值的:
CV_WRAP HOGDescriptor() : winSize(64,128), blockSize(16,16), blockStride(8,8),
cellSize(8,8), nbins(9), derivAperture(1), winSigma(-1),
histogramNormType(HOGDescriptor::L2Hys), L2HysThreshold(0.2), gammaCorrection(true),
nlevels(HOGDescriptor::DEFAULT_NLEVELS)
- 1
- 2
- 3
- 4
計算一下的話會發現這些值滿足之前說的條件,所以當我們在設計這些引數時,也要注意這點。
此外,上面這些引數是沒有視窗步長的,這是因為視窗步長定義在hog.compute()
函式中,該函式對滑動窗是有自動補齊功能的。
程式碼實現
OpenCV中,HOG被封裝在了HOGDescriptor 類中,而且OpenCV提供了直接利用HOG+SVM進行多尺度行人檢測的函式detectMultiScale(),在這裡我們不介紹它,只說明如何利用HOG提取出可以輸入到SVM中的特徵矩陣。需要說明的是,這是一個影象分類任務的特徵提取過程,所以,這要求我們將整個影象作為一個視窗在構建hog特徵。hog.compute()
函式在計算特徵時,不在滑動視窗。
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/objdetect/objdetect.hpp>
using namespace std;
using namespace cv;
#define Posnum 2 //正樣本個數
#define Negnum 2 //負樣本個數
int main()
{
char adpos[128],adneg[128];
HOGDescriptor hog(Size(64,64),Size(16,16),Size(8,8),Size(8,8),3);//利用建構函式,給物件賦值。
int DescriptorDim;//HOG描述子的維數
Mat samFeatureMat, samLabelMat;
//依次讀取正樣本圖片,生成HOG描述子
for (int i = 1;i <= Posnum ;i++)
{
sprintf_s(adpos, "F:\\pos\\%d.jpg", i);
Mat src = imread(adpos);//讀取圖片
resize(src,src,Size(64,64));
vector<float> descriptors;//HOG描述子向量
hog.compute(src,descriptors);
if ( i == 1)
{
DescriptorDim = descriptors.size();
samFeatureMat = Mat::zeros(Posnum +Negnum , DescriptorDim, CV_32FC1);
samLabelMat = Mat::zeros(Posnum +Negnum , 1, CV_32FC1);
}
for(int j=0; j<DescriptorDim; j++)
{
samFeatureMat.at<float>(i-1,j) = descriptors[j];
samLabelMat.at<float>(i-1,0) = 1;
}
}
//依次讀取負樣本圖片,生成HOG描述子
for (int k = 1;k <= Negnum ;k++)
{
sprintf_s(adneg, "F:\\neg\\%d.jpg", k);
Mat src = imread(adneg);//讀取圖片
resize(src,src,Size(64,64));
vector<float> descriptors;//HOG描述子向量
hog.compute(src,descriptors);
for(int l=0; l<DescriptorDim; l++)
{
samFeatureMat.at<float>(k+Posnum-1,l) = descriptors[l];
samLabelMat.at<float>(k+Posnum-1,0) = -1;
}
}
cout<<"特徵個數:"<<samFeatureMat.rows<<endl;
cout<<"特徵維度:"<<samFeatureMat.cols<<endl;
return 0;
}
程式碼的邏輯還是很簡單的,要注意的地方在於讀取正樣本的for迴圈中加入了一個if判斷是為了初始化samFeatureMat矩陣的行列,顯然,最後SVM要用來訓練的矩陣為samFeatureMat和samLabelMat。samLabelMat的列為1,因為他只存放了一個正或負的標籤,而samFeatureMat的則為:所有樣本的個數*描述子維數。這也就是為啥初始化要放在迴圈裡面了,因為沒有提取特徵呢,誰知道描述子維數是多少呢?(這樣就不用手算了)