簡單實現一個人臉檢測器(HOG+SVM實現人臉檢測)
第一步,準備資料。
原始資料採用FDDB人臉檢測測評資料集,FDDB是全世界最具權威的人臉檢測評測平臺之一,包含2845張圖片,共有5171個人臉作為測試集。測試集範圍包括:不同姿勢、不同解析度、旋轉和遮擋等圖片,同時包括灰度圖和彩色圖,標準的人臉標註區域為橢圓形。
當然,為了簡單起見,我們不直接使用這個資料集。我的做法是,自己做一些正樣本和負樣本資料集。正樣本,即人臉,從FDDB資料集中選擇一些圖片,然後手動裁剪出人臉部分的圖片,大概裁剪了100張。部分人臉如下圖:
負樣本,即非人臉,採用程式,每隔一定的間隔滑動裁剪。當然,可能會裁剪出一些人臉,這些需要手動除去。其中,裁剪影象的大小有幾種,分別是64*64,100*100,128*128,144*144,160*160,總共截取了大概500張圖片。部分影象結果如下:
訓練資料集,從上述的正樣本中拿出90張,負樣本中拿出400張,作為訓練集。將剩下的正負樣本作為測試集。
第二步,提取特徵。此處特徵採用HOG特徵。
具體步驟如下:
1)讀取訓練的圖片
2)將訓練圖片縮放至64*64大小
3)將影象轉換為灰度影象
4)對灰度影象求出HOG特徵。
得到的HOG為1*144大小的Mat型別的資料。由於下一步SVM訓練中,送入SVM的訓練資料為Mat型別的矩陣,其中每一行表示一個訓練資料,故SVM訓練資料Mat大小為n*144,n表示訓練集數量。因此,需要先建立一個n*144的Mat型別資料,然後將每個影象的HOG特徵複製到剛才產生的Mat資料,每個影象的HOG特徵為剛才產生的Mat資料的一行。
第三步,訓練SVM。opencv3自帶SVM,只需要簡單幾步設定即可。
函式如下:
1)建立SVM
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
2)設定SVM型別
svm->setType(cv::ml::SVM::Types::C_SVC);
3)設定核函式為線性核函式
svm->setKernel(cv::ml::SVM::KernelTypes::LINEAR);
4)設定迭代終止條件
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 100, 1e-6));
5)開始訓練
svm->train(sampleFeatureMat, cv::ml::SampleTypes::ROW_SAMPLE, sampleLabelMat);
6)儲存訓練後的SVM引數
svm->save("svm_save.xml");
注:一些引數說明
svm_type –指定SVM的型別,下面是可能的取值:
CvSVM::C_SVC C類支援向量分類機。 n類分組 (n \geq 2),允許用異常值懲罰因子C進行不完全分類。
CvSVM::NU_SVC \nu類支援向量分類機。n類似然不完全分類的分類器。引數為 \nu 取代C(其值在區間【0,1】中,nu越大,決策邊界越平滑)。
CvSVM::ONE_CLASS 單分類器,所有的訓練資料提取自同一個類裡,然後SVM建立了一個分界線以分割該類在特徵空間中所佔區域和其它類在特徵空間中所佔區域。
CvSVM::EPS_SVR \epsilon類支援向量迴歸機。訓練集中的特徵向量和擬合出來的超平面的距離需要小於p。異常值懲罰因子C被採用。
CvSVM::NU_SVR \nu類支援向量迴歸機。 \nu 代替了 p。
kernel_type –SVM的核心型別,下面是可能的取值:
CvSVM::LINEAR 線性核心。沒有任何向對映至高維空間,線性區分(或迴歸)在原始特徵空間中被完成,這是最快的選擇。K(x_i, x_j) = x_i^T x_j.
CvSVM::POLY 多項式核心: K(x_i, x_j) = (\gamma x_i^T x_j + coef0)^{degree},\gamma > 0.
CvSVM::RBF 基於徑向的函式,對於大多數情況都是一個較好的選擇: K(x_i, x_j) = e^{-\gamma ||x_i - x_j||^2},\gamma > 0.
CvSVM::SIGMOID Sigmoid函式核心:K(x_i, x_j) = \tanh(\gamma x_i^T x_j +coef0).
degree –核心函式(POLY)的引數degree。
gamma –核心函式(POLY/ RBF/ SIGMOID)的引數\gamma。
coef0 –核心函式(POLY/ SIGMOID)的引數coef0。
Cvalue – SVM型別(C_SVC/ EPS_SVR/ NU_SVR)的引數C。
nu – SVM型別(NU_SVC/ ONE_CLASS/ NU_SVR)的引數 \nu。
p – SVM型別(EPS_SVR)的引數 \epsilon。
class_weights – C_SVC中的可選權重,賦給指定的類,乘以C以後變成 class\_weights_i * C。所以這些權重影響不同類別的錯誤分類懲罰項。權重越大,某一類別的誤分類資料的懲罰項就越大。
term_crit – SVM的迭代訓練過程的中止條件,解決部分受約束二次最優問題。您可以指定的公差和/或最大迭代次數。
第四步,測試。將新的圖片送入SVM中,讓SVM預測結果。
總體程式如下:
(測試環境:Win7 64位+ Visual Studio 2015 + Opencv310)
#include<iostream>
#include <fstream>
#include<opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/ml.hpp"
using namespace std;
using namespace cv;
//Parameters
#define N_BINS 16 //Number of bins
#define N_DIVS 3 //Number of cells = N_DIVS*N_DIVS
#define N_PHOG N_DIVS*N_DIVS*N_BINS
#define BIN_RANGE (2*CV_PI)/N_BINS
//Haar Cascade Path
//Input: Grayscale image
//Output: HOG features
Mat hog(const Mat &Img);
#define PosSamNO 90 //正樣本個數
#define NegSamNO 400 //負樣本個數
#define HardExampleNO 0
#define TRAIN true //是否進行訓練,true表示重新訓練,false表示讀取xml檔案中的SVM模型
int main()
{
// initial SVM
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
int DescriptorDim;//HOG描述子的維數,由圖片大小、檢測視窗大小、塊大小、細胞單元中直方圖bin個數決定
Mat sampleFeatureMat;//所有訓練樣本的特徵向量組成的矩陣,行數等於所有樣本的個數,列數等於HOG描述子維數
Mat sampleLabelMat;//訓練樣本的類別向量,行數等於所有樣本的個數,列數等於1;1表示有人,-1表示無人
if (TRAIN)
{
//依次讀取正樣本圖片,生成HOG特徵
for (int i = 1; i <= PosSamNO; i++)
{
char pic_name[64];
sprintf(pic_name, "E:\\data\\face\\pos\\%04d.jpg", i);
// cout << pic_name << endl;
Mat src = imread(pic_name);//讀取圖片
resize(src, src, Size(64, 64));//將圖片大小縮放為64*64
Mat img_gray;
cvtColor(src, img_gray, CV_BGR2GRAY);//將彩色圖片轉換為灰度圖
Mat feature = hog(img_gray);//提取HOG特徵
if (1 == i)
{
DescriptorDim = feature.cols;//feature.size();//HOG描述子的維數
//初始化所有訓練樣本的特徵向量組成的矩陣,行數等於所有樣本的個數,列數等於HOG描述子維數sampleFeatureMat
sampleFeatureMat = Mat::zeros(PosSamNO + NegSamNO + HardExampleNO, DescriptorDim, CV_32FC1);
//初始化訓練樣本的類別向量,行數等於所有樣本的個數,列數等於1;1表示有人,0表示無人
sampleLabelMat = Mat::zeros(PosSamNO + NegSamNO + HardExampleNO, 1, CV_32SC1);
}
for (int j = 0; j < DescriptorDim; j++)
sampleFeatureMat.at<float>(i - 1, j) = feature.at<float>(0, j);//第i個樣本的特徵向量中的第j個元素
sampleLabelMat.at<int>(i - 1, 0) = 1;//正樣本類別為1,是人臉
}
//依次讀取負樣本圖片,生成HOG特徵
for (int i = 1; i <= NegSamNO; i++)
{
char pic_name[64];
sprintf(pic_name, "E:\\data\\face\\neg2\\%04d.jpg", i);
// cout << pic_name << endl;
Mat src = imread(pic_name);//讀取圖片
resize(src, src, Size(64, 64));
Mat img_gray;
cvtColor(src, img_gray, CV_BGR2GRAY);
Mat feature = hog(img_gray);
for (int j = 0; j < DescriptorDim; j++)
sampleFeatureMat.at<float>(PosSamNO + i - 1, j) = feature.at<float>(0, j);//第i個樣本的特徵向量中的第j個元素
sampleLabelMat.at<int>(PosSamNO + i - 1, 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分類器
//cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
svm->setType(cv::ml::SVM::Types::C_SVC);//設定SVM型別
svm->setKernel(cv::ml::SVM::KernelTypes::LINEAR);//設定核函式
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 100, 1e-6));
// train operation
svm->train(sampleFeatureMat, cv::ml::SampleTypes::ROW_SAMPLE, sampleLabelMat);
svm->save("svm_save.xml");
}
else//若TRAIN為false,從XML檔案讀取訓練好的分類器
{
String filename = "svm_save.xml";
svm = cv::ml::StatModel::load<cv::ml::SVM>(filename);
//svm->load(filename);
}
//下面開始預測
for (int i = 1; i < 22; i++)
{
char pic_name[64];
sprintf(pic_name, "E:\\data\\face\\test\\%04d.jpg", i);
cout << pic_name << ":";
Mat src = imread(pic_name);//讀取圖片
resize(src, src, Size(64, 64));
Mat img_gray;
cvtColor(src, img_gray, CV_BGR2GRAY);
Mat feature = hog(img_gray);
float respose = svm->predict(feature);
if (respose == 1)
cout << "人臉" << endl;
else if (respose == -1)
cout << "非人臉" << endl;
}
getchar();
return 0;
}
Mat hog(const Mat &Img)
{
Mat Hog;
Hog = Mat::zeros(1, N_PHOG, CV_32FC1);
Mat Ix, Iy;
//Find orientation gradients in x and y directions
Sobel(Img, Ix, CV_16S, 1, 0, 3);
Sobel(Img, Iy, CV_16S, 0, 1, 3);
int cellx = Img.cols / N_DIVS;
int celly = Img.rows / N_DIVS;
int img_area = Img.rows * Img.cols;
for (int m = 0; m < N_DIVS; m++)
{
for (int n = 0; n < N_DIVS; n++)
{
for (int i = 0; i<cellx; i++)
{
for (int j = 0; j<celly; j++)
{
float px, py, grad, norm_grad, angle, nth_bin;
//px = Ix.at(m*cellx+i, n*celly+j);
//py = Iy.at(m*cellx+i, n*celly+j);
px = static_cast<float>(Ix.at<int16_t>((m*cellx) + i, (n*celly) + j));
py = static_cast<float>(Iy.at<int16_t>((m*cellx) + i, (n*celly) + j));
grad = static_cast<float>(std::sqrt(1.0*px*px + py*py));
norm_grad = grad / img_area;
//Orientation
angle = std::atan2(py, px);
//convert to 0 to 360 (0 to 2*pi)
if (angle < 0)
angle += 2 * CV_PI;
//find appropriate bin for angle
nth_bin = angle / BIN_RANGE;
//add magnitude of the edges in the hog matrix
Hog.at<float>(0, (m*N_DIVS + n)*N_BINS + static_cast<int>(angle)) += norm_grad;
}
}
}
}
//Normalization
for (int i = 0; i< N_DIVS*N_DIVS; i++)
{
float max = 0;
int j;
for (j = 0; j<N_BINS; j++)
{
if (Hog.at<float>(0, i*N_BINS + j) > max)
max = Hog.at<float>(0, i*N_BINS + j);
}
for (j = 0; j<N_BINS; j++)
Hog.at<float>(0, i*N_BINS + j) /= max;
}
return Hog;
}
測試集如下:
預測結果如下:
結果均正確。
說明:這只是一個簡單的例子,要想真正很好地實現人臉檢測,還需要很多改進。
參考文獻: