首先來說說圖像處理領域的HOG特徵:
HOG(histogram of oriented gradient) 是用於目標檢測的特徵描述子,由Navneet Dalal和Bill Triggs於05年在CVPR首次提出,是用於靜態圖像或視頻行人檢測的實用方法。HOG的核心思想:是利用物體的像素梯度以及邊緣方向分佈來描述該物體的appearance 和shape。
HOG算法步驟:
掃描一副圖像,
①將圖像灰度化
②採用Gamma校正法進行顏色空間的標準化(歸一化)——調節圖像的對比度,降低圖像局部的陰影、光照變化所造成的影響,抑制噪音的干擾
③計算每個像素梯度的大小和方向
④將圖像劃分成小cells
⑤統計每個cell的梯度直方圖
⑥將2x2cell或者3x3cell或者更多...組成一個block,一個block內所有cell的特徵descriptor串聯起來便得到該block的HOG特徵descriptor。將圖像內的所有block的HOG特徵descriptor串聯起來就可以得到該圖像的HOG特徵descriptor。
說了這麼多,其實我這種一般的玩家並沒有懂得HOG算法的深層數學性原理,以後有空要好好研究一下。下面來說說HOG與SVM的結合。
我利用opencv+VS2010 for win7 64bits,很方便地實現了HOG特徵的提取和SVM訓練。OpenCV中的HOG特徵提取功能使用了HOGDescriptor這個類來進行封裝,其中也有現成的行人檢測的接口。
①HOGDescriptor類,默認構造函數如下(更詳細資料請看http://blog.csdn.net/raodotcong/article/details/6239431)
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)
winSize : 窗口的大小
blockSize :塊的大小
blockStride:塊步長
cellSize: 胞元的大小
nbins: 方向bin的個數 nBins表示在一個胞元(cell)中統計梯度的方向數目,例如nBins=9時,在一個胞元內統計9個方向的梯度直方圖,每個方向爲360/9=40度
②HOGDescriptor.compute(src,descriptors,Size(8,8))方法:計算源圖像src的描述子,步長爲(8,8)
然後是SVM訓練方法。首先需要了解CvSVM類,更詳細資料請看http://blog.csdn.net/xidianzhimeng/article/details/41979785。
用opencv訓練SVM的一般方法是:
①設置訓練樣本集
需要兩組數據,一組是數據的類別,一組是數據的向量信息。
②設置SVM參數
利用CvSVMParams類實現類內的成員變量svm_type表示SVM類型:
CvSVM::C_SVC C-SVC
CvSVM::NU_SVCv-SVC
CvSVM::ONE_CLASS一類SVM
CvSVM::EPS_SVRe-SVR
CvSVM::NU_SVRv-SVR
成員變量kernel_type表示核函數的類型:
CvSVM::LINEAR線性:u‘v
CvSVM::POLY多項式:(r*u'v + coef0)^degree
CvSVM::RBFRBF函數:exp(-r|u-v|^2)
CvSVM::SIGMOIDsigmoid函數:tanh(r*u'v + coef0)
成員變量degree針對多項式核函數degree的設置,gamma針對多項式/rbf/sigmoid核函數的設置,coef0針對多項式/sigmoid核函數的設置,Cvalue爲損失函數,在C-SVC、e-SVR、v-SVR中有效,nu設置v-SVC、一類SVM和v-SVR參數,p爲設置e-SVR中損失函數的值,class_weightsC_SVC的權重,term_crit爲SVM訓練過程的終止條件。其中默認值degree = 0,gamma = 1,coef0 = 0,Cvalue = 1,nu = 0,p = 0,class_weights = 0
③訓練SVM
調用CvSVM::train函數建立SVM模型,第一個參數爲訓練數據,第二個參數爲分類結果,最後一個參數即CvSVMParams
用這個SVM進行分類
調用函數CvSVM::predict實現分類
④獲得支持向量
除了分類,也可以得到SVM的支持向量,調用函數CvSVM::get_support_vector_count獲得支持向量的個數,CvSVM::get_support_vector獲得對應的索引編號的支持向量。
// step 1:
float labels[4] = {1.0, -1.0, -1.0, -1.0};
Mat labelsMat(3, 1, CV_32FC1, labels);
float trainingData[4][2] = { {501, 10}, {255, 10}, {501, 255}, {10, 501} };
Mat trainingDataMat(3, 2, CV_32FC1, trainingData);
// step 2:
CvSVMParams params;
params.svm_type = CvSVM::C_SVC;
params.kernel_type = CvSVM::LINEAR;
params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6);
// step 3:
CvSVM SVM;
SVM.train(trainingDataMat, labelsMat, Mat(), Mat(), params);
// step 4:
Vec3b green(0, 255, 0), blue(255, 0, 0);
for (int i=0; i<image.rows; i++)
{
for (int j=0; j<image.cols; j++)
{
Mat sampleMat = (Mat_<float>(1,2) << i,j);
float response = SVM.predict(sampleMat);
if (fabs(response-1.0) < 0.0001)
{
image.at<Vec3b>(j, i) = green;
}
else if (fabs(response+1.0) < 0.001)
{
image.at<Vec3b>(j, i) = blue;
}
}
}
// step 5:
int c = SVM.get_support_vector_count();
for (int i=0; i<c; i++)
{
const float* v = SVM.get_support_vector(i);
}
好了接下來進入正題(咳咳,其實前面也是正題,不要在意我的措辭細節):
首先是找樣本,正負樣本當然是越多越好,我找的樣本不多,正負樣本各1200張左右,網上能找到很多有關人體檢測的樣本的,這裏貼一下我的樣本來源:http://pascal.inrialpes.fr/data/human/(很有名的INRIA行人數據庫~)。樣本蒐集好之後,還要對正負樣本的格式進行處理,我的處理步驟比較粗略:
①格式轉換爲統一的jpg格式(下載好的圖片既有jpg也有png,所以爲了處理方便,進行了統一轉換);②正樣本的大小進行歸一化處理,我將正樣本統一處理成64×128像素尺寸,負樣本倒是不需要過多處理;③命名規則化——正負樣本放入不同的文件夾,分別命名爲1.jpg,2.jpg,3.jpg...這樣做能方便圖片的輸入。
In particular...我剛開始自己編程寫了個文件批量改名程序和格式轉換程序,花了不少時間- -(聲明:洛基是學渣)然後發現可以直接用專業的軟件進行快速處理,當時心情簡直了- -By the way,在這裏洛基推薦一款圖像處理方便挺不錯的軟件——ACDSee 18。除了會一直佔用內存外,這款軟件還是很好用的。
OK,now let's begin our game.
#include<iostream>
#include <fstream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/objdetect/objdetect.hpp>
#include <opencv2/ml/ml.hpp>
using namespace cv;
using namespace std;
#define PosSamNO 1126 //正樣本個數
#define NegSamNO 1210 //負樣本個數
//生成setSVMDetector()中用到的檢測子參數時要用到的SVM的decision_func參數時protected類型,只能繼承之後通過函數訪問
class MySVM : public CvSVM
{
public:
//獲得SVM的決策函數中的alpha數組
double * get_alpha_vector()
{
return this->decision_func->alpha;
}
//獲得SVM的決策函數中的rho參數,即偏移量
float get_rho()
{
return this->decision_func->rho;
}
};
int main()
{
HOGDescriptor hog(Size(64,128),Size(16,16),Size(8,8),Size(8,8),9);//窗口大小(64,128),塊尺寸(16,16),塊步長(8,8),cell尺寸(8,8),直方圖bin個數9
int DescriptorDim;//HOG描述子的維數,由圖片大小、檢測窗口大小、塊大小、細胞單元中直方圖bin個數決定
MySVM svm;
string ImgName;//圖片名
ifstream finPos("pos.txt");//正樣本圖片的文件名列表
ifstream finNeg("neg.txt");//負樣本圖片的文件名列表
Mat sampleFeatureMat;//所有訓練樣本的特徵向量組成的矩陣,行數等於所有樣本的個數,列數等於HOG描述子維數
Mat sampleLabelMat;//訓練樣本的類別向量,行數等於所有樣本的個數,列數等於1;1表示有人,-1表示無人
//依次讀取正樣本圖片,生成HOG描述子
for(int num=0; num<PosSamNO && getline(finPos,ImgName); num++)
{
ImgName = "E:\\INRIAPerson\\Posjpg64_128\\" + ImgName;//加上正樣本的路徑名
Mat src = imread(ImgName);//讀取圖片
vector<float> descriptors;//HOG描述子向量
hog.compute(src,descriptors,Size(8,8));//計算HOG描述子,檢測窗口移動步長(8,8)
//處理第一個樣本時初始化特徵向量矩陣和類別矩陣,因爲只有知道了特徵向量的維數才能初始化特徵向量矩陣
if( 0 == num )
{
DescriptorDim = descriptors.size();//HOG描述子的維數
//初始化所有訓練樣本的特徵向量組成的矩陣sampleFeatureMat,行數等於所有樣本的個數,列數等於HOG描述子維數
sampleFeatureMat = Mat::zeros(PosSamNO+NegSamNO, DescriptorDim, CV_32FC1);
//初始化訓練樣本的類別向量,行數等於所有樣本的個數,列數等於1;1表示有人,-1表示無人
sampleLabelMat = Mat::zeros(PosSamNO+NegSamNO+HardExampleNO, 1, CV_32FC1);
}
//將計算好的HOG描述子複製到樣本特徵矩陣sampleFeatureMat
for(int i=0; i<DescriptorDim; i++)
sampleFeatureMat.at<float>(num,i) = descriptors[i];//第num個樣本的特徵向量中的第i個元素
sampleLabelMat.at<float>(num,0) = 1;//正樣本類別爲1,有人
}
//處理負樣本的流程和正樣本大同小異
for(int num=0; num<NegSamNO && getline(finNeg,ImgName); num++)
{
ImgName = "E:\\INRIAPerson\\Negjpg_undesign\\" + ImgName;//加上負樣本的路徑名
Mat src = imread(ImgName);//讀取圖片
vector<float> descriptors;//HOG描述子向量
hog.compute(src,descriptors,Size(8,8));//計算HOG描述子,檢測窗口移動步長(8,8)
//將計算好的HOG描述子複製到樣本特徵矩陣sampleFeatureMat
for(int i=0; i<DescriptorDim; i++)
sampleFeatureMat.at<float>(num+PosSamNO,i) = descriptors[i];//第PosSamNO+num個樣本的特徵向量中的第i個元素
sampleLabelMat.at<float>(num+PosSamNO,0) = -1;//負樣本類別爲-1,無人
}
//輸出樣本的HOG特徵向量矩陣到文件
ofstream fout("SampleFeatureMat.txt");
for(int i=0; i<PosSamNO+NegSamNO; i++)
{
fout<<i<<endl;
for(int j=0; j<DescriptorDim; j++)
fout<<sampleFeatureMat.at<float>(i,j)<<" ";
fout<<endl;
}
//訓練SVM分類器,迭代終止條件,當迭代滿1000次或誤差小於FLT_EPSILON時停止迭代
CvTermCriteria criteria = cvTermCriteria(CV_TERMCRIT_ITER+CV_TERMCRIT_EPS, 1000, FLT_EPSILON);
//SVM參數:SVM類型爲C_SVC;線性核函數;鬆弛因子C=0.01
CvSVMParams param(CvSVM::C_SVC, CvSVM::LINEAR, 0, 1, 0, 0.01, 0, 0, 0, criteria);
cout<<"開始訓練SVM分類器"<<endl;
svm.train(sampleFeatureMat, sampleLabelMat, Mat(), Mat(), param);
cout<<"訓練完成"<<endl;
svm.save("SVM_HOG.xml");//將訓練好的SVM模型保存爲xml文件
DescriptorDim = svm.get_var_count();//特徵向量的維數,即HOG描述子的維數
cout<<"描述子維數:"<<DescriptorDim<<endl;
int supportVectorNum = svm.get_support_vector_count();//支持向量的個數
cout<<"支持向量個數:"<<supportVectorNum<<endl;
Mat alphaMat = Mat::zeros(1, supportVectorNum, CV_32FC1);//alpha向量,長度等於支持向量個數
Mat supportVectorMat = Mat::zeros(supportVectorNum, DescriptorDim, CV_32FC1);//支持向量矩陣
Mat resultMat = Mat::zeros(1, DescriptorDim, CV_32FC1);//alpha向量乘以支持向量矩陣的結果
//將支持向量的數據複製到supportVectorMat矩陣中,共有supportVectorNum個支持向量,每個支持向量的數據有DescriptorDim維(種)
for(int i=0; i<supportVectorNum; i++)
{
const float * pSVData = svm.get_support_vector(i);//返回第i個支持向量的數據指針
for(int j=0; j<DescriptorDim; j++)
supportVectorMat.at<float>(i,j) = pSVData[j];//第i個向量的第j維數據
}
//將alpha向量的數據複製到alphaMat中
//double * pAlphaData = svm.get_alpha_vector();//返回SVM的決策函數中的alpha向量
double * pAlphaData = svm.get_alpha_vector();
for(int i=0; i<supportVectorNum; i++)
{
alphaMat.at<float>(0,i) = pAlphaData[i];//alpha向量,長度等於支持向量個數
}
resultMat = -1 * alphaMat * supportVectorMat;//計算-(alphaMat * supportVectorMat),結果放到resultMat中,
//注意因爲svm.predict使用的是alpha*sv*another-rho,如果爲負的話則認爲是正樣本,在HOG的檢測函數中,
//使用rho-alpha*sv*another如果爲正的話是正樣本,所以需要將後者變爲負數之後保存起來
//得到最終的setSVMDetector(const vector<float>& detector)參數中可用的檢測子
vector<float> myDetector;
//將resultMat中的數據複製到數組myDetector中
for(int i=0; i<DescriptorDim; i++)
{
myDetector.push_back(resultMat.at<float>(0,i));
}
myDetector.push_back(svm.get_rho());//最後添加偏移量rho,得到檢測子
cout<<"檢測子維數:"<<myDetector.size()<<endl;
//設置HOGDescriptor的檢測子,用我們訓練的檢測器代替默認的檢測器
HOGDescriptor myHOG;
myHOG.setSVMDetector(myDetector);
//保存檢測子參數到文件
ofstream fout("HOGDetectorParagram.txt");
for(int i=0; i<myDetector.size(); i++)
fout<<myDetector[i]<<endl;
//讀入圖片進行人體檢測
Mat src = imread("test1.png");
vector<Rect> found, found_filtered;//矩形框數組
cout<<"進行多尺度HOG人體檢測"<<endl;
myHOG.detectMultiScale(src, found, 0, Size(8,8), Size(32,32), 1.05, 2);//對圖片進行多尺度行人檢測
cout<<"找到的矩形框個數:"<<found.size()<<endl;
//找出所有沒有嵌套的矩形框r,並放入found_filtered中,如果有嵌套的話,則取外面最大的那個矩形框放入found_filtered中
for(int i=0; i < found.size(); i++)
{
Rect r = found[i];
int j=0;
for(; j < found.size(); j++)
{
if(j != i && (r & found[j]) == r)//說明r是被嵌套在found[j]裏面的,捨棄當前的r
break;
}
if( j == found.size())//r沒有被嵌套在第0,1,2...found.size()-1號的矩形框內,則r是符合條件的
found_filtered.push_back(r);
}
//對畫出來的矩形框做一些大小調整
for(int i=0; i<found_filtered.size(); i++)
{
Rect r = found_filtered[i];
r.x += cvRound(r.width*0.1);
r.width = cvRound(r.width*0.8);
r.y += cvRound(r.height*0.07);
r.height = cvRound(r.height*0.8);
rectangle(src, r.tl(), r.br(), Scalar(255,0,0), 2);
}
imwrite("ImgProcessed.jpg",src);
namedWindow("src",0);
imshow("src",src);
waitKey();
system("pause");
}
這些是部分檢測結果圖~其實檢測效果一般,很多誤判和漏檢,所以需要注意的是,正樣本一定要把格式處理好,最好是一張圖片大部分是包含那個人體,其他物體、風景不要包含太多進去了,同時正負樣本的數量要多~~等過兩天改進一下,添加一個自舉檢測的功能試試看,據說效果很好,拭目以待吧~Bye~!