第一步,准备数据。

原始数据采用FDDB人脸检测测评数据集,FDDB是全世界最具权威的人脸检测评测平台之一,包含2845张图片,共有5171个人脸作为测试集。测试集范围包括:不同姿势、不同分辨率、旋转和遮挡等图片,同时包括灰度图和彩色图,标准的人脸标注区域为椭圆形。

       当然,为了简单起见,我们不直接使用这个数据集。我的做法是,自己做一些正样本和负样本数据集。正样本,即人脸,从FDDB数据集中选择一些图片,然后手动裁剪出人脸部分的图片,大概裁剪了100张。部分人脸如下图:

简单实现一个人脸检测器(HOG+SVM实现人脸检测)_#include

 

 

负样本,即非人脸,采用程序,每隔一定的间隔滑动裁剪。当然,可能会裁剪出一些人脸,这些需要手动除去。其中,裁剪图像的大小有几种,分别是64*64,100*100,128*128,144*144,160*160,总共截取了大概500张图片。部分图像结果如下:

简单实现一个人脸检测器(HOG+SVM实现人脸检测)_数据_02

 

       训练数据集,从上述的正样本中拿出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;
}

测试集如下:

 

简单实现一个人脸检测器(HOG+SVM实现人脸检测)_#define_03

预测结果如下:

简单实现一个人脸检测器(HOG+SVM实现人脸检测)_数据_04

 

结果均正确。

说明:这只是一个简单的例子,要想真正很好地实现人脸检测,还需要很多改进。

参考文献:

【OpenCV】opencv3.0中的SVM训练 mnist 手写字体识别

​自己训练SVM分类器进行HOG行人检测​