经过之前几篇博客的讲解,我们已经成功搭建了MFC应用框架,并实现了基本的图像显示和人脸检测程序,在这篇博文中我们要向其中添加性别识别代码。
关于性别识别,之前已经专门拿出两篇博客的篇幅来进行讲解,这里不再赘述,具体参见:C++开发人脸性别识别教程(5)——通过FaceRecognizer类实现性别识别和C++开发人脸性别识别教程(6)——通过SVM实现性别识别。
一、分类器训练
在进行人脸性别识别之前需要训练性别识别的分类器,而分类器的训练过程是相对耗时的(大约五分钟),因此这里我们采用离线训练在线识别的模式,即提前将分类器训练好,作为程序的数据进行保存,程序运行过程中直接加载已经训练好的分类器进行性别分类,这样速度就会大大提高。
在上面提供的两篇博客中都详细介绍了性别识别分类器的训练方法,这里一共需要训练四种分类器,分别是PCA、Fisher、LBP、SVM:
二、添加下拉列表控件
1、绘制控件
由于这里有四种性别识别的方法,因此在程序运行时,需要用户指定一种性别识别的方法,这里提供一个下拉选择列表(Combo Box)控件来供用户选择。首先从工具箱中选中该控件,在MFC主窗口的合适位置进行绘制,并将ID更改为IDC_COMBO_FUNCTION:
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变换“:
这里有两个小细节需要注意:
(1)需要提前指定Combo Box的下拉范围,这样才能保证在单击下拉按钮时控件能够将所有选项全部显示出来:
(2)Combo Box控件的”sort“属性,应该置为”false“:
三、添加性别识别算法
绘制完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、在”初始化“按钮中加载分类器
这里将分类器的加载操作安排在”初始化“按钮对应的事件响应函数OnBnClickedButtonInitial()中,即用户单击”初始化“按钮之后,程序会根据当前用户选择的方法来加载指定的分类器。由于需要根据用户当前在下拉列表中的选择情况来进行分类器的加载,因此需要下得到用户的选择的标号,然后通过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类中:
然后再向CGenderRecognitionMFCDlg类中添加一个int类型的标签,用来保存对当前图片的预测结果(“1”代表男性,“2”代表女性):
接下来开始编写性别识别函数,与之前加载分类器的流程类似,这里同样需要判断用户所选择的方法的标号,然后调用对应的分类器对输入图片进行预测,不过这里需要先将输入的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,大功告成:
四、总结
经过这篇博客之后,可以说我们的性别识别MFC程序已经基本成型,拥有了图片读取与显示,人脸检测、性别识别等基本功能,在接下来的博文中我们将介绍如何进行摄像头视频流的人脸性别识别。不过这里有几个问题需要再次强调一下。
1、分类器种类
之前我们说程序中用到了四种性别识别分类器:PCA、Fisher、LBP、SVM。其实这种说法是不严谨的,这里只是有四种API函数,而从分类器层面上将只有两种分类器。前面三个本质上都是用的K近邻分类器,只是提取了三种不同的特征而已。
2、MFC教程
在这个程序的开发过程中用到了很多MFC的相关知识,如果大家希望系统了解MFC开发的相关注意事项及技巧的话,推荐大家参考孙鑫老师的MFC视频教程。这个视频教程比较长,大家有选择性的学习即可。
3、添加初始化完成的提示对话框
这里我们向“初始化”按钮的响应函数中添加了初始化完成的提示对话框,原因是加载分类器的过程需要大约5秒左右的时间,添加一个完成提示对话框会使得程序显得更有提示性,更友好。
4、resource.h文件的功能
resource.h保存了当前资源(各种空间,图片,字符串)的ID号,必要时大家可以从这个文件中查找:
5、全局变量
程序中不推荐使用静态的全局变量,会降低程序的安全性。