Opencv之HOG特徵與SVM相結合的人體檢測

Hi洛基又回來了,最近正在學習計算機視覺和機器學習,這兩門課程都要求做課程設計,於是我想到了這個一舉兩得(偷懶- -)的題目。

首先來說說圖像處理領域的HOG特徵:

HOG(histogram of oriented gradient) 是用於目標檢測的特徵描述子,由Navneet DalalBill 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~!








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章