利用hog+svm(梯度方向直方圖和支援向量機)實現物體檢測
最近利用hog+svm做了一個物體檢測的小程式,可以先給大家看看實驗的結果。從照片中,檢測出以任意姿態擺放在任意位置的公仔。(其實打算檢測是紅色的大公仔,但是小的公仔也被檢測了出來,至於為什麼會這樣以及這個問題的解決方法,咱們下面可以接著討論)
其實吧,網上關於hog和svm的教程和書籍也非常多。但是很少有那種讓初學者或者不太瞭解相關內容的人一看就懂的文章或是部落格。反正我是看了好多的部落格,文章,又找了程式動手做,才能大概理解程式的具體執行過程。所以,我這次重點做一下查漏補缺的工作,我把別人文章裡經常會忽略掉的細節,同時也是初學者不太容易理解的地方,挑選出來,充分的捋一捋。
首先說hog,中文名為梯度方向直方圖。這個網上的教程很多,我找了一個還不錯的分享給大家:
教程裡面講解的非常詳細,同時也有一些數學公式,剛開始看很可能一臉懵逼的。不過沒關係,一開始只需要搞懂關鍵的問題就可以了。剩下的細節可以以後再慢慢的琢磨。
hog的話,相當於用一個滑動的視窗,在影象上從左到右,從上到下的滑動,每滑動到一個位置,計算其hog資訊,重點要明白,這個hog資訊,其實就是一個1*n的矩陣,說白了就是一個向量,這個向量就代表了這個視窗內的資訊。
在物體檢測方面,這個視窗內有或者沒有我要檢測的物體,其得到的hog向量應該是有不同的。但是怎麼去比較不同呢,很有肯能是上百維度甚至上千維度的向量,這時候用svm就是一個很好的選擇。
接下來說svm,也叫支援向量機,具體的教程可以看這裡:
同樣的,裡面有很多數學公式,一時看不懂耶沒關係,明白一點就可以了。svm就是一個二值分類器,能夠將向量進行二值分類。
說白了,給它一堆向量,告訴它這些都是a型別,再給另外一堆向量,告訴它這些向量都是b型別,然後讓svm自己去訓練就好了。它能夠訓練出一個不錯的方法,使得你再給它一個向量的時候,它可以告訴你這個向量是屬於a型別還是b型別。
說到這裡,hog+svm 檢測物體的原理基本上也就全都說清楚了。將詳細的過程再描述一遍,以本文檢測公仔為例子:
1.首先需要確定hog檢測視窗的大小,這裡以64*64畫素大小的方框作為檢測視窗。
1.為了讓svm得到充足的訓練,首先要準備大量的正負樣本。這裡我收集了各個姿態下的公仔的圖片,共3000多張,這些樣本都是正樣本,且都為64*64畫素大小。如圖:
2.同樣的,還需要負樣本,在沒有公仔的場地內隨意的拍照吧,大小也需要時64*64才行,共收集了15萬張負樣本圖片,負樣本圖片裡面都是不含有公仔的。(其實這裡我偷了個懶,我用高解析度的相機拍了很多照片,然後在這些照片中去擷取64*64大小的畫素塊作為負樣本,這樣,一張普通的圖片可以截取出上百張負樣本圖片,對於收集資料來說比較省事),如圖:
3.然後,將正負樣本送入svm中進行訓練,得到一個可以檢測正負樣本的結果。(看著資料量不少,幾十萬張圖片,其實運算速度還好,我用個普通的電腦,這個訓練的過程也就用了大概十分鐘吧。其實,這只是個例子,真正要訓練的話,這些數量的訓練資料還是不夠的)
4.然後就需要對圖片中的物體進行檢測了。以64*64的滑動視窗在影象上滑動,每滑動到一個位置,計算其hog特徵,然後給svm產生的結果進行比較。如果方框裡有我們要找的物體,那麼理論上可以將這個hog特徵歸類為正樣本的類別,這就完成了物體的檢測。
這裡有個細節性的東西,圖片中物體的大小不一樣,如何以一個固定大小的視窗去檢測大小不同的物體。其實,道理是,滑動視窗的大小不變,但是圖片的尺度是可以放大縮小的。所以實際操作中,視窗大小始終為64*64,但是將圖片放大縮小到多個解析度上進行檢測,就可以發現不同位置,不同大小的物體了。
5.由於滑動視窗移動的步長非常短,所以一個物體很有可能被檢測出很多次,這些個檢測出的框會密密麻麻的框住同一個物體。所以,後期還需要把這些同一個物體上的不同的框合成為一個。主要是根據框之間的距離和大小來進行方框的合併。最後合併完之後,就像下圖一樣:
然後對結果進行分析:
訓練的時候,以紅色的大公仔作為正樣本,場地內不包含公仔的一些隨機的場景圖片作為負樣本進行訓練,理論上來講,最理想的效果是應該能夠檢測出指定的紅色大公仔。但是實際的結果是將紅色的大公仔和藍色的小公仔都檢測出來了(有的時候還有可能把場地內沒有公仔的地方檢測出有公仔),這就說明,表示紅色大公仔的hog特徵和藍色小公仔的hog特徵有點相似,為了避免這種情況,可以再進行訓練,訓練的時候,把藍色小公仔的圖片作為負樣本輸入訓練集,同樣的,對於其他的沒有物體而檢測出物體的地方,也作為負樣本輸入,將本來有公仔但是沒有檢測出來的地方,同樣也手動標出來作為正樣本輸入訓練集。這樣的過程可以重複幾次。經過這種重複的訓練,並且對正負樣本進行校正,可以大大提高檢測的準確度!
對了,最後把程式碼附上來:(程式碼中,上面被註釋的一部分,是訓練svm的程式碼,下面的一部分,是用訓練好的svm模型來進行物體檢測)
//#include<iostream>
//using namespace std;
//int main()
//{
// cout<<"Hello World"<<endl;
// return 0
//}
//#include "cui_hog_slow.h"
//#include"cui_hog.h"
#include<ctime>
#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 std;
using namespace cv;
#define squ 64 //方框的邊長
#define PosSamNO 3513 //正樣本個數
#define NegSamNO 157160 //負樣本個數
#define TRAIN true //是否進行訓練,true表示重新訓練,false表示讀取xml檔案中的SVM模型
#define CENTRAL_CROP true //true:訓練時,對96*160的INRIA正樣本圖片剪裁出中間的64*128大小人體
//HardExample:負樣本個數。如果HardExampleNO大於0,表示處理完初始負樣本集後,繼續處理HardExample負樣本集。
//不使用HardExample時必須設定為0,因為特徵向量矩陣和特徵類別矩陣的維數初始化時用到這個值
#define HardExampleNO 0
//繼承自CvSVM的類,因為生成setSVMDetector()中用到的檢測子引數時,需要用到訓練好的SVM的decision_func引數,
//但通過檢視CvSVM原始碼可知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()
{
// //檢測視窗(96,96),塊尺寸(16,16),塊步長(8,8),cell尺寸(8,8),直方圖bin個數9
// HOGDescriptor hog(Size(squ,squ),Size(16,16),Size(8,8),Size(8,8),9);//HOG檢測器,用來計算HOG描述子的
// int DescriptorDim;//HOG描述子的維數,由圖片大小、檢測視窗大小、塊大小、細胞單元中直方圖bin個數決定
// MySVM svm;//SVM分類器
// //若TRAIN為true,重新訓練分類器
// if(TRAIN)
// {
// //string ImgName;//圖片名(絕對路徑)
// ifstream finPos("INRIAPerson96X160PosList.txt");//正樣本圖片的檔名列表
// //ifstream finPos("PersonFromVOC2012List.txt");//正樣本圖片的檔名列表
// ifstream finNeg("NoPersonFromINRIAList.txt");//負樣本圖片的檔名列表
// Mat sampleFeatureMat;//所有訓練樣本的特徵向量組成的矩陣,行數等於所有樣本的個數,列數等於HOG描述子維數
// Mat sampleLabelMat;//訓練樣本的類別向量,行數等於所有樣本的個數,列數等於1;1表示有人,-1表示無人
// //依次讀取正樣本圖片,生成HOG描述子
// //for(int num=1; num<(PosSamNO+1) && getline(finPos,ImgName); num++)
//for(int num=0; num<PosSamNO; num++)
// {
// string ImgName;//圖片名(絕對路徑)
// stringstream stream;
// stream<<(num+1);
// ImgName=stream.str();
// cout<<"處理:"<<ImgName<<endl;
// //ImgName = "D:\\DataSet\\PersonFromVOC2012\\" + ImgName;//加上正樣本的路徑名
// ImgName = "D:\\file\\ylab_copy\\photo\\true_64\\" + ImgName+".jpg";
// cout<<ImgName<<endl;
// //ImgName = "D:\\DataSet\\INRIAPerson\\INRIAPerson\\96X160H96\\Train\\pos\\" + ImgName;//加上正樣本的路徑名
// Mat src = imread(ImgName);//讀取圖片
// //if(CENTRAL_CROP)
// //src = src(Rect(16,16,64,128));//將96*160的INRIA正樣本圖片剪裁為64*128,即剪去上下左右各16個畫素
// //resize(src,src,Size(64,128));
// vector<float> descriptors;//HOG描述子向量
// hog.compute(src,descriptors,Size(8,8));//計算HOG描述子,檢測視窗移動步長(8,8)
// //cout<<"描述子維數:"<<descriptors.size()<<endl;
// //處理第一個樣本時初始化特徵向量矩陣和類別矩陣,因為只有知道了特徵向量的維數才能初始化特徵向量矩陣
// if( 0 == num )
// {
// DescriptorDim = descriptors.size();//HOG描述子的維數
// //初始化所有訓練樣本的特徵向量組成的矩陣,行數等於所有樣本的個數,列數等於HOG描述子維數sampleFeatureMat
// sampleFeatureMat = Mat::zeros(PosSamNO+NegSamNO+HardExampleNO, DescriptorDim, CV_32FC1);
// //初始化訓練樣本的類別向量,行數等於所有樣本的個數,列數等於1;1表示有人,0表示無人
// 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,有人
// }
// //依次讀取負樣本圖片,生成HOG描述子
// //for(int num=1; num<(NegSamNO+1) && getline(finNeg,ImgName); num++)
//for(int num=0; num<NegSamNO; num++)
// {
// string ImgName;//圖片名(絕對路徑)
// stringstream stream;
// stream<<(num+1);
// ImgName=stream.str();
// cout<<"處理:"<<ImgName<<endl;
// ImgName = "D:\\file\\ylab_copy\\photo\\false_64\\" + ImgName+".jpg";
// //ImgName = "D:\\DataSet\\NoPersonFromINRIA\\" + ImgName;//加上負樣本的路徑名
// Mat src = imread(ImgName);//讀取圖片
// //resize(src,img,Size(64,128));
// vector<float> descriptors;//HOG描述子向量
// hog.compute(src,descriptors,Size(8,8));//計算HOG描述子,檢測視窗移動步長(8,8)
// //cout<<"描述子維數:"<<descriptors.size()<<endl;
// //將計算好的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,無人
// }
// //處理HardExample負樣本
// //if(HardExampleNO > 0)
// //{
// // ifstream finHardExample("HardExample_2400PosINRIA_12000NegList.txt");//HardExample負樣本的檔名列表
// // //依次讀取HardExample負樣本圖片,生成HOG描述子
// // for(int num=0; num<HardExampleNO && getline(finHardExample,ImgName); num++)
// // {
// // cout<<"處理:"<<ImgName<<endl;
// // ImgName = "D:\\DataSet\\HardExample_2400PosINRIA_12000Neg\\" + ImgName;//加上HardExample負樣本的路徑名
// // Mat src = imread(ImgName);//讀取圖片
// // //resize(src,img,Size(64,128));
// // vector<float> descriptors;//HOG描述子向量
// // hog.compute(src,descriptors,Size(8,8));//計算HOG描述子,檢測視窗移動步長(8,8)
// // //cout<<"描述子維數:"<<descriptors.size()<<endl;
// // //將計算好的HOG描述子複製到樣本特徵矩陣sampleFeatureMat
// // for(int i=0; i<DescriptorDim; i++)
// // sampleFeatureMat.at<float>(num+PosSamNO+NegSamNO,i) = descriptors[i];//第PosSamNO+num個樣本的特徵向量中的第i個元素
// // sampleLabelMat.at<float>(num+PosSamNO+NegSamNO,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檔案
// }
// else //若TRAIN為false,從XML檔案讀取訓練好的分類器
// {
// svm.load("SVM_HOG_2400PosINRIA_12000Neg_HardExample(誤報少了漏檢多了).xml");//從XML檔案讀取訓練好的SVM模型
// }
// //*************************************************************************************************
// // 線性SVM訓練完成後得到的XML檔案裡面,有一個數組,叫做support vector,還有一個數組,叫做alpha,有一個浮點數,叫做rho;
// //將alpha矩陣同support vector相乘,注意,alpha*supportVector,將得到一個列向量。之後,再該列向量的最後新增一個元素rho。
// //如此,變得到了一個分類器,利用該分類器,直接替換opencv中行人檢測預設的那個分類器(cv::HOGDescriptor::setSVMDetector()),
// //就可以利用你的訓練樣本訓練出來的分類器進行行人檢測了。
// //***************************************************************************************************/
// DescriptorDim = svm.get_var_count();//特徵向量的維數,即HOG描述子的維數
// 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矩陣中
// for(int i=0; i<supportVectorNum; i++)
// {
// const float * pSVData = svm.get_support_vector(i);//返回第i個支援向量的資料指標
// for(int j=0; j<DescriptorDim; j++)
// {
// //cout<<pData[j]<<" ";
// supportVectorMat.at<float>(i,j) = pSVData[j];
// }
// }
// //將alpha向量的資料複製到alphaMat中
// double * pAlphaData = svm.get_alpha_vector();//返回SVM的決策函式中的alpha向量
// for(int i=0; i<supportVectorNum; i++)
// {
// alphaMat.at<float>(0,i) = pAlphaData[i];
// }
// //計算-(alphaMat * supportVectorMat),結果放到resultMat中
// //gemm(alphaMat, supportVectorMat, -1, 0, 1, resultMat);//不知道為什麼加負號?
// resultMat = -1 * alphaMat * supportVectorMat;
// //得到最終的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));
// }
// //最後新增偏移量rho,得到檢測子
// myDetector.push_back(svm.get_rho());
// cout<<"檢測子維數:"<<myDetector.size()<<endl;
// //設定HOGDescriptor的檢測子
// HOGDescriptor myHOG(Size(squ,squ),Size(16,16),Size(8,8),Size(8,8),9);
// myHOG.setSVMDetector(myDetector);
// //myHOG.setSVMDetector(HOGDescriptor::getDefaultPeopleDetector());
// //儲存檢測子引數到檔案
// ofstream fout("HOGDetector.txt");
// for(int i=0; i<myDetector.size(); i++)
// {
// fout<<myDetector[i]<<endl;
// }
// cout<<"訓練完成!"<<endl;
//**************讀入圖片進行HOG行人檢測******************/
vector<float> myDetector;
float f;
String ds;
HOGDescriptor myHOG(Size(squ,squ),Size(16,16),Size(8,8),Size(8,8),9);
ifstream finPos("HOGDetector_64.txt");
while(getline(finPos,ds))
{
//cout<<ds<<endl;
f=atof(ds.c_str());
myDetector.push_back(f);
//cout<<f<<endl;
}
myHOG.setSVMDetector(myDetector);
Mat src = imread("D:\\file\\ylab_copy\\photo\\test\\5.jpg");
Mat image;
resize(src,image,Size(src.cols/2,src.rows/2));
vector<Rect> found, found_filtered;//矩形框陣列
clock_t t1,t2,t3;//用來統計程式的執行時間
t1=clock();
myHOG.detectMultiScale(image, found, 0, Size(8,8), Size(32,32), 1.07, 2);
//myHOG.detectMultiScale(image, found, 0, Size(8,8), Size(32,32), 1.05, 2);//對圖片進行多尺度行人檢測
cout<<"找到的矩形框個數:"<<found.size()<<endl;
t2=clock();
//找出所有沒有巢狀的矩形框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) //按位與操作
break;
if( j == found.size())
found_filtered.push_back(r);
}
for(int i=0; i<found.size(); i++)
{
Rect r = found[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(image, r.tl(), r.br(), Scalar(0,255,0), 3);
}
cout << t2-t1 << "/" << CLOCKS_PER_SEC << " (s) "<< endl;
cout << t3-t2 << "/" << CLOCKS_PER_SEC << " (s) "<< endl;
//imwrite("ImgProcessed.jpg",src);
imshow("image",image);
//imwrite("D:\\file\\ylab_copy\\photo\\result\\64\\1.jpg",image);
waitKey(0);//
}