1. 程式人生 > >HOG原理與OpenCV實現

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的則為:所有樣本的個數*描述子維數。這也就是為啥初始化要放在迴圈裡面了,因為沒有提取特徵呢,誰知道描述子維數是多少呢?(這樣就不用手算了)