C++开发人脸性别识别教程(12)——加入性别识别功能

时间:2023-03-09 15:23:02
C++开发人脸性别识别教程(12)——加入性别识别功能

  经过之前几篇博客的解说,我们已经成功搭建了MFC应用框架,并实现了主要的图像显示和人脸检測程序,在这篇博文中我们要向当中加入性别识别代码。

  关于性别识别,之前已经专门拿出两篇博客的篇幅来进行解说。这里不再赘述。详细參见:C++开发人脸性别识别教程(5)——通过FaceRecognizer类实现性别识别C++开发人脸性别识别教程(6)——通过SVM实现性别识别

  一、分类器训练

  在进行人脸性别识别之前须要训练性别识别的分类器,而分类器的训练过程是相对耗时的(大约五分钟),因此这里我们採用离线训练在线识别的模式,即提前将分类器训练好,作为程序的数据进行保存,程序执行过程中直接载入已经训练好的分类器进行性别分类,这样速度就会大大提高。

  在上面提供的两篇博客中都详细介绍了性别识别分类器的训练方法,这里一共须要训练四种分类器,各自是PCA、Fisher、LBP、SVM:

C++开发人脸性别识别教程(12)——加入性别识别功能

  二、加入下拉列表控件

  1、绘制控件

  因为这里有四种性别识别的方法,因此在程序执行时,须要用户指定一种性别识别的方法,这里提供一个下拉选择列表(Combo Box)控件来供用户选择。

首先从工具箱中选中该控件,在MFC主窗体的合适位置进行绘制。并将ID更改为IDC_COMBO_FUNCTION:

C++开发人脸性别识别教程(12)——加入性别识别功能

  2、指定选项值

  接下来须要在CGenderRecognitionMFCDlg类的OnInitDialog()初始化函数中为下拉列表设置ID标号以及相应的显示文本:

    /*********初始化Combo Box控件**********/
((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->AddString("PCA变换");
((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->AddString("Fisher变换");
((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->AddString("LBP变换");
((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->AddString("支持向量机");
((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->SetCurSel(1); //设置当前默认显示选项

  注意这里Combo Box控件的各个选项的标号是默认从“0”開始进行标号的。即这里“0”代表“PCA变换”。“1”代表“Fisher变换”。“2”代表“LBP变换”。“3”代表“支持向量机”。默认显示”Fisher变换“:

C++开发人脸性别识别教程(12)——加入性别识别功能

  这里有两个小细节须要注意:

  (1)须要提前指定Combo Box的下拉范围。这样才干保证在单击下拉button时控件能够将所有选项所有显示出来:

C++开发人脸性别识别教程(12)——加入性别识别功能

  (2)Combo Box控件的”sort“属性,应该置为”false“:

C++开发人脸性别识别教程(12)——加入性别识别功能

  三、加入性别识别算法

  绘制完ComboBox控件之后,開始向当中填入性别识别算法。

  1、全局变量声明

  在之前性别识别的博客中介绍得非常清楚。在使用OpenCv封装的分类器之前。须要声明几个静态的模板变量,我们这里将其声明为全局变量,放在GenderRecognitionMFCDlg.cpp文件的开头部分:

/************初始化性别分类器************/
static Ptr<FaceRecognizer> model_PCA = createEigenFaceRecognizer(); //PCA分类器
static Ptr<FaceRecognizer> model_Fisher = createFisherFaceRecognizer();//Fisher分类器
static Ptr<FaceRecognizer> model_LBP = createLBPHFaceRecognizer(); //LBP分类器
static CvSVM svm; //支持向量机分类器

  2、在”初始化“button中载入分类器

  这里将分类器的载入操作安排在”初始化“button相应的事件响应函数OnBnClickedButtonInitial()中,即用户单击”初始化“button之后,程序会依据当前用户选择的方法来载入指定的分类器。因为须要依据用户当前在下拉列表中的选择情况来进行分类器的载入,因此须要下得到用户的选择的标号,然后通过switch语句实现有选择的载入,代码例如以下:

    /**********依据用户的选择来载入分类器**********/
int index = 0;
index = ((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->GetCurSel();
switch (index)
{
case 0:
model_PCA->load("E:\\性别识别数据库—CAS-PEAL\\面部训练样本\\PCA_Model.xml");
break;
case 1:
model_Fisher->load("E:\\性别识别数据库—CAS-PEAL\\面部训练样本\\Fisher_Model.xml");
break;
case 2:
model_LBP->load("E:\\性别识别数据库—CAS-PEAL\\面部训练样本\\LBP_Model.xml");
break;
case 3:
svm.load("E:\\性别识别数据库—CAS-PEAL\\面部训练样本\\SVM_SEX_Model.txt");
break;
default:
break;
}

  载入完毕后,给出提示:

MessageBox("初始化完毕");

  这里给出初始化函数的完整代码:

void CGenderRecognitionMFCDlg::OnBnClickedButtonInitial()
{
m_boolInitOK = true;
cascade = cvLoadHaarClassifierCascade("D:\\opencv\\sources\\data\\haarcascades\
\\haarcascade_frontalface_alt_tree.xml",cvSize(30,30));
storage = cvCreateMemStorage(0); /**********依据用户的选择来载入分类器**********/
int index = 0;
index = ((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->GetCurSel();
switch (index)
{
case 0:
model_PCA->load("E:\\性别识别数据库—CAS-PEAL\\面部训练样本\\PCA_Model.xml");
break;
case 1:
model_Fisher->load("E:\\性别识别数据库—CAS-PEAL\\面部训练样本\\Fisher_Model.xml");
break;
case 2:
model_LBP->load("E:\\性别识别数据库—CAS-PEAL\\面部训练样本\\LBP_Model.xml");
break;
case 3:
svm.load("E:\\性别识别数据库—CAS-PEAL\\面部训练样本\\SVM_SEX_Model.txt");
break;
default:
break;
}
MessageBox("初始化完毕");
// TODO: 在此加入控件通知处理程序代码
}

  3、编写性别识别函数

  将性别识别编写为一个名为GenderRecognition(IplImage* img)的函数,将其作为成员函数加入到CGenderRecognitionMFCDlg类中:

C++开发人脸性别识别教程(12)——加入性别识别功能

  然后再向CGenderRecognitionMFCDlg类中加入一个int类型的标签,用来保存对当前图片的预測结果(“1”代表男性,“2”代表女性):

C++开发人脸性别识别教程(12)——加入性别识别功能

  接下来開始编写性别识别函数,与之前载入分类器的流程相似,这里相同须要推断用户所选择的方法的标号,然后调用相应的分类器对输入图片进行预測,只是这里须要先将输入的IplImage类型变量转换为Mat类型变量,代码例如以下:

    Mat image(img);
Mat trainImg;
resize(image,image,Size(92,112)); /***********依据当前用户选择的方法来使用相应的分类器进行分类**********/
int index = 0;
index = ((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->GetCurSel();
switch (index)
{
case 0:
{
m_genderLabel = model_PCA->predict(image);
break;
}
case 1:
{
m_genderLabel = model_Fisher->predict(image);
break;
}
case 2:
{
m_genderLabel = model_LBP->predict(image);
break;
}
case 3:
{
resize(image, trainImg, cv::Size(64,64), 0, 0, INTER_CUBIC);
HOGDescriptor *hog=new HOGDescriptor(cvSize(64,64),cvSize(16,16),cvSize(8,8),cvSize(8,8), 9);
vector<float>descriptors;
hog->compute(trainImg, descriptors,Size(1,1), Size(0,0));
Mat SVMtrainMat = Mat::zeros(1,descriptors.size(),CV_32FC1);
int n=0;
for(vector<float>::iterator iter=descriptors.begin();iter!=descriptors.end();iter++)
{
SVMtrainMat.at<float>(0,n) = *iter;
n++;
}
m_genderLabel = svm.predict(SVMtrainMat);
break;
}
default:
{
break;
}
}

  这里须要注意的一点就是在使用SVM进行性别识别时,相同须要先提取測试样本的HOG特征。參数设置要与之前训练时的HOG參数设置相同,详细參见:C++开发人脸性别识别教程(6)——通过SVM实现性别识别

同一时候要将測试样本先归一化到和训练样本相同的尺寸,这里为92*112。

  4、显示识别结果

  我们设计通过一个编辑框控件(Edit Control)来显示当前图片的性别识别结果,即m_genderRecognition为“1”时显示“帅哥”。为“2”时显示“美女”。首先在主界面上绘制这个控件,并将其ID指定为IDC_EDIT_RecognitionResult。

  然后我们在GenderRecognition()函数中加入结果显示代码:

    /**********显示识别结果**********/
if (1 == m_genderLabel)
{
GetDlgItem(IDC_EDIT_RESULT)->SetWindowText("帅哥");
}
else if(2 == m_genderLabel)
{
GetDlgItem(IDC_EDIT_RESULT)->SetWindowText("美女");
}

  此时性别识别函数编写完毕,这里给出该函数的总体代码:

void CGenderRecognitionMFCDlg::GenderRecognition(IplImage* img)
{
Mat image(img);
Mat trainImg;
resize(image,image,Size(92,112)); /***********依据当前用户选择的方法来使用相应的分类器进行分类**********/
int index = 0;
index = ((CComboBox*)GetDlgItem(IDC_COMBO_FUNCTION))->GetCurSel();
switch (index)
{
case 0:
{
m_genderLabel = model_PCA->predict(image);
break;
}
case 1:
{
m_genderLabel = model_Fisher->predict(image);
break;
}
case 2:
{
m_genderLabel = model_LBP->predict(image);
break;
}
case 3:
{
resize(image, trainImg, cv::Size(64,64), 0, 0, INTER_CUBIC);
HOGDescriptor *hog=new HOGDescriptor(cvSize(64,64),cvSize(16,16),cvSize(8,8),cvSize(8,8), 9);
vector<float>descriptors;
hog->compute(trainImg, descriptors,Size(1,1), Size(0,0));
Mat SVMtrainMat = Mat::zeros(1,descriptors.size(),CV_32FC1);
int n=0;
for(vector<float>::iterator iter=descriptors.begin();iter!=descriptors.end();iter++)
{
SVMtrainMat.at<float>(0,n) = *iter;
n++;
}
m_genderLabel = svm.predict(SVMtrainMat);
break;
}
default:
{
break;
}
} /**********显示识别结果**********/
if (1 == m_genderLabel)
{
GetDlgItem(IDC_EDIT_RESULT)->SetWindowText("帅哥");
}
else if(2 == m_genderLabel)
{
GetDlgItem(IDC_EDIT_RESULT)->SetWindowText("美女");
}
}

  四、调用性别识别函数

  编写完性别识别函数之后,我们就能够准备调用这个函数来进行性别识别了,因为程序的设计是先进行人脸检測,然后进行性别识别。因此我们准备在人脸检測函数detect_and_draw()中调用这个性别识别函数。

  1、人脸区域切割

  显然,在进行人脸检測之后,我们须要将检測到的人脸区域切割出来,再送入GenderRecognition()性别识别函数中进行识别。因此我们须要向detect_and_draw()函数中加入人脸区域切割的代码。

  首先,分析一下detect_and_draw(IplImage* img)函数中现有变量的含义:

  IplImage* img:为输入的原始图像。须要在这个原始图像上进行人脸区域切割;

  IplImage* gray:为灰度化的图像,但gray经过了直方图均衡化的操作,导致其丢失了原始的性别信息,因此无法用其进行性别识别,这也就意味着我们须要又一次对原始图像img进行灰度化操作。然后进行切割;

  CvRect* rect:保存了人脸检測的结果,须要依据这个矩形的位置和 尺寸来进行人脸区域切割。

  OK。经过以上分析,我们给出人脸区域切割的代码:

    /**********切割人脸区域**********/
cvSetImageROI(img,*rect); //设置图像人脸部分ROI区域
IplImage* faceImage = cvCreateImage(cvSize(rect->width,rect->width),IPL_DEPTH_8U,1);
if (img->nChannels = 3)
{
cvCvtColor(img,faceImage, CV_BGR2GRAY);//将图像灰度化存放在gray中
}
else
{
faceImage = img;
}
cvResetImageROI(img); /**********性别识别**********/
GenderRecognition(faceImage);
cvReleaseImage(&faceImage);

  这里在进行区域切割时採用了设置ROI区域的方法。这是OpenCv1.x中的方法,在2.x中的Mat类型中封装了更为简洁的方法,详见OpenCV中ROI 总结

  考虑到在进行人脸检測时会出现检測失败的情况,假设我们在人脸检測失败的情况下仍坚持启用人脸切割及性别识别程序,程序就会因为各种变量的没有定义而崩溃,因此我们这里选择将这段人脸切割、性别识别的代码放在if语句中。保证其仅仅有在人脸检測成功的情况下才执行,为了方便大家理清逻辑。这里给出detect_and_draw()函数改动后的总体代码:

void CGenderRecognitionMFCDlg::detect_and_draw(IplImage* img)
{
/**********初始化**********/
IplImage* gray = cvCreateImage(cvSize(img->width,img->height),8,1); /**********灰度化**********/
if (img->nChannels = 3)
{
cvCvtColor(img,gray, CV_BGR2GRAY);//将图像灰度化存放在gray中
}
else
{
gray = img;
} /**********直方图均衡**********/
cvEqualizeHist(gray,gray); /**********人脸检測**********/
cvClearMemStorage(storage);
CvSeq* objects = cvHaarDetectObjects(gray,//待检測图像
cascade, //分类器标识
storage, //存储检測到的候选矩形
1.3, //相邻两次检測中窗体扩大的比例
3, //觉得是人脸的最小矩形数(阈值)
0, //CV_HAAR_DO_CANNY_PRUNING
cvSize(30,30)); //初始检測窗体大小 /**********对检測出的人脸区域面积做比較,选取当中的最大矩形**********/
int maxface_label = 0; //最大面积人脸标签
Mat max_face = Mat::zeros(objects->elem_size,1,CV_32FC1); //候选矩形面积
for(int i = 0;i< objects->total;i++)
{
CvRect* r = (CvRect*)cvGetSeqElem(objects,i);
max_face.at<float>(i,0) = (float)(r->height * r->width);
if(i > 0&&max_face.at<float>(i,0) > max_face.at<float>(i - 1,0))
{
maxface_label = i;
} } /**********绘制检測结果**********/
if(objects->total > 0) //假设人脸检測成功
{
CvRect* rect = (CvRect*)cvGetSeqElem(objects,maxface_label);
cvRectangle(img,cvPoint(rect->x,rect->y),
cvPoint(rect->x + rect->width,rect->y + rect->height),cvScalar(0.0,255));   /**********切割人脸区域**********/
  cvSetImageROI(img,*rect); //设置图像人脸部分ROI区域
  IplImage* faceImage = cvCreateImage(cvSize(rect->width,rect->width),IPL_DEPTH_8U,1);
  if (img->nChannels = 3)
  {
   cvCvtColor(img,faceImage, CV_BGR2GRAY);//将图像灰度化存放在gray中
  }
  else
  {
   faceImage = img;
   }
  cvResetImageROI(img);    /**********性别识别**********/
   GenderRecognition(faceImage);
   cvReleaseImage(&faceImage);
} /**********在图像控件上显示图像**********/
CvvImage cvvImage;
cvvImage.CopyOf(img);
cvvImage.DrawToHDC(m_pPicCtlHdc,m_PicCtlRect);
cvReleaseImage(&gray);
}

  OK,大功告成:

C++开发人脸性别识别教程(12)——加入性别识别功能

  四、总结

  经过这篇博客之后,能够说我们的性别识别MFC程序已经基本成型。拥有了图片读取与显示,人脸检測、性别识别等基本功能,在接下来的博文中我们将介绍怎样进行摄像头视频流的人脸性别识别。只是这里有几个问题须要再次强调一下。

  1、分类器种类

  之前我们说程序中用到了四种性别识别分类器:PCA、Fisher、LBP、SVM。事实上这样的说法是不严谨的,这里仅仅是有四种API函数,而从分类器层面上将仅仅有两种分类器。

前面三个本质上都是用的K近邻分类器,仅仅是提取了三种不同的特征而已。

  2、MFC教程

  在这个程序的开发过程中用到了非常多MFC的相关知识,假设大家希望系统了解MFC开发的相关注意事项及技巧的话。推荐大家參考孙鑫老师的MFC视频教程。

这个视频教程比較长。大家有选择性的学习就可以。

  3、加入初始化完毕的提示对话框

  这里我们向“初始化”button的响应函数中加入了初始化完毕的提示对话框,原因是载入分类器的过程须要大约5秒左右的时间,加入一个完毕提示对话框会使得程序显得更有提示性,更友好。

  4、resource.h文件的功能

  resource.h保存了当前资源(各种空间。图片,字符串)的ID号,必要时大家能够从这个文件里查找:

C++开发人脸性别识别教程(12)——加入性别识别功能

  5、全局变量

  程序中不推荐使用静态的全局变量,会减少程序的安全性。