(14)——灰度图像识别BUG处理

最后更新于:2022-04-01 20:14:38

  在这篇博客中,我们对目前程序中一个隐藏很深的BUG进行处理,这个BUG导致程序目前有一部分逻辑出现错误(虽然没有表现出来)。   一、触发BUG   1、准备触发样本   为了复现这个隐藏的BUG,需要实现准备两张测试样本,一张是彩色图(三通道图),一张是灰度图(单通道图): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638b25bab.png)   临时读入这两个图像,验证其属性: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638b3fa3f.png)   注意此时程序能够正常读取这两个图片,不会崩溃。   2、修改代码,触发BUG   解析来我们修改“图片文件夹”按钮对应事件响应函数中图像读取的代码,这里由于这是为了复现BUG,只修改一个模式下的读取函数即可,假设我们修改“图片文件”模式下的读取函数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638b52e4a.png)   我们这里就是修改了cvLoadImage()的在读取图片时的标志位,“-1”代表原始读取方式,即在读取时不会改变图像的任何属性(如通道数,位数等),这种更改看上去是很合理的,因为我们应该保证加载到的图像就是图像本身,在加载图像的过程中对图像进行预处理本身是不合理的,因为我们会编写专门的图像预处理代码。   此时,在“图片文件”模式下读取彩色图像进行性别识别,一切正常。然后我们在“图片文件”模式下读取灰度图(单通道图),程序崩溃了: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638b654cb.png)   接下来开始追踪定位这个BUG。   二、定位第一个BUG   出现这个错误之后,首先判断是否在图像加载的过程中出了问题。在cvLoadImage()函数处添加一个断点,F5调试运行: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638b76011.png)   程序运行到断点处后,按下F10,运行断点对应的程序语句,发现图片加载正常: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638b86cf7.png)   继续按F10,运行下面的人脸检测函数(detect_and_draw),程序崩溃,因此可以确定BUG出在detect_and_draw函数中。   detect_and_draw函数中,最复杂的操作莫过于是人脸检测函数cvHaarDetectObjects了,因此这里出现BUG的可能性也非常大,在这里设置一个断点,F5运行,程序崩溃,说明BUG在这条语句之前。   此时我们有理由怀疑是直方图均衡函数cvEqualizeHist()出了问题,在这个语句前面打一个断点,F5运行,程序依然崩溃,说明前面还有错误。   此时detect_and_draw()函数还剩三行代码需要检测,不妨将断点设在函数开始的位置,F5运行,程序正常进入函数体: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638b964df.png)   按F10单步运行,意外发现程序准备执行cvCvtColor()语句,这条语句是对图像进行灰度化处理,但很显然我们选择的图片本身就是灰度图,也就是单通道图,对灰度图进行灰度化,程序自然崩溃,第一个BUG算是找到了: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638ba93a5.png)   至于这个BUG出现的原因,想必大家此时都已经看出来了,由于我之前粗心,将if的判断条件写成了赋值语句if (img->nChannels = 3),导致这个条件永远为真,以至于不管是否为彩色图,都会执行if内的语句,程序崩溃,气人的是当输入图像本身就是彩色图时,这个BUG并不会显现,隐藏很深。   三、定位第二个BUG   修改if条件为“if (img->nChannels == 3)”,运行程序,选择灰度图像进行性别识别,程序再次崩溃。   通过之前的方法我们很快确定BUG依然出在detect_and_draw函数中,同样我们先检查其中的cvHaarDetectObjects()函数,经过“设置断点—>F5调试—>F10单步运行”的调试方式,很快确定这个函数能够正常运行: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638bbdcb4.png)   接下来我们有理由推测是性别识别函数GenderRecognition()出现问题,在这里打上断点,F5运行,程序崩溃,说明问题在前面。   此时我们能够确定BUG出在cvHaarDetectObjects()函数和GenderRecognition()函数之间,这部分包含两段代码,一段代码是用来统计人脸检测结果中面积最大的矩形标号,另一段代码是用来绘制矩形检测结果。将断点设在“if(objects->total > 0)”处,F5调试运行程序,程序能够正常命中断点,说明前面的代码都没问题。   F10单步运行断点之后的代码,在这里又发现一处if (img->nChannels = 3)的错误,赶紧将其改正。   再次F5运行程序,命中断点,F10单步运行,发现程序居然顺利通过GenderRecognition()函数,似乎程序BUG已经消失,继续F10单步运行,程序在“ cvReleaseImage(&gray);”语句处发生错误,很明显句代码有问题。   四、终极BUG的发现   首先来解决“cvReleaseImage(&gray);”这句代码的BUG。这是IplImage结构体所特有的释放内存的语句,IplImage类型的变量在生命周期结束之后需要通过这句代码来显示的释放掉内存,而OpenCv2.x中的Mat类型则不需要这部操作(封装了智能指针),所以从这点来讲Mat类还是要优于IplImage结构体的。如果这句代码出现问题,很大一部分原因就是待释放的对象不存在(或者说是已经释放过),我们在这行代码处设置一个断点,F5运行程序,果然,变量gray已经被清空: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638bd3102.png)   那我们是在什么时候不经意间把这个IplImage变量释放掉的呢?最值得怀疑的就是上面的那句“cvReleaseImage(&faceImage);”代码,很可能是这句代码在释放faceImage变量时连同gray变量也一起释放掉了,我们将断点放在这行代码上,验证我们的猜想,从下图中可以看到,在执行这句代码之前,faceImage和gray两个变量对应的图像均不为空: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638be7f09.png)   程序运行到这里细心的朋友应该能够发现问题了,那就是faceImage所存储的图片居然和img变量所对应的图片是一样的!这显然是不合道理的,因为理论上faceImage中保存的应该是分割出来的人脸图像,然后把这个图像送入GenderRecognition(faceImage)函数中进行性别识别,可现在的情况却是faceImage保存了原始图片,并且是经过直方图均衡化的原始图片,也就意味着此时进行性别识别的是全部图像而非人脸部分,这种情况下的性别识别毫无道理可言,这就是所谓的终极BUG。   五、修改   首先,我们分析这个BUG出现的原因,理论情况下是通过检测到的人脸矩形,在原图(img)的基础上进行人脸区域分割,将分割得到的人脸赋值给faceImage变量,很明显这部分代码现在出了问题: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638c0c8cf.png)   为什么会出问题呢?仔细分析这段代码,不难看出,如果当前图片是彩色图片,则“img->nChannels == 3”条件为真,执行cvCvtColor()函数,且我们已经为img图像变量指定了ROI区域,因此在灰度化操作中会只对ROI区域的图像进行操作,因此这步灰度化操作也间接的完成了ROI区域(也就是人脸区域)的分割;而目前的图片是灰度图片,“img->nChannels == 3”条件为假,执行“faceImage = img;”语句,问题是这种直接用等号连接的赋值语句属于“弱拷贝”,也就是只拷贝指针指向地址,但两个变量仍共享一段内存区域,通俗的将就是“faceImage ”和“img”这两个变量本质上都代表了同一个内存中的图像,这也就解释了为什么我们在释放完faceImage之后,连同img、gray这两个变量也一起释放掉了,因为它们三个之间都是通过这种“弱拷贝”的关系联系在了一起,都代表着同一图像。   更严重的问题就是在进行“弱拷贝”的过程中,ROI区域是无效的,因此在这种情况下人脸分割失败。   很明显,消除这种BUG的最有效的方法就是将“弱拷贝”替换为深度拷贝,即两个变量代表两个图像,指向两个不同的内存地址,这样就不会因为位置的连带关系而引发莫名的BUG。IplImage对应的深度拷贝函数有两个:void cvCopy( const CvArr* src, CvArr* dst, const CvArr* mask=NULL )和IplImage* cvCloneImage( const IplImage* image )。其中cvCopy在拷贝过程中只拷贝设置的ROI区域,正好符合我们的需求,因此在这里采用这个函数来替换之前的“浅拷贝”: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638c23174.png)   以及: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638c3a20d.png)   此时再次运行程序,发现faceImage中正常保存的分割后的人脸图像: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638c4b2c8.png)   至此,这个重大BUG修改完成。   六、总结   在这篇博文中讲述了一个发现BUG、定位BUG、分析BUG、解决BUG的完整过程,希望对大家有所帮助。在下一篇博客中,开始引入摄像头。
';

(13)——针对单张图片的性别识别

最后更新于:2022-04-01 20:14:36

  在之前的博文中我们的性别识别程序已经初步成型,能够识别某个文件夹下的图片文件。不过这里有一个问题,假设这个文件夹下有着大量的图片,而我们希望识别这些图片中的某一张,此时需要我们不停的单击“下一张”按钮才会轮询到对应的图片,这是相当麻烦的,因此在这篇博客中我们向程序中添加一个功能——单张图片的性别识别。   一、基本思想   最基本的办法就是在主界面再添加一个按钮控件,命名为“图片文件”(之前的按钮为“图片文件夹”),不过这样会使得界面上的按钮控件过于繁多,给人一种“作者只会用button控件”的感觉。这里我们决定用一种相对巧妙的方式来解决这个问题,即向之前的“图片文件夹”button控件在添加一个读取单张图片的功能,然后再通过某一操作来进行两个功能间的切换,这里我们使用鼠标双击的操作。所以最终的效果就是:我们双击一下鼠标,就会从“图片文件夹”模式转换到“单张图片”模式(或者从“单张图片”模式转换为“图片文件夹”模式)。   二、添加鼠标双击的响应事件   MFC程序是基于消息响应机制的,关于这点一两句话也说不清楚,推荐大家去看孙鑫老师的MFC教学视频。在这里我们需要通过双击鼠标左键来触发一个消息,在消息响应函数中进行图片读取的模式反转。首先,我们向类中添加双击鼠标左键的事件响应函数。在类视图窗口中,右击CGenderRecognitionMFCDlg类,选择属性: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638aa4f40.png)   在打开的属性对话框中,单击“消息”图标,在消息列表中找到WM_LBUTTONDBLCLK消息,单击右侧的下拉按钮,选择“add OnLButtonDblClk”命令: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638ab7f27.png)   此时,鼠标的双击消息响应函数添加完成: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638ac6ec0.png)   三、编写双击消息响应函数   1、添加模式标记   接下来我们开始编写鼠标双击后对应的执行代码OnLButtonDblClk()。首先,我们需要一个布尔变量来记录当前处于何种模式(是“图片文件夹”模式还是“单张图片”模式),这里有两个选择,一是将这个变量定义为全局布尔变量,二是定义为类的成员变量,首选成员变量。因此需要向CGenderRecognitionMFCDlg添加一个bool变量m_boolFolderOrImage来进行标记: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638ad7a24.png)   2、状态转换,更新按钮   接下来进行模式反转,同时在反转后更改按钮所显示的文本用以提示用户当前程序的工作状态,完成后OnLButtonDblClk()函数的代码如下: ~~~ void CGenderRecognitionMFCDlg::OnLButtonDblClk(UINT nFlags, CPoint point) { // TODO: 在此添加消息处理程序代码和/或调用默认值 m_boolFolderOrImage = !m_boolFolderOrImage; if(m_boolFolderOrImage == FALSE) { SetDlgItemTextA(IDC_BUTTON_ImageFile,"图片文件夹"); } else if(m_boolFolderOrImage == TRUE) { SetDlgItemTextA(IDC_BUTTON_ImageFile,"图片文件"); } CDialogEx::OnLButtonDblClk(nFlags, point); } ~~~   OK,调试运行,在主界面的任意位置双击鼠标左键,会发现按钮控件会在“图片文件夹”和“图片文件”两种模式之间进行切换。   四、改写OnBnClickedButtonImagefile函数   OnBnClickedButtonImagefile()函数是“图片文件夹”(或者是“图片文件”)按钮的控件响应函数,由于我们对按钮控件添加了新的功能,理所应当需要对其控件响应函数进行扩充。   1、“打开文件”代码片   这里首先编写打开单个文件的代码,MFC中打开文件需要使用CFileDialog这个类,这个类使用起来也非常简单: ~~~ CFileDialog FDlg(TRUE); if(FDlg.DoModal() == IDOK) { m_Path = FDlg.GetPathName(); UpdateData(false); } ~~~   m_Path变量中保存了当前选中文件的全路径。   2、OnBnClickedButtonImagefile()函数   接下来需要对已有的OnBnClickedButtonImagefile()函数体的结构进行改造,即需要先判断m_boolFolderOrImage标志位,如果其为假,则为“图片文件夹”模式,执行文件夹批量读取代码(SHBrowseForFolder方法);若为真,则为“图片文件”模式,执行单张图片的读取代码(CFileDialog)方法。这里给出更改后的OnBnClickedButtonImagefile()函数的整体代码: ~~~ void CGenderRecognitionMFCDlg::OnBnClickedButtonImagefile() { /**********是否已经进行了初始化操作**********/ if (m_boolInitOK == false) { MessageBox("请先进行初始化"); return; } if (!m_boolFolderOrImage) { /**********初始化变量**********/ CString str; //存储图像路径 BROWSEINFO bi; //用来存储用户选中的目录信息 TCHAR name[MAX_PATH]; //存储路径 ZeroMemory(&bi,sizeof(BROWSEINFO)); //清空目录对应的内存 bi.hwndOwner = GetSafeHwnd(); //得到窗口句柄 bi.pszDisplayName = name; /**********设置对话框并读取目录信息**********/ BIF_BROWSEINCLUDEFILES; bi.lpszTitle = _T("Select folder"); //对话框标题 bi.ulFlags = 0x80; //设置对话框形式 LPITEMIDLIST idl = SHBrowseForFolder(&bi); //返回所选中文件夹的ID SHGetPathFromIDList(idl,str.GetBuffer(MAX_PATH)); //将文件信息格式化存储到对应缓冲区中 str.ReleaseBuffer(); //与GerBuffer配合使用,清空内存 m_Path = str; //将路径存储在m_path中 if(str.GetAt(str.GetLength()-1)!='\\') m_Path += "\\"; UpdateData(FALSE); IMalloc * imalloc = 0; if (SUCCEEDED(SHGetMalloc(&imalloc))) { imalloc->Free (idl); imalloc->Release(); } /**********获取该路径下的第一个文件**********/ m_ImageDir = (LPSTR)(LPCTSTR)m_Path; m_pDir = opendir(m_ImageDir); for (int i = 0; i < 1; i ++) //过滤目录 .. 和 . { m_pEnt = readdir(m_pDir); } /**********启动图像显示程序**********/ GetNextBigImg(); } else { /**********通过打开文件对话框来获得目标文件的路径**********/ CFileDialog FDlg(TRUE); if(FDlg.DoModal() == IDOK) { m_Path = FDlg.GetPathName(); UpdateData(false); } /**********判断是否为图像文件**********/ char* jpg = strstr((LPSTR)(LPCTSTR)m_Path,".jpg"); char* bmp = strstr((LPSTR)(LPCTSTR)m_Path,".bmp"); char* png = strstr((LPSTR)(LPCTSTR)m_Path,".png"); if (jpg == NULL && bmp == NULL && png == NULL) //如果该文件不是图像文件 { MessageBox("这不是一个图像文件"); return; } /**********人脸检测过程**********/ IplImage* src; CvvImage srcCvvImg; src = cvLoadImage(m_Path); detect_and_draw(src); /**********绘制图像到控件**********/ srcCvvImg.CopyOf(src); srcCvvImg.DrawToHDC(m_pPicCtlHdc,&m_PicCtlRect); cvReleaseImage(&src); srcCvvImg.Destroy(); } // TODO: 在此添加控件通知处理程序代码 } ~~~   这里用几个问题需要强调:   (1)文件属性判断。在之前文件夹工作模式下,程序会在GetNextBigImg()函数中自动判断文件属性,过滤非图像文件。而在直接读取具体文件时,无法保证用户选择的是一个图像文件,如果程序因为用户文件选择失误而崩溃,明显是不合理的,因此在这里添加了文件属性判断,并当用户选择了一个非图像文件时程序会给出友好提示并安全返回。   (2)与之前文件夹读取模式的另外一个不同点就是这里将图像的加载和检测识别操作直接放在了OnBnClickedButtonImagefile()函数中,因为这里无需再进行文件轮询的操作,况且我们已经将性别识别程序封装在了人脸检测函数中,这样写也并不会使代码显得有多乱。   OK,此时运行程序,如预期结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638b017e5.png)   五、总结   在写这篇博文所对应的程序中,我发现了一个非常严重,同时隐藏的也非常深的BUG,这个BUG直接导致我们的程序的一部分逻辑出现错误,确切的说是有一部分代码被架空,更为严重的是这个BUG并不会导致程序运行的问题,很难被发现,在此先向各位关注《C++开发人脸性别识别教程》系列博客的读者表示歉意,我会在下一篇博文中专门对这个BUG进行更正,同时对界面进行一点小小的美化。
';

(12)——添加性别识别功能

最后更新于:2022-04-01 20:14:33

  经过之前几篇博客的讲解,我们已经成功搭建了MFC应用框架,并实现了基本的图像显示和人脸检测程序,在这篇博文中我们要向其中添加性别识别代码。   关于性别识别,之前已经专门拿出两篇博客的篇幅来进行讲解,这里不再赘述,具体参见:[C++开发人脸性别识别教程(5)——通过FaceRecognizer类实现性别识别](http://blog.csdn.net/u013088062/article/details/50458810)和[C++开发人脸性别识别教程(6)——通过SVM实现性别识别](http://blog.csdn.net/u013088062/article/details/50480518)。   一、分类器训练   在进行人脸性别识别之前需要训练性别识别的分类器,而分类器的训练过程是相对耗时的(大约五分钟),因此这里我们采用离线训练在线识别的模式,即提前将分类器训练好,作为程序的数据进行保存,程序运行过程中直接加载已经训练好的分类器进行性别分类,这样速度就会大大提高。   在上面提供的两篇博客中都详细介绍了性别识别分类器的训练方法,这里一共需要训练四种分类器,分别是PCA、Fisher、LBP、SVM: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6389f0479.png)   二、添加下拉列表控件   1、绘制控件   由于这里有四种性别识别的方法,因此在程序运行时,需要用户指定一种性别识别的方法,这里提供一个下拉选择列表(Combo Box)控件来供用户选择。首先从工具箱中选中该控件,在MFC主窗口的合适位置进行绘制,并将ID更改为IDC_COMBO_FUNCTION:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638a1a397.png)   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变换“: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638a2af88.png)   这里有两个小细节需要注意:   (1)需要提前指定Combo Box的下拉范围,这样才能保证在单击下拉按钮时控件能够将所有选项全部显示出来: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638a3da31.png)   (2)Combo Box控件的”sort“属性,应该置为”false“: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638a4bf4a.png)   三、添加性别识别算法   绘制完ComboBox控件之后,开始向其中填入性别识别算法。   1、全局变量声明   在之前性别识别的博客中介绍得很清楚,在使用OpenCv封装的分类器之前,需要声明几个静态的模板变量,我们这里将其声明为全局变量,放在GenderRecognitionMFCDlg.cpp文件的开头部分: ~~~ /************初始化性别分类器************/ static Ptr model_PCA = createEigenFaceRecognizer(); //PCA分类器 static Ptr model_Fisher = createFisherFaceRecognizer();//Fisher分类器 static Ptr 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类中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638a5e5bb.png)   然后再向CGenderRecognitionMFCDlg类中添加一个int类型的标签,用来保存对当前图片的预测结果(“1”代表男性,“2”代表女性): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638a74d8b.png)   接下来开始编写性别识别函数,与之前加载分类器的流程类似,这里同样需要判断用户所选择的方法的标号,然后调用对应的分类器对输入图片进行预测,不过这里需要先将输入的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); vectordescriptors; hog->compute(trainImg, descriptors,Size(1,1), Size(0,0)); Mat SVMtrainMat = Mat::zeros(1,descriptors.size(),CV_32FC1); int n=0; for(vector::iterator iter=descriptors.begin();iter!=descriptors.end();iter++) { SVMtrainMat.at(0,n) = *iter; n++; } m_genderLabel = svm.predict(SVMtrainMat); break; } default: { break; } } ~~~   这里需要注意的一点就是在使用SVM进行性别识别时,同样需要先提取测试样本的HOG特征,参数设置要与之前训练时的HOG参数设置相同,具体参见:[C++开发人脸性别识别教程(6)——通过SVM实现性别识别](http://blog.csdn.net/u013088062/article/details/50480518)。同时要将测试样本先归一化到和训练样本相同的尺寸,这里为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); vectordescriptors; hog->compute(trainImg, descriptors,Size(1,1), Size(0,0)); Mat SVMtrainMat = Mat::zeros(1,descriptors.size(),CV_32FC1); int n=0; for(vector::iterator iter=descriptors.begin();iter!=descriptors.end();iter++) { SVMtrainMat.at(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 总结](http://blog.csdn.net/qianqing13579/article/details/45250823)。   考虑到在进行人脸检测时会出现检测失败的情况,如果我们在人脸检测失败的情况下仍坚持启用人脸分割及性别识别程序,程序就会因为各种变量的未定义而崩溃,因此我们这里选择将这段人脸分割、性别识别的代码放在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(i,0) = (float)(r->height * r->width); if(i > 0&&max_face.at(i,0) > max_face.at(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,大功告成: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6388b3fdb.png)   四、总结   经过这篇博客之后,可以说我们的性别识别MFC程序已经基本成型,拥有了图片读取与显示,人脸检测、性别识别等基本功能,在接下来的博文中我们将介绍如何进行摄像头视频流的人脸性别识别。不过这里有几个问题需要再次强调一下。   1、分类器种类   之前我们说程序中用到了四种性别识别分类器:PCA、Fisher、LBP、SVM。其实这种说法是不严谨的,这里只是有四种API函数,而从分类器层面上将只有两种分类器。前面三个本质上都是用的K近邻分类器,只是提取了三种不同的特征而已。   2、MFC教程   在这个程序的开发过程中用到了很多MFC的相关知识,如果大家希望系统了解MFC开发的相关注意事项及技巧的话,推荐大家参考孙鑫老师的MFC视频教程。这个视频教程比较长,大家有选择性的学习即可。   3、添加初始化完成的提示对话框   这里我们向“初始化”按钮的响应函数中添加了初始化完成的提示对话框,原因是加载分类器的过程需要大约5秒左右的时间,添加一个完成提示对话框会使得程序显得更有提示性,更友好。   4、resource.h文件的功能   resource.h保存了当前资源(各种空间,图片,字符串)的ID号,必要时大家可以从这个文件中查找: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638a8ebd0.png)   5、全局变量   程序中不推荐使用静态的全局变量,会降低程序的安全性。
';

(11)——图片人脸检测程序BUG处理

最后更新于:2022-04-01 20:14:31

  在这篇博客需要解决之前遗留的两个BUG,一是用户在不初始化条件下运行程序,二是人脸检测的误差结果。   一、添加初始化警告   目前我们在“初始化”按钮对应的响应函数中封装了人脸分类器加载、开辟内存等操作: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6389859f0.png)   因此,如果用户在未单击“初始化”按钮的情况下进行图片读入,人脸检测,程序就会因为缺少人脸检测器而崩溃,因此我们向CGenderRecognitionMFCDlg类中添加一个布尔类型的标志位用于指示当前用户是否完成了初始化操作: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63899975d.png)   并在CGenderRecognitionMFCDlg类的构造函数中将其初始化为false(正常情况下VS会自动完整这个操作): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6389abc90.png)   接下来,在“初始化”按钮对应的响应函数中,将这个标志位置为true,代表初始化已经完成: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6389bb573.png)   接下来在“打开文件夹”按钮对应的响应函数的开头,添加标志位判断代码: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6389cb120.png)   此时,如果用户在未初始化时点击了“图像文件夹”按钮,程序会弹出对话框,提示用户先进行初始化操作: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6389dcc54.png)   二、人脸检测优化   在之前的程序中,人脸检测所返回的矩形不止一个,也就意味着存在检测误差。这里我们添加人脸检测的结果筛选代码,即根据检测结果矩形的面积进行筛选,只保留最大面积的矩形作为人脸检测的结果。这里需要对成员函数detect_and_draw()进行一些修改。   1、计算矩形面积   在人脸检测完成后,轮询检测结果序列,计算矩形面积,并保留面积最大的矩形标号: ~~~ /**********对检测出的人脸区域面积做比较,选取其中的最大矩形**********/ 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(i,0) = (float)(r->height * r->width); if(i > 0&&max_face.at(i,0) > max_face.at(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)); } ~~~   大功告成。这里由于对detect_and_draw()函数做了较大修改,因此在此给出修改后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(i,0) = (float)(r->height * r->width); if(i > 0&&max_face.at(i,0) > max_face.at(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)); } /**********在图像控件上显示图像**********/ CvvImage cvvImage; cvvImage.CopyOf(img); cvvImage.DrawToHDC(m_pPicCtlHdc,m_PicCtlRect); cvReleaseImage(&gray); } ~~~         
';

(10)——添加图片的人脸检测程序

最后更新于:2022-04-01 20:14:29

  如今我们的MFC框架已经初具规模,能够读取并显示文件夹下的图片,在这篇博文中我们将向其中添加人脸检测的程序。   一、人脸检测算法   这里我们使用OpenCv封装的Adaboost方法来进行人脸检测,参见:[C++开发人脸性别识别教程(4)——OpenCv的人脸检测函数](http://blog.csdn.net/u013088062/article/details/50439630)   二、初始化   1、添加初始化按钮   在进行人脸检测之前需要初始化一些相关变量,例如开辟内存,加载检测器等等。首先,我们为MFC框架添加一个初始化按钮,并将ID更改为IDC_BUTTON_INITIAL: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6388ef242.png)   双击这个按钮,添加事件响应函数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63890b684.png)   2、初始化变量   从之前的博客中可知,OpenCv在进行人脸检测时需要用到两个静态变量:static CvMemStorage* storage和static CvHaarClassifierCascade* cascade,这里我们将其作为成员变量添加到CGenderRecognitionMFCDlg类中,这里由于static CvMemStorage*和static CvHaarClassifierCascade*这两个类型名在MFC类向导中是无法被识别的,因此需要手动添加至CGenderRecognitionMFCDlg类的构造函数中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63891b1b7.png)   接下里对这两个惊天变量进行初始化。C++明确规定静态成员变量要在类外进行初始化,而不能在类内声明时或者构造函数内进行初始化,原因就是静态变量时属于类本身的,而非类对象的属性,和全局变量类似,因此我们将这两个静态变量的初始化操作放在GenderRecognitionMFCDlg.cpp文件(从解决方案资源管理器窗口中查找cpp文件)的开头位置: ~~~ // 用于应用程序“关于”菜单项的 CAboutDlg 对话框 CvMemStorage* CGenderRecognitionMFCDlg::storage = NULL; CvHaarClassifierCascade* CGenderRecognitionMFCDlg::cascade = NULL; ~~~   然后在“初始化”按钮的响应函数OnBnClickedButtonInitial()中加载对应的人脸检测器: ~~~ void CGenderRecognitionMFCDlg::OnBnClickedButtonInitial() { cascade = cvLoadHaarClassifierCascade("D:\\opencv\\sources\\data\\haarcascades \\haarcascade_frontalface_alt_tree.xml",cvSize(30,30)); storage = cvCreateMemStorage(0); // TODO: 在此添加控件通知处理程序代码 } ~~~   初始化完成。   三、编写人脸检测函数     这里将人脸检测的操作封装成一个函数detect_and_draw(),作为成员函数添加到CGenderRecognitionMFCDlg类中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63892ea3b.png)    在类视图中找到detect_and_draw()函数,完善其人脸检测代码,由于之前已经详细介绍过人脸检测的相关操作,这里直接给出代码: ~~~ void CGenderRecognitionMFCDlg::detect_and_draw(IplImage* img) { /**********初始化**********/ double scale = 1.2; 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)); //初始检测窗口大小 /**********绘制检测结果**********/ if(objects->total > 0) //如果人脸检测成功 { for (int i = 0; i < (objects ? objects->total : 0); i++) { CvRect* rect = (CvRect*)cvGetSeqElem(objects,i); cvRectangle(img,cvPoint(rect->x,rect->y), cvPoint(rect->x + rect->width,rect->y + rect->height),cvScalar(0.0,255)); } } /**********在图像控件上显示图像**********/ CvvImage cvvImage; cvvImage.CopyOf(img); cvvImage.DrawToHDC(m_pPicCtlHdc,m_PicCtlRect); cvReleaseImage(&gray); } ~~~   注意这里相对于之前的程序,添加了一项直方图均衡化的操作,以提高人脸检测的成功率: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63893f93a.png)    四、调用人脸检测函数   理论上在显示图像之前应该自动调用人脸检测操作,因此在GetNextBigImg()函数中调用人脸检测函数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638951805.png)   由于在detect_and_draw()函数中已经封装了picture显示的程序,所以可以将GetNextBigImg()函数中原有的picture控件显示程序去掉。   大功告成,顺利完成人脸检测: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6389675a2.png)   三、总结   这里我们初步完成了MFC中的人脸检测功能,但这里存在两个严重的BUG,一是如果用户未单击“初始化”按钮,直接打开图片,程序会因缺少必要的初始化步骤而直接崩溃;二是如上图所见,OpenCv在进行人脸检测时可能会错误检测出多个矩形,其中只有一个矩形包含人脸,其余的都是干扰,需要进行处理,我们将在下一篇博客中介绍如何解决这两个BUG。   同时在此需要强调一下两个问题:   1、静态成员变量的初始化:[c++中可以对类中私有成员中的静态变量初始化吗?](http://www.cnblogs.com/carbs/archive/2012/04/04/2431992.html)   2、字符串的连接:[C++字符换行 .](http://www.cnblogs.com/zhoug2020/archive/2012/04/01/2428156.html)  
';

(9)——搭建MFC框架之显示图片

最后更新于:2022-04-01 20:14:26

  在之前的博客中我们已经实现读取用户选定的文件夹,并将其路径保存在相应的变量中,在这篇博文中我们将介绍如何借助CvvImage类将图片显示在picture控件中,并自动读取文件夹下的其他图片。   一、添加“下一张”按钮   由于我们需要读取文件夹下的所有图像文件,而非某一张文件,因此有必要添加一个按钮来进行控制,具体功能就是:每单击一次这个按钮,程序就会自动读取下一张图片并显示在界面上。由于之前已经详细介绍了MFC中添加Button控件的方式,这里不再赘述。添加一个按钮,命名为“下一张”,将ID更改为IDC_BUTTON_NextImage: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63882a1ed.png)   二、编写遍历函数   在上一篇博客中我们提到,在选中文件夹之后,程序会将文件夹的路径保存在m_Path变量之中。接下来我们就借助这个变量来进一步遍历其路径下的图像文件。这里我们专门编写一个函数来实现“遍历下一张图片”的功能,命名为GetNextBigImg。因此,需要向CGenderRecognitionMFCDlg类中添加这个成员函数。在类视图中右击相应的类,在快捷菜单中选择“添加->添加函数”,输入函数的属性: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638838120.png)   GetNextBigImg()函数主要承担着一下几个任务: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63884f3c3.png)   1、开始遍历   这里将GetNextBigImg()放在OnBnClickedButtonImagefile()函数中的末尾部分进行调用,用以在单击“图片文件夹”按钮读取文件夹信息之后启用文件读取程序。   2、从当前目录路径下读入一个文件   这里读取文件主要通过readdir函数来完成,考虑到用户可能会选择一个空文件夹,因此这里需要对读取操作进行一次判断: ~~~ if (m_pDir && (m_pEnt = readdir(m_pDir)) != NULL) { } ~~~   readdir()函数能够实现对当前目录结构(m_pDir)中的文件的无重复顺序读取,即每次读取完成后都会自动移到下一个待读取的文件,与指针的机制类似,readdir()函数包含在dirent.h头文件中,之前已经添加并包含完毕。此时,m_pEnt变量中保存了文件名称: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63886077d.png)   3、判断是否为图像文件   这里采用strstr()函数来判断文件名中是否包含对应的扩展名字符串,这里默认的图像格式有四种:jpg ,bmp,png: ~~~ if (m_pDir && (m_pEnt = readdir(m_pDir)) != NULL) { /**********判断是否为图像文件**********/ char* jpg = strstr(m_pEnt->d_name,".jpg"); char* bmp = strstr(m_pEnt->d_name,".bmp"); char* png = strstr(m_pEnt->d_name,".png"); } ~~~   至于“m_pEnt->d_name”这种调用格式,在dirent.h头文件中有着明确定义,有疑问的话可以查阅相关文件。接下里通过判断jpg、bmp、png这几个变量是否为空来确定文件是否是图像文件: ~~~ if (m_pDir && (m_pEnt = readdir(m_pDir)) != NULL) { /**********判断是否为图像文件**********/ char* jpg = strstr(m_pEnt->d_name,".jpg"); char* bmp = strstr(m_pEnt->d_name,".bmp"); char* png = strstr(m_pEnt->d_name,".png"); if (jpg == NULL && bmp == NULL && png == NULL) //如果该文件不是图像文件 { GetNextBigImg(); } else { /**********显示该图片**********/ } } ~~~   注意这里采用了一种递归的方式来实现非图像文件的轮询,即当前文件被判定为非图像文件时(jpg、bmp、png均为空),则调用自身GetNextBigImg(),也就意味着再次执行一遍readdir()函数,使得文件指针后移意味,层层递归实现最终的文件遍历;相应的,如果当前文件为三种图像文件中的一种,则将当前图片绘制到picture控件中,接下来编写绘制图像的代码。   4、绘制图像至picture控件   此时该轮到CvvImage大显身手了。在此之前,我们需要先为picture控件关联一个CRect类型的矩形变量,这个变量将用来保存picture控件在客户区所处的位置。首先,为CGenderRecognitionMFCDlg类添加成员变量m_PicCtlRect: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63887086a.png)   然后,再添加一个HDC(句柄)变量m_pPicCtlHdc,用于保存控件的句柄: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6388826f9.png)    然后在CGenderRecognitionMFCDlg的对话框初始化函数OnInitDialog()中编写两行代码,将控件、句柄、位置信息这三个变量相互关联起来: ~~~ /*********初始化picture控件**********/ m_pPicCtlHdc = GetDlgItem(IDC_PICTURE)->GetDC()->GetSafeHdc(); //返回控件句柄 GetDlgItem(IDC_PICTURE)->GetClientRect(m_PicCtlRect); //关联控件位置 ~~~   将这两句代码添加到OnInitDialog()末尾即可,这里有三个问题需要强调:   (1)为什么需要用到句柄和CRect变量?原因很简单,CvvImage类的要求。这里我们介绍一个查看函数形参的小技巧,即在函数名的括号中输入一个逗号,VS就会自动给出函数的形参格式: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638894e50.png)   可见,DrawToHDC这个函数需要两个参数,一个是HDC类型的,一个是RECT*类型的。   (2)如何快速查找类的成员函数?最直接的方法就是通过类视图,单击对应的类来进行浏览即可: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6388a2ab3.png)   当然,通过上方的搜索栏也是可以的。   (3)OnInitDialog函数。这个函数在程序开始构造MFC框架时执行,因此有关控件的初始化操作都应该在这个函数中进行,而非构造函数。   此时准备工作已经完成,可以为GetNextBigImg()函数添加正式的显示代码了: ~~~ /**********显示该图片**********/ IplImage* imageSrc; CvvImage imageSrcCvvImg; char imageFullName [500]; //保存图像文件的全路径 sprintf_s(imageFullName,"%s%s",m_ImageDir,m_pEnt->d_name); //拼出文件全路径 imageSrc = cvLoadImage(imageFullName); imageSrcCvvImg.CopyOf(imageSrc); imageSrcCvvImg.DrawToHDC(m_pPicCtlHdc,m_PicCtlRect); cvReleaseImage(&imageSrc); ~~~   此时,运行程序,通过“图像文件夹按钮”,选择一个含有图片文件的文件夹,程序正常显示图片: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6388b3fdb.png)   5、添加“下一张”功能   接下来我们为界面中的“下一张”按钮指定其功能。双击“下一张”按钮,添加响应函数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6388de722.png)   由于之前我们已经将图片轮询、显示操作封装在了GetNextBigImg()函数中,在这里我们只需调用一把这个函数即可实现“下一张”的功能: ~~~ void CGenderRecognitionMFCDlg::OnBnClickedButtonNextimage() { GetNextBigImg(); // TODO: 在此添加控件通知处理程序代码 } ~~~   OK,大功告成。    三、总结   经过这篇博文,我们的MFC框架已经具备了基本的图像显示功能,在下一篇博文中我们将向其中添加人脸检测的功能。这里有几个问题需要注意。   1、OpenCv2.x关于图片显示的问题   大家留心观察会发现,这里用到的CvvImage方法是完全基于OpenCv1.x的,用IplImage变量来表示图片。   2、递归层数的问题   这里GetNextBigImg()函数存在一个递归调用的过程,存在递归就需要考虑递归深度的问题。这里每遍历到一个非图像文件,递归的深度就增加一层,如果超过规定的递归深度,程序就会崩溃,从这个角度来讲通过递归的方法来轮询图像文件和非图像文件,是存在严重BUG隐患的,只要文件夹下有足够多的非图像文件,程序必然会因为无限递归而崩溃,相信大家有能力找到其他更安全的方法来解决这个问题。
';

(8)——搭建MFC框架之读取文件夹信息

最后更新于:2022-04-01 20:14:24

  在上一篇博客中我们已经绘制了MFC界面,在这篇博客中我们将添加响应代码,为MFC框架添加一个最基本的功能:打开一个文件夹。   一、添加相关头文件   这里头文件主要包含三类:opencv头文件、批量读取文件相关的头文件、CvvImage。这里需要强调CvvImage这个头文件,这个是用来关联OpenCv和picture控件,并且这个头文件是隶属于OpenCv1.x的,在2.x版本中已经将这个类移除,因此需要手动下载这两个文件(CvvImage.h和CvvImage.cpp),下载地址:[CvvImage](http://download.csdn.net/detail/u013088062/9395566)。下载后将这两个文件放在工程目录下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6387678e6.png)   然后在VS的解决方案资源管理器窗口中,右击该工程,在快捷菜单中选择“添加->现有项”: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63877e9ea.png)   将这两个文件添加到当前工程中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63878e06a.png)   添加完成后,可以在代码中添加响应的头文件了,建议将include命令添加在GenderRecognitionMFCDlg.h头文件中: ~~~ #include #include #include #include #include "CvvImage.h" using namespace std; using namespace cv; ~~~   这里有两点需要强调:   (1)#include语句包含两种形式,“ #include<> ”和“ #include“” ”。这两种格式的区别在于优先搜索路径的不同,“ #include<> ”默认优先按照系统路径进行搜索,“ #include“” ”默认优先搜索当前的工程目录。   (2)include语句的位置。大型工程中的include语句要注意避免一个重复包含的问题,即要保证每条include语句只执行一次,否则就会出现重定义类型的错误。C++提供两种机制来确保include语句执行的唯一性,一是通过“ifndef”宏来包围include代码块,二是通过“#pragma once”宏来实现,这里默认使用第二种,因此所有的include语句应该位于“#pragma once”语句之后。   二、添加控件响应函数   由于读取显示图片的操作是通过“图像文件夹”按钮来控制的,因此需要为这个按钮添加响应的事件响应函数,方法非常简单,在资源视图窗口中双击对应控件即可,VS将自动添加响应函数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63879eefc.png)   三、打开文件夹,读取图片路径   接下来为OnBnClickedButtonImagefile()函数添加批量读取图片的代码。我们这里选用SHBrowseForFolder方法。关于这个方法我之前曾写过一篇博客来专门介绍,具体参见[一种批量读取文件的方法——SHBrowseForFolder](http://blog.csdn.net/u013088062/article/details/39137809)。注意一点,在SHBrowseForFolder方法中需要用到dir目录相关的操作函数(如opendir等),这算是Linux的移植版,因此需要借用dirent.h头文件,不过我们已经在之前下载的资源中提供了这个文件,只需依据之前CvvImage文件的配置方法,添加到当前工程中即可: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6387abc62.png)   接下来,向CGenderRecognitionMFCDlg类中添加若干成员变量,用以记录读取的文件夹以及文件的路径及属性,具体需要添加以下四个成员变量,分别是CString m_Path(图像文件路径)、char* m_ImageDir(文件结构)、DIR *m_pDir(目录结构)、struct dirent *m_pEnt(目录结构),这里以m_pEnt为例,介绍VS中添加类成员变量的方法。在类视图中,右击CGenderRecognitionMFCDlg类,在快捷菜单中选择“添加->添加变量”: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6387bd832.png)   在弹出的向导窗口中,指定变量的属性,然后单击“完成”按钮: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6387ce009.png)   同理,添加其他三个成员变量(注意变量类型): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6387ded85.png)   变量添加完成后,即可向OnBnClickedButtonImagefile()函数中添加相应代码: ~~~ /**********初始化变量**********/ CString str; //存储图像路径 BROWSEINFO bi; //用来存储用户选中的目录信息 TCHAR name[MAX_PATH]; //存储路径 ZeroMemory(&bi,sizeof(BROWSEINFO)); //清空目录对应的内存 bi.hwndOwner = GetSafeHwnd(); //得到窗口句柄 bi.pszDisplayName = name; /**********设置对话框并读取目录信息**********/ BIF_BROWSEINCLUDEFILES; bi.lpszTitle = _T("Select folder"); //对话框标题 bi.ulFlags = 0x80; //设置对话框形式 LPITEMIDLIST idl = SHBrowseForFolder(&bi); //返回所选中文件夹的ID SHGetPathFromIDList(idl,str.GetBuffer(MAX_PATH)); //将文件信息格式化存储到对应缓冲区中 str.ReleaseBuffer(); //与GerBuffer配合使用,清空内存 m_Path=str; //将路径存储在m_path中 if(str.GetAt(str.GetLength()-1)!='\\') m_Path += "\\"; UpdateData(FALSE); IMalloc * imalloc = 0; if (SUCCEEDED(SHGetMalloc(&imalloc))) { imalloc->Free (idl); imalloc->Release(); } /**********获取该路径下的第一个文件**********/ m_ImageDir = (LPSTR)(LPCTSTR)m_Path; m_pDir = opendir(m_ImageDir); for (int i = 0; i < 1; i ++) //过滤目录 .. 和 . { m_pEnt = readdir(m_pDir); } ~~~   有关SHBrowseForFolder的方法介绍,除了之前给出的那篇博客之外,这里再推荐两篇更为详细的博客:[文件夹浏览(SHBrowseForFolder)](http://www.cnblogs.com/Clingingboy/archive/2011/04/16/2018284.html)以及[使用SHBrowseForFolder函数打开文件目录对话框](http://www.cppblog.com/franksunny/archive/2010/12/30/137754.html)。   四、简单调试   完成上面那段代码之后,进行一下简单调试。首先按下F7对工程进行编译,在编译过程中注意将调试器版本设置为X64(64位)。编译通过后,设置断点,按下F5进行调试运行。此时单击“图像文件夹”按钮,将弹出打开文件夹对话框: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6387eda2e.png)   选择一个文件夹,然后查看各个变量的情况: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638808adb.png)   可见,m_Path保存了当前选择的文件夹路径。在下一篇博文中我们将通过这个变量来完成对应文件夹目录下图像文件的遍历和显示。  
';

(7)——搭建MFC框架之界面绘制

最后更新于:2022-04-01 20:14:22

  在之前的博客中我们已经将项目中用到的算法表述完毕,包括人脸检测算法以及四种性别识别算法,在这篇博客中我们将着手搭建基本的MFC框架。   一、框架概况   在这篇博文中我们将搭建最基本的MFC框架,绘制MFC界面。   二、搭建流程   1、新建一个MFC工程并配置OpenCv   打开VS,按下“ctrl+n”,在新建窗口中选择“MFC应用程序”,命名为GenderRecognitionMFC: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6386b4110.png)   单击确定,程序类型选择“基于对话框”,MFC使用选择“在静态库中使用MFC”: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6386ce132.png)   直接单击“完成”,创建完毕。OpenCv的配置方法和以前相同,这里不再赘述。   2、绘制对话框控件   创建完成后,编译器将默认显示对话框资源窗口,当然我们可以在资源视图的相应位置找到它: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6386e6ebb.png)   打开右侧工具箱窗口,选中button控件: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6387003a8.png)   然后在主控件上绘制一个按钮(直接从工具箱中拖动出来也可以): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63872040f.png)   接下来设置这个按钮的属性,包括显示文本和对应ID。在按钮控件上右击,在快捷菜单中选择“属性”,打开属性对话框,更改对应外观和ID: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638731949.png)   3、绘制picture控件   在MFC中显示图像需要用到picture控件,在工具箱对话框中找到这个控件,并已合适大小绘制到主对话框中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638745610.png)   最终布局如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638756ea6.png)   三、总结   这篇博文的内容相对简单,介绍了MFC界面的绘制过程,在下一篇博客中我们将为其编写响应代码,实现显示图像的功能。
';

(6)——通过SVM实现性别识别

最后更新于:2022-04-01 20:14:20

  上一篇教程中我们介绍了如何使用OpenCv封装的FaceRecognizer类实现简单的人脸性别识别,这里我们为大家提供另外一种基本的性别识别手段——支持向量机(SVM)。   支持向量机在解决二分类问题方面有着强大的威力(当然也可以解决多分类问题),性别识别是典型的二分类模式识别问题,因此很适合用SVM进行处理,同时OpenCv又对SVM进行了很好的封装,调用非常方便,因此我们在这个性别识别程序中考虑加入SVM方法。   在这里我们采用了HOG+SVM的模式来进行,即先提取图像的HOG特征,然后将这些HOG特征输入SVM中进行训练。   一、SVM概述   SVM的数学原理十分复杂,我们不在这里过多讨论,有关OpenCv中SVM的用法,这里为大家提供两篇博客以供参考:[OpenCV的SVM用法](http://blog.csdn.net/carson2005/article/details/6547250)以及[OpenCV 2.4+ C++ SVM介绍](http://www.cnblogs.com/justany/archive/2012/11/23/2784125.html)。   二、HOG特征概述   HOG特征是图像的梯度特征,具体参见:[目标检测的图像特征提取之(一)HOG特征](http://blog.csdn.net/zouxy09/article/details/7929348)   三、建立训练集   这里继续沿用上一篇博文中提到的性别识别训练集,400张男性人脸样本400张女性人脸样本,下载地址:[性别识别数据集](http://download.csdn.net/detail/u013088062/9389882)。   四、算法的训练与测试   1、建立控制台工程,配置OpenCv环境   这里将工程命名为:GenderSVM。   2、编写批量读取函数read_csv()   只要涉及到训练,都需要批量读取训练样本的操作,SVM也不例外,因此需要先编写批量读取函数read_csv()。考虑到之前的批量读取函数必须一次性将所有训练样本读入内存中,内存消耗较大,在这里做一个小小的改进: ~~~ void read_csv(String& csvPath,Vector& trainPath,Vector& label,char separator = ';') { string line,path,classLabel; ifstream file(csvPath.c_str(),ifstream::in); while (getline(file,line)) { stringstream lines(line); getline(lines,path,separator); getline(lines,classLabel); if (!path.empty()&&!classLabel.empty()) { trainPath.push_back(path); label.push_back(atoi(classLabel.c_str())); } } } ~~~   可见这里我们将输入参数由vector改为vector,然后返回装有训练样本的所有路径的容器,需要时在根据其中的路径进行读取,降低了内存占用量。   3、读入训练样本路径 ~~~ string trainCsvPath = "E:\\性别识别数据库—CAS-PEAL\\at.txt"; vector vecTrainPath; vector vecTrainLabel; read_csv(trainCsvPath,vecTrainPath,vecTrainLabel); ~~~   顺利批量读入路径: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6386132ef.png)   4、训练初始化   在提取HOG特征之前,需要初始化训练数据矩阵: ~~~ /**********初始化训练数据矩阵**********/ int iNumTrain = 800; Mat trainDataHog; Mat trainLabel = Mat::zeros(iNumTrain,1,CV_32FC1); ~~~   需要强调的是SVM的训练数据必须都是CV_32FC1格式,因此这里显式的将标签矩阵trainLabel初始化为CV_32FC1格式,trainDataHog稍后进行初始化。   5、提取图像HOG特征   接下来循环读入所有的训练样本,提取HOG特征,放在训练数据矩阵中。考虑嵌套代码的复杂性,这里先给出整体代码,稍后解释: ~~~ /**********提取HOG特征,放入训练数据矩阵中**********/ Mat imageSrc; for (int i = 0; i < iNumTrain; i++) { imageSrc = imread(vecTrainPath[i].c_str(),1); resize(imageSrc,imageSrc,Size(64,64)); HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16), cvSize(8,8),cvSize(8,8),9); vector descriptor; hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0)); if (i == 0) { trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1); } int n = 0; for (vector::iterator iter = descriptor.begin();iter != descriptor.end();iter++) { trainDataHog.at(i,n) = *iter; n++; } trainLabel.at(i,0) = vecTrainLabel[i]; } ~~~   接下来我们对这段代码进行详细解释。   (1)循环读入训练样本   从vecTrainPath容器中逐条取出训练样本路径,然后读取: ~~~ imageSrc = imread(vecTrainPath[i].c_str(),1); ~~~   (2)尺寸归一化   我们这里将图像尺寸归一化为64*64,这是因为当时在写程序时参考了一篇关于HOG特征的博客。这里的尺寸大家可以随意设定,当然也会影响最终的识别效率,64*64可能并不是一个最优的尺寸: ~~~ imageSrc = imread(vecTrainPath[i].c_str(),1); resize(imageSrc,imageSrc,Size(64,64)); ~~~   (3)计算HOG特征   OpenCv给出的HOG特征计算接口非常简洁,三句话即完成: ~~~ HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16), cvSize(8,8),cvSize(8,8),9); vector descriptor; hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0)); ~~~   提取的特征以容器的数据 结构形式给出。至于计算时的参数设定,参见我之前提供的那两篇博客即可。   (4)初始化数据矩阵trainDataHog   前面提到,SVM中用到的训练数据矩阵必须是CV_32FLOAT形式的,因此需要对数据矩阵显示的指定其尺寸和类型。然后由于trainDataHog行数为训练样本个数,而列数为图片HOG特征的维数,因此无法在进行HOG特征提取之前确定其尺寸,因此这里选择在进行完第一张样本的HOG特征、得到对应维数之后,在进行初始化: ~~~ if (i == 0) { trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1); } ~~~   (5)将得到的HOG特征存入数据矩阵   得到的HOG特征是浮点数容器的形式,我们需要将其转换成矩阵的形式以便于训练SVM,这就涉及到了vector和Mat两个数据结构的遍历。vector遍历这里推荐使用迭代器的方式,而Mat遍历这里则选择了相对耗时但是最简单的方式——直接使用at函数: ~~~ int n = 0; for (vector::iterator iter = descriptor.begin();iter != descriptor.end();iter++) { trainDataHog.at(i,n) = *iter; n++; } trainLabel.at(i,0) = vecTrainLabel[i]; ~~~   训练得到的HOG特征如图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6386282d0.png)   可见在当前的参数设定下,提取到的HOG特征为1764维,共800张训练样本,每一行代表一个图片的HOG特征向量。通过“ctrl+鼠标滚轮”放大观察特征向量的具体参数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6386420bb.png)   6、训练SVM分类器   有关OpenCv中SVM分类器的使用可以参见以下博客:[OpenCV 2.4+ C++ SVM介绍](http://www.cnblogs.com/justany/archive/2012/11/23/2784125.html)。   首先,初始化相关参数: ~~~ /**********初始化SVM分类器**********/ CvSVM svm; CvSVMParams param; CvTermCriteria criteria; criteria = cvTermCriteria( CV_TERMCRIT_EPS, 1000, FLT_EPSILON ); param = CvSVMParams(CvSVM::C_SVC, CvSVM::RBF, 10.0, 0.09, 1.0, 10.0, 0.5, 1.0, NULL, criteria ); ~~~   开始训练、训练完成后保存分类器: ~~~ /**********训练并保存SVM**********/ svm.train(trainDataHog,trainLabel,Mat(),Mat(),param); svm.save("E:\\性别识别数据库—CAS-PEAL\\SVM_SEX_Model.txt"); ~~~   注意我们这里选择将分类器保存为txt形式: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638659fa0.png)   当然,我们可以打开这个txt文件,查看里面的参数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63866fd62.png)   7、测试分类效果   测试过程和训练过程基本相同,读取图片、尺寸归一化、提取HOG特征、预测: ~~~ /**********测试SVM分类性能**********/ Mat testImage = imread("E:\\性别识别数据库—CAS-PEAL\\测试样本\\女性测试样本\\face_35.bmp"); resize(testImage,testImage,Size(64,64)); HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16), cvSize(8,8),cvSize(8,8),9); vector descriptor; hog->compute(testImage,descriptor,Size(1,1),Size(0,0)); Mat testHog = Mat::zeros(1,descriptor.size(),CV_32FC1); int n = 0; for (vector::iterator iter = descriptor.begin();iter != descriptor.end();iter++) { testHog.at(0,n) = *iter; n++; } int predictResult = svm.predict(testHog); ~~~   8、完整代码  这里给出HOG+SVM进行性别识别的完整代码: ~~~ // GenderSVM.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include #include #include #include using namespace std; using namespace cv; void read_csv(String& csvPath,vector& trainPath,vector& label,char separator = ';') { string line,path,classLabel; ifstream file(csvPath.c_str(),ifstream::in); while (getline(file,line)) { stringstream lines(line); getline(lines,path,separator); getline(lines,classLabel); if (!path.empty()&&!classLabel.empty()) { trainPath.push_back(path); label.push_back(atoi(classLabel.c_str())); } } } int _tmain(int argc, _TCHAR* argv[]) { /**********批量读入训练样本路径**********/ string trainCsvPath = "E:\\性别识别数据库—CAS-PEAL\\at.txt"; vector vecTrainPath; vector vecTrainLabel; read_csv(trainCsvPath,vecTrainPath,vecTrainLabel); /**********初始化训练数据矩阵**********/ int iNumTrain = 800; Mat trainDataHog; Mat trainLabel = Mat::zeros(iNumTrain,1,CV_32FC1); /**********提取HOG特征,放入训练数据矩阵中**********/ Mat imageSrc; for (int i = 0; i < iNumTrain; i++) { imageSrc = imread(vecTrainPath[i].c_str(),1); resize(imageSrc,imageSrc,Size(64,64)); HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16), cvSize(8,8),cvSize(8,8),9); vector descriptor; hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0)); if (i == 0) { trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1); } int n = 0; for (vector::iterator iter = descriptor.begin();iter != descriptor.end();iter++) { trainDataHog.at(i,n) = *iter; n++; } trainLabel.at(i,0) = vecTrainLabel[i]; } /**********初始化SVM分类器**********/ CvSVM svm; CvSVMParams param; CvTermCriteria criteria; criteria = cvTermCriteria( CV_TERMCRIT_EPS, 1000, FLT_EPSILON ); param = CvSVMParams(CvSVM::C_SVC, CvSVM::RBF, 10.0, 0.09, 1.0, 10.0, 0.5, 1.0, NULL, criteria ); /**********训练并保存SVM**********/ svm.train(trainDataHog,trainLabel,Mat(),Mat(),param); svm.save("E:\\性别识别数据库—CAS-PEAL\\SVM_SEX_Model.txt"); /**********测试SVM分类性能**********/ Mat testImage = imread("E:\\性别识别数据库—CAS-PEAL\\测试样本\\女性测试样本\\face_35.bmp"); resize(testImage,testImage,Size(64,64)); HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16), cvSize(8,8),cvSize(8,8),9); vector descriptor; hog->compute(testImage,descriptor,Size(1,1),Size(0,0)); Mat testHog = Mat::zeros(1,descriptor.size(),CV_32FC1); int n = 0; for (vector::iterator iter = descriptor.begin();iter != descriptor.end();iter++) { testHog.at(0,n) = *iter; n++; } int predictResult = svm.predict(testHog); return 0; } ~~~   五、总结   以上就是通过HOG特征+SVM进行性别识别的完整代码,在编写代码的过程中遇到了一些有趣的问题,这里稍作总结。   1、变量命名格式   当代码量很大的时候,变量的命名格式就显得十分重要,相信大家早已不用那种a、b、m、n这种简单的无意义的命名方法了。在C++中推荐大家使用匈牙利命名法,即“类型缩写+变量名缩写”的命名格式。例如vecTrainPath这个变量名,前缀“vec”表明这个变量是一个vector格式的变量,而“TrainPath”则表明这个容器中存放的是训练样本的路径。这种命名方式在大型工程中非常重要,还有一点需要注意的是当变量名中出现多个缩略短语时,推荐第一个短语小写,其他短语的首字母大写。   2、为何选择HOG特征   通过实验发现,直接将图像向量化后输入SVM(不经过特征提取)的方式的正确率将不理想。虽然本质上像素本身最能代表图像的语义信息,但由于SVM并不具备特征提取能力,因此效果不佳。确切的说,特征提取是模式分类的必要过程,即便是深度学习也不例外,因为深度学习(DeepLearning)本质上也是一种特征提取的手段,只不过提取得到的特征更深层,更抽象,表现力更强。为此我之前曾专门写过一篇博客进行阐述:[浅谈模式识别中的特征提取](http://blog.csdn.net/u013088062/article/details/45952613)   当然这里大家可以尝试提取其他特征之后再进行分类,甚至可以考虑通过提起深度特征来进行分类,这里只是以HOG特征为例而已。   4、有关vector的一些使用(为什么不用int型数组)   在这段代码中我们大量用到了vector结构,这是C++11的新特性。仔细观察,其实vector结构的最明显的一个优势就是能够动态分配大小,实时添加/删除元素,这点是数组所不能实现的。虽然可以通过new操作符来实现数组的动态分配,但我们仍推荐大家在需要使用可动态变化的数组的场合,使用vector。   5、Vectot和vector   在编写代码是仔细留心编译器给出的拼写提示,会发现这样一现象: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638687c31.png)   那么vector和Vector有什么区别呢?一句话,Vector是OpenCv中的vector,类似的还有String和string等。Vector和String这类结构是隶属于OpenCv的: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63869d1c3.png)   OK,以上就是这次博文的所有内容,在接下来的博文中我们将开始进入MFC编程阶段,欢迎大家讨论:[http://blog.csdn.net/u013088062](http://blog.csdn.net/u013088062)。
';

(5)——通过FaceRecognizer类实现性别识别

最后更新于:2022-04-01 20:14:17

  在之前的博客中已经解决了人脸检测的问题,我们计划在这篇博客中介绍人脸识别、性别识别方面的相关实现方法。   其实性别识别和人脸识别本质上是相似的,因为这里只是一个简单的MFC开发,主要工作并不在算法研究上,因此我们直接将性别识别视为一种特殊的人脸识别模式。人脸识别可能需要分为几十甚至上百个类(因为有几十甚至上百个人),而性别识别则是一种特殊的人脸识别——只有两个类。   一、基本工具   通过OpenCv进行性别识别的基本工具是FaceRecognizer。这是OpenCv2.x版本中的一个基本的人脸识别类,它封装了三种基本但也是经典的人脸识别算法:基于PCA变换的人脸识别(EigenFaceRecognizer)、基于Fisher变换的人脸识别(FisherFaceRecognizer)、基于局部二值模式的人脸识别(LBPHFaceRecognizer)。这些算法差不多都是十年以前的人脸识别方法了,因此在今天看来正确率应该不会太让人满意,不过我们这里重在实践,而非算法研究(虽然本人就是搞图像识别算法研究的),因此我们不会在算法创新方面下太多功夫,所以选择了这三个基本的识别算法。   关于FaceRecognizer类人脸识别的详细操作,这里为大家推荐两篇博客:[FaceRecognizer帮助文档](http://www.cnblogs.com/guoming0000/archive/2012/09/27/2706019.html)以及[FaceRecognizer](http://blog.csdn.net/u013088062/article/details/38588185)。   这里我们直接使用FaceRecognizer类的相关操作方法,对于其基本用法就不再赘述。   二、数据集准备   进行性别识别理所应当需要先准备一些性别识别方面的训练样本,需要强调的一点是,数据集的准备过程中也需要一些小的技巧,在之后我会专门写一篇博文来解释如何制作一个简易的性别识别训练集,这里我们直接用我已经做好的训练集,下载地址:[性别识别数据集](http://download.csdn.net/detail/u013088062/9389882)   1、概况   我这里整理的性别识别训练集是取自中科院的人脸数据库CAS-PEAL的光照子集,包含400张男性人脸图片和400张女性人脸图片,剩余人脸图片作为测试样本   2、训练集基本结构   训练集包含三部分:男性样本、女性样本、测试样本: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6385629af.png)   这里我们通过CSV文件方法来批量读取训练样本,因此这里提前制作了一个txt文件来存储每一个训练样本图片的路径: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63857d600.png)   注意这里at.txt文件中的路径实际上是由两部分内容组成,即“路径;性别标号”。性别标号“1”代表男性,“2”代表女性。至于如何通过csv文件方法来批量读取文件,参见:[一种批量读取文件的方法—CSV文件](http://blog.csdn.net/u013088062/article/details/38588281)。   同理,在测试样本中同样需要用txt文件来记录样本路径和标签: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638590d12.png)   三、识别算法的训练与测试   1、新建一个控制台工程,配置OpenCv   这里不再赘述,建议加上预编译头即可,这里工程名暂定为GenderRecognition   2、编写批量读取文件函数read_csv()   首先,批量txt文件是典型的io操作,需要包含以下头文件: ~~~ #include #include #include ~~~   然后开始编写read_csv函数,函数相对比较简单,这里直接给出代码: ~~~ void read_csv(string& fileName,vector& images,vector& labels,char separator = ';') { ifstream file(fileName.c_str(),ifstream::in); //以读入的方式打开文件 String line,path,label; while (getline(file,line)) //从文本文件中读取一行字符,未指定限定符默认限定符为“/n” { stringstream lines(line); getline(lines,path,separator); //根据指定分割符进行分割,分为“路径+标号” getline(lines,label); if (!path.empty()&&!label.empty()) //如果读取成功,则将图片和对应标签压入对应容器中 { images.push_back(imread(path,1)); //读取训练样本 labels.push_back(atoi(label.c_str())); //读取训练样本标号 } } } ~~~   read_csv()函数的主要功能就是读取指定目录下的路径文件(例如这里的at.txt),然后根据路径文件中的记录,逐行读入对应路径的训练样本路径及其标号,并放入对应容器(vector)中。至于为什么采用vector数据结构来存储训练样本,一是因为这样做简单直观,二是因为OpenCv的训练函数提供的是vector接口。当然这样做也存在一定弊端,就是必须一次性将训练样本全部读入到内存中,当训练样本数量庞大时这种方法不但会消耗掉巨额内存,而且效率低下。   更多关于read_csv()批量读取的知识参见[一种批量读取文件的方法—CSV文件](http://blog.csdn.net/u013088062/article/details/38588281)。   3、读入训练样本   接下来在主函数中调用read_csv()函数,读取训练样本及标签,并放入对应容器中: ~~~ int _tmain(int argc, _TCHAR* argv[]) { String csvPath = "E:\\性别识别数据库—CAS-PEAL\\at.txt"; vector images; vector labels; read_csv(csvPath,images,labels); return 0; } ~~~   读取成功,images和labels两个容器都包含800个样本: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6385a5e67.png)   4、训练分类器   OpenCv中的FaceRecognizer类提供的分类器训练API函数非常简单,只需三句话,以EigenFaceRecognizer为例: ~~~ Ptr modelPCA = createEigenFaceRecognizer(); modelPCA->train(images,labels); modelPCA->save("E:\\性别识别数据库—CAS-PEAL\\PCA_Model.xml"); ~~~   训练完成后(大约五分钟左右),训练好的分类器已经以XML文件的形式保存在了指定路径下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6385bb7ec.png)    同理,训练FisherFaceRecognizer、LBPHFaceRecognizer两个分类器并保存: ~~~ Ptr modelFisher = createFisherFaceRecognizer(); modelFisher->train(images,labels); modelFisher->save("E:\\性别识别数据库—CAS-PEAL\\Fisher_Model.xml"); Ptr modelLBP = createLBPHFaceRecognizer(); modelLBP->train(images,labels); modelLBP->save("E:\\性别识别数据库—CAS-PEAL\\LBP_Model.xml"); ~~~   得到另外两个分类器: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6385d3205.png)   4、测试分类器   训练完分类器后,接下来我们介绍如何使用这些训练好的分类器对测试样本进行分类。首先加载三个分类器 ~~~ Ptr modelPCA = createEigenFaceRecognizer(); Ptr modelFisher = createFisherFaceRecognizer(); Ptr modelLBP = createLBPHFaceRecognizer(); modelPCA->load("E:\\性别识别数据库—CAS-PEAL\\PCA_Model.xml"); modelFisher->load("E:\\性别识别数据库—CAS-PEAL\\Fisher_Model.xml"); modelLBP->load("E:\\性别识别数据库—CAS-PEAL\\LBP_Model.xml"); ~~~   然后读入一张测试样本,通过三个分类器对其进行预测: ~~~ Mat testImage = imread("E:\\性别识别数据库—CAS-PEAL\\测试样本\\男性测试样本\\face_480.bmp",0); int predictPCA = modelPCA->predict(testImage); int predictLBP = modelLBP->predict(testImage); int predictFisher = modelFisher->predict(testImage); ~~~   预测结果如图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6385ec3e7.png)   可见对于这张测试图片,三个分类器均给出了正确预测(数字“1”代表男性),正确率可以接受。   四、代码   这部分博客所涉及的代码同样较为简洁,因此在这里给出整体代码: ~~~ // GenderRecognition.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include #include #include #include using namespace std; using namespace cv; void read_csv(string& fileName,vector& images,vector& labels,char separator = ';') { ifstream file(fileName.c_str(),ifstream::in); //以读入的方式打开文件 String line,path,label; while (getline(file,line)) //从文本文件中读取一行字符,未指定限定符默认限定符为“/n” { stringstream lines(line); getline(lines,path,separator); //根据指定分割符进行分割,分为“路径+标号” getline(lines,label); if (!path.empty()&&!label.empty()) //如果读取成功,则将图片和对应标签压入对应容器中 { images.push_back(imread(path,0)); //读取训练样本 labels.push_back(atoi(label.c_str())); //读取训练样本标号 } } } int _tmain(int argc, _TCHAR* argv[]) { String csvPath = "E:\\性别识别数据库—CAS-PEAL\\at.txt"; vector images; vector labels; read_csv(csvPath,images,labels); Ptr modelPCA = createEigenFaceRecognizer(); modelPCA->train(images,labels); modelPCA->save("E:\\性别识别数据库—CAS-PEAL\\PCA_Model.xml"); Ptr modelFisher = createFisherFaceRecognizer(); modelFisher->train(images,labels); modelFisher->save("E:\\性别识别数据库—CAS-PEAL\\Fisher_Model.xml"); Ptr modelLBP = createLBPHFaceRecognizer(); modelLBP->train(images,labels); modelLBP->save("E:\\性别识别数据库—CAS-PEAL\\LBP_Model.xml"); //Ptr modelPCA = createEigenFaceRecognizer(); //Ptr modelFisher = createFisherFaceRecognizer(); //Ptr modelLBP = createLBPHFaceRecognizer(); modelPCA->load("E:\\性别识别数据库—CAS-PEAL\\PCA_Model.xml"); modelFisher->load("E:\\性别识别数据库—CAS-PEAL\\Fisher_Model.xml"); modelLBP->load("E:\\性别识别数据库—CAS-PEAL\\LBP_Model.xml"); Mat testImage = imread("E:\\性别识别数据库—CAS-PEAL\\测试样本\\男性测试样本\\face_480.bmp",0); int predictPCA = modelPCA->predict(testImage); int predictLBP = modelLBP->predict(testImage); int predictFisher = modelFisher->predict(testImage); return 0; } ~~~   四、总结   这篇博客主要介绍了如何使用OpenCv提供的人脸识别类FaceRecognizer来进行性别识别,并提供了一段win32控制台工程下的简洁代码,同时,有以下几个方面需要特别注意一下。   1、人脸识别和性别识别的关系   在这篇博客的开始部分曾提到过性别识别和人脸识别的关系,在这里需要再次强调一下。性别识别本质上属于人脸识别,但是和人脸识别还是有很多方面的区别。性别识别是二分类问题,人脸识别是多分类问题,二者在算法上也有很大差异。我们这里之所以简单的将性别识别看做简化的人脸识别,是因为在这套教程中我们主要注重实践,注重OpenCv的使用以及MFC框架编程方法,因此在算法方面会显得不够严谨。因此希望大家不要被这些简化的观点所误导,真正的性别识别算法也远比这些复杂,也和人脸识别方法大不相同,作为图像处理的行内人,我觉得很有必要把这点说清楚。   2、read_csv函数   这里对read_csv()批量读取函数介绍得相对简洁,大家可以参照我提供的博客来进行详细学习,同时考虑到这个函数相对简洁,可以凡在main()函数之前,从而避免提前声明。   3、数据集原始路径问题   这篇博文中并没有详细介绍如何制作性别识别训练数据集,因此大家在使用网上下载的数据集时一定要注意路径的问题。下载后数据集必须放在E盘根目录下,否则的话则需要重新制作路径文件(at.txt),不过这一步也并不复杂,参见[一种批量读取文件的方法—CSV文件](http://blog.csdn.net/u013088062/article/details/38588281)。   同时,这里在向路径文件后边添加类别标号时,当初我采用的是手动添加的方式,不过我相信大家能够找到更为简便的添加方式。   这里之所以没有介绍数据集的制作,是因为我计划将这部分内容作为程序的一个附加功能来单独进行介绍(也就是所谓的“人脸批量分割”),在之后进入到MFC编程部分时会进行专门的介绍。   4、关于性别识别的其他方法   在接下来的博文中我会介绍性别识别中的另外一种基础方法——SVM方法。  
';

(4)——OpenCv的人脸检测函数

最后更新于:2022-04-01 20:14:15

  这个项目主要包含三部分:人脸检测、特征提取、性别分类: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6384b066d.png)   这篇博客中我们重点介绍OpenCv的人脸检测函数。这篇博客我们先不提MFC,而是在win32控制台下编写一段人脸检测的程序。   一、开启摄像头   我们先讲解如何通过摄像头来采集图像,这听起来更有实际意义。   1、新建工程并配置OpenCv(注意工程类型选择win32控制台应用程序): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6384be108.png)   2、包含头文件   OpenCv2.x版本包含头文件非常方便,一句话搞定: ~~~ #include using namespace cv; using namespace std; ~~~   谈到包含头文件,这里有一个地方需要详细说一下,就是OpenCv2.x之所以操作简洁,是因为其将各个模块的头文件全部置于“opencv.hpp”这个文件中了,右键打开opencv.hpp文档,你会发现如下内容: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6384d66a7.png)   3、初始化一个摄像头捕捉器   首先,需要建立一个摄像头捕捉器,并将其与当前设备中的摄像头相关联: ~~~ /***********初始化一个摄像头捕捉器***********/ CvCapture* capture = cvCreateCameraCapture(0); cvNamedWindow("Camera"); ~~~   注意以"cv"开头的结构体和函数名都是隶属于OpenCv1.x版本中的内容,不过OpenCv2.x是完全兼容1.x版本的,而且貌似在2.x版本并未对摄像头相关函数进行重写,因此这里暂且延用1.x中的代码。   4、调用摄像头步骤画面并显示   首先,给出代码,稍后解释: ~~~ IplImage* cameraImage = NULL; while ((cameraImage = cvQueryFrame(capture)) != NULL) { cvShowImage("Camera",cameraImage); cvWaitKey(1); } ~~~   显然cvQueryFrame()函数的作用是在当前的时间点从摄像头抓取的视频流中截出一帧,这里将其赋值给变量camearImage(IplImage*类型,因为这是1.0的代码),若其非空,则显示在屏幕上。注意这里必须添加延时函数cvWaitKey(单位是毫秒),哪怕只延时一毫秒否则将无法正常显示摄像头输出。   按下Ctrl+F5,程序正常运行: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6384ecfef.png)   二、人脸检测   OpenCv2.x版本中封装的人脸检测函数基于AdaBoost(级联分类器)人脸检测算法,当然这里我们无需深入了解算法相关的知识,因为Intel已经将需要用到的、训练好的人脸检测器(分类器)放在了安装文件里: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63852e8f9.png)   1、准备工作   调用人脸检测函数前需要做一些准备工作,分别是初始化所需内存、初始化检测器指针、设置检测器路径: ~~~ static CvMemStorage* storage = NULL; static CvHaarClassifierCascade* cascade = NULL; const char* cascadePath = "D:\\opencv\\sources\\data\ \\haarcascades\\haarcascade_frontalface_alt_tree.xml"; ~~~   这里有两个问题需要强调:   (1)从路径中可以看出,检测器位于安装目录下的source文件夹下的data文件夹下的haarcascades文件夹中。   (2)在C++表示路径是要用双斜杠,因为第一个斜杠会默认为是转义字符,对第二个斜杠进行转义。   (3)这三个变量均为全局变量。   2、图像灰度化   由于这里用到的人脸检测函数主要针对于灰度图像,因此需要将摄像头采集的彩色图像灰度化: ~~~ /**********灰度化***********/ IplImage* grayImage = cvCreateImage(cvSize(cameraImage->width,cameraImage->height),8,1); cvCvtColor(cameraImage,grayImage,CV_BGR2GRAY); ~~~   这里涉及到如何通过cvCreatImage创建一个空的8位无符号整型单通道图,即需要通过一个cvSize结构体来指定图像初始的尺寸,这点在opencv2.x得到了很大改良(Mat类的加入)。   3、调用人脸检测函数   首先,创建一块内存区域,并加载相应的检测器(这个在主循环外完成即可): ~~~ storage = cvCreateMemStorage(0); cascade = (CvHaarClassifierCascade*)cvLoad(cascadePath); ~~~   然后,清空指定位置内存块,调用人脸检测函数: ~~~ /**********人脸检测***********/ cvClearMemStorage(storage); CvSeq* objects = cvHaarDetectObjects(grayImage,cascade,storage,1.1,2,0,cvSize(30,30)); ~~~   cvhaardetectobjects函数的参数较为复杂,具体参数设置参见:[cvhaardetectobjects参数设置](http://blog.sina.com.cn/s/blog_4c78d3fb0100u8lv.html)。我们这里需要了解的就是这个函数的返回参数是一系列检测结果序列,每个检测结果实际上就是一个矩形结构体对象。   4、绘制人脸区域矩形框   接下来一一绘制检测到的矩形结果: ~~~ /**********绘制检测结果***********/ for (int i = 0; i < (objects ? objects->total : 0); i++) { CvRect* rect = (CvRect*)cvGetSeqElem(objects,i); cvRectangle(cameraImage,cvPoint(rect->x,rect->y), cvPoint(rect->x + rect->width,rect->y + rect->height),cvScalar(0.0,255)); } cvShowImage("Camera",cameraImage); ~~~   注意这里需要把之前测试摄像头程序中的图片显示语句注释掉,否则前后在显示图像时会发生覆盖,不能正常看到图像的检测结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63854468d.png)   5、总程序   这里给出摄像头人脸检测的总程序: ~~~ // Camera.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include using namespace cv; using namespace std; static CvMemStorage* storage = NULL; static CvHaarClassifierCascade* cascade = NULL; const char* cascadePath = "D:\\opencv\\sources\\data\ \\haarcascades\\haarcascade_frontalface_alt_tree.xml"; int _tmain(int argc, _TCHAR* argv[]) { /***********初始化一个摄像头捕捉器***********/ CvCapture* capture = cvCreateCameraCapture(0); cvNamedWindow("Camera"); /***********初始化人脸检测相关变量***********/ IplImage* cameraImage = NULL; storage = cvCreateMemStorage(0); cascade = (CvHaarClassifierCascade*)cvLoad(cascadePath); while ((cameraImage = cvQueryFrame(capture)) != NULL) { //cvShowImage("Camera",cameraImage); cvWaitKey(1); /**********灰度化***********/ IplImage* grayImage = cvCreateImage(cvSize(cameraImage->width,cameraImage->height),8,1); cvCvtColor(cameraImage,grayImage,CV_BGR2GRAY); /**********人脸检测***********/ cvClearMemStorage(storage); CvSeq* objects = cvHaarDetectObjects(grayImage,cascade,storage,1.1,2,0,cvSize(30,30)); /**********绘制检测结果***********/ for (int i = 0; i < (objects ? objects->total : 0); i++) { CvRect* rect = (CvRect*)cvGetSeqElem(objects,i); cvRectangle(cameraImage,cvPoint(rect->x,rect->y), cvPoint(rect->x + rect->width,rect->y + rect->height),cvScalar(0.0,255)); } cvShowImage("Camera",cameraImage); } return 0; } ~~~  
';

(3)——OpenCv配置和ImageWatch插件介绍

最后更新于:2022-04-01 20:14:13

  OpenCv是C++图像处理的重要工具,这个人脸性别识别的项目就是借助OpenCv进行开发的。虽然网上已经有了很多关于OpenCv的配置教程,但出于教程完整性考虑,这里还是用专门的一篇博客来介绍OpenCv的具体配置方法,同时也介绍一下OpenCv中的一个强有力的图像处理插件——ImageWatch。   由于这个程序是一年前写的,当时的OpenCv的最新版本为2.4.9(现在已经更新到了3.0),并且2.4.9版本和3.0版本在配置方法上稍有不同,这里我仍以2.4.9版本为例来介绍配置方法,有关3.0的新特性以及配置方法大家可以参考网络资源。   一、OpenCv的下载安装   首先,给出OpenCv的官方下载地址:[OpenCv下载](http://opencv.org/downloads.html)。   下载完成后,得到一个大约300M左右的exe文件: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382c35f0.png)     双击进行开始安装,输入安装位置,单击Extract按钮: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382d1447.png)     安装过程实质上就是一个解压缩的过程: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382e4349.png)   安装完成后存在两个目录:build和source。Build目录下主要存放了相关的库文件,也就是OpenCv的主体部分。Source目录下主要存放了一些帮助文档和官方提供的资源(例如已经训练好的分类器等): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382f4004.png)     二、配置路径   下载安装完成之后, 启动VS2012,任意打开一个项目。我们这里新建一个空的Win32控制台应用程序。在其中调用OpenCv: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63830f4a0.png)   这里用红色下划线标记了#include语句,说明当前尚未进行OpenCv配置。OK,接下来开始配置。   1、配置VS路径   单击“项目—>属性”,打开属性对话框: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63831d56c.png)     在“配置属性—>VC++目录”节点下,单击“包含目录”右侧的下来按钮: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63832eba8.png)     选择OpenCv目录下的include文件夹路径: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638345510.png)     同理,在“库目录”中添加lib文件夹路径: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638357557.png)     这里有两点需要说明:   (1)X64。在build文件夹下有两个文件X86和X64,X86是针对32位windows系统的,X64是针对64位系统的。由于我的电脑是64位win7,所以理所当然选择了X64,不过需要说明的一点是64位系统下同样可以使用X86下的文件,只要将调试器设置为win32即可。   (2)VC11。在X86和X64文件夹下分别都有三个文件夹:V10、V11、V12。其中V10适用于VS2010,V11使用于VS2012,V12适用于VS2013(当时还未发布VS20150),我们这里选择VS11文件夹。   接下来在“配置属性—>链接器—>输入”节点下,在“附加依赖项”窗口中输入以下OpenCv库文件名称:   opencv_calib3d249d.lib   opencv_core249d.lib   opencv_features2d249d.lib   opencv_flann249d.lib   opencv_gpu249d.lib   opencv_highgui249d.lib   opencv_imgproc249d.lib   opencv_legacy249d.lib   opencv_ml249d.lib   opencv_objdetect249d.lib   opencv_ts249d.lib   opencv_video249d.lib   opencv_contrib249d.lib   opencv_nonfree249d.lib ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638369a40.png)     单击应用,关闭设置窗口。   2、配置环境变量   Windows系统的环境变量几乎是所有编程软件必须折腾的地方,因为编译器在启动时都会通过环境变量来自动读取搜索路径。   至于如何打开环境变量窗口这里就不再赘述,这里需要将OpenCv的bin(可执行文件)目录的路径“”添加到环境变量中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63838a8e1.png)     配置完成后,重启VS,再次输入include命令,发现在VS给出的提示列表中出现了“OpenCv2”这一项,初步认定配置完成: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63839888c.png)     接下来我们编写一个小程序来测试OpenCv:读取F盘根目录下的一张彩色图片,灰度化,然后显示,代码如图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6383a8deb.png)     按下F7进行编译,程序报错,类型为“error LNK2019: 无法解析的外部符号……”。这是因为程序默认使用了32位的Debug调试器,而我们配置的是64位的opencv,因此需要使用64位的Debug调试器。单击工具栏“win32”对应的下拉菜单,选择配置管理器: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6383bd0f7.png)     在弹出的窗口中单击“Win32”下拉按钮,选择“新建”: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6383cec72.png)   新建平台选择X64:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6383e02a5.png)   单击确定,关闭设置对话框,此时我们已经创建了一个64位的Debug调试器,接下来在调试器栏选择这个64位调试器即可: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6383f306b.png)     再次按下F7,编译成功。F5,调试成功。Ctrl+F5,程序顺利运行,显示图片。   三、ImageWatch插件   接下来介绍OpenCv的一个强力的VS辅助插件:ImageWatch。   1、外观   首先强调一点,ImageWatch必须在调试过程中才能起作用,要想使用ImageWatch相应的就要给程序设置一些断点来使程序暂停。首先,给出ImageWatch的外观:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63840fab9.png)   可以看出,ImageWatch能够实时的显示出当前程序中的图片(确切的说是Mat矩阵)信息,包括尺寸、像素、外观等等,着无疑给程序的调试带来了相当的大的便利。接下来我们详细介绍一下这个插件的功能。   2、ImageWatch插件安装   首先,给出一篇我之前写的关于ImageWatch插件教程:[ImageWatch教程](http://blog.csdn.net/u013088062/article/details/42710819)。   ImageWatch官方帮助文档:[帮助文档](http://research.microsoft.com/en-us/um/redmond/groups/ivm/imagewatchhelp/imagewatchhelp.htm#_Toc351981444)。   ImageWatch下载地址:[下载地址](https://visualstudiogallery.msdn.microsoft.com/e682d542-7ef3-402c-b857-bbfba714f78d)。   下载完成后,双击安装即可。安装过程中会自动识别当前已安装的编译器,选择VS2012即可。   安装完成后,需要手动启用这个插件。打开VS,单击“视图—>其他窗口—>ImageWatch”: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63842d98f.png)   此时再次设置断点,F5调试,ImageWatch正常工作。接下来我们介绍它的几个常用功能。   3、常用功能   (1)图片查看   能够实时显示当前图片的状态,缩略图等,这点不必多说: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638444bff.png)   (2)查看像素信息   将鼠标放在右侧预览窗口,上方标题栏会实时显示当前鼠标指针所在像素的位置坐标以及像素值、缩放比例(从左往右依次是坐标值和像素值、缩放比例): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638467505.png)   在预览窗口中按下“Ctrl+鼠标滚轮”,会对图片进行放大和缩小,直至放大到可以看清像素值(彩色图像由三个值,RGB): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63848585e.png)   4、保存图片   ImageWatch可以方便的将当前的图片保存下来,只需在对应图标上单击右键,在快捷菜单中选择“Dump to File”即可: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63849872f.png)   ImageWatch还有更多功能,例如图像的关联显示等等,详细功能请大家参加之前提供的博客以及官方帮助文档,这里就不再赘述。   四、总结   这是本套教程准备部分的最后一篇文档,在接下里的博客中我们将开始编写代码。这里说几个需要注意的事项:   (1)OpenCv还是建议大家用新版。OpenCv的发展经历了1.x、2.x,到如今的3.0版。1.x版本的OpenCv只提供了C语言的接口,其中的所有的API函数都以“cv”开头,只有结构体,没有类结构,用Iplimage指针类型表示图像等等。不等不说,OpenCv的发展趋于完善,2.4系列版本是公认的成熟版本。不过前两天我在浏览OpenCv官网的时候发现在3.0版本中出现了很多比较新的算法,例如DeepLearning,因此建议搞图像处理算法的同行们还是去尝试使用一下3.0版本吧。   (2)写教程要多用图。这段话可能说得让大家有些莫名其妙,但我觉得有必要拿出来说一说。以前我写教程时大多长话连篇、恨不得连代码都想用语言叙述出来,后来我在翻译《最全Pycharm教程》的过程中,深深体会到了图文并茂的重要性,也深深的体会到了歪果仁在编程、叙事方面的严谨。所以我也吸取了经验,用图说话,于是你就看到了这篇不到2500字,但有着23张图的文章,以后的文章也是这样,不知道大家能否适应哈。
';

(2)——VisualStudio初探

最后更新于:2022-04-01 20:14:11

  上一篇教程中已经大致描述了项目的最终效果,考虑到读者中有很多零基础的同学,我们这里并不急于进行代码的编写,而是先简要介绍下所用到的开发工具——VisualStudio2012。   VisualStudio是微软推出的非常强大的开发软件,在C++开发领域可谓占据了半壁江山。VisualStudio经典版本主要有2005、2008、2010、2012、2013、2015等版本。2005和2008现在已经稍显过时,现在应用已经不太广泛。目前2010、2012、2013是目前较为流行的版本,免费版本也相对容易找到。2015是最新发布的版本,文件较大(大约5个G),对硬件要求相对苛刻一点,并且不容易找到免费版本。这里推荐使用VS2012版本,原因有两个:一是VS2012版本还不算过时,而且界面相比08、10版本更为友好;二是VS2012支持一个重要的OpenCv插件:ImageWatch。   一、启动VS2012   这里假设已经安装了VS2012并破解完成,相关的安装教程网上有很多资源,大家如果有疑问的话可以在下方留言,有必要的话我会上传相关的安装文件以及破解方法。 双击快捷方式(开始菜单栏、桌面快捷方式、任务栏中的快速启动图标均可),首次启动会提示选择编程语言种类,这里选择C++即可(可能需要几分钟的初始化时间)。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638191d07.png)   接下来进入开始界面: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6381a4c55.png)     二、基本设置    1、主题设置   首先,VS2012默认提供两种主题,深色和浅色。通过“工具—>选项”主菜单命令打开设置对话框,展开“环境”节点,在“常规”设置界面中进行设置: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6381ba05a.png)   2、窗口布局   观察当前VS所显示的窗口布局,推荐大家将VS的界面调整为这种格式,方便编写程序。   首先在窗口的右侧显示三个编辑窗口,分别为解决方案资源管理器、类视图、资源视图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6381d5cf4.png)     这三个窗口将以三种模式来显示当前的工程结构,这里我们打开工程文件,对这三个窗口进行详细介绍。Ctrl+O,弹出打开工程对话框,选择对应的project文件: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6381e651e.png)   在解决方案管理器窗口中,将以头文件和源文件的形式显示工程目录: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638214468.png)     在类视图窗口中,将显示当前程序工程中的所有类结构: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382260ec.png)     在资源视图窗口中,显示当前工程所用到的所有资源,包括图片、对话框、字符串表等: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382328a4.png)     一般在创建、添加文件时使用解决方案资源管理器;在编写代码(尤其是大型的、面向对象的代码工程)使用类视图,方便定位相应的类以及成员函数;在资源视图中完成MFC界面设置以及相关资源的查找引用等。   3、基本操作   这里简单的介绍一些VS2012在编写代码过程中的常用操作,这里主要介绍快捷键,当然相应的功能也可以通过主菜单栏的菜单命令和快捷菜单命令来完成。   (1)拼写提示   VS一个非常让人舒服的功能就是在敲代码时提供了拼写提示的功能,这也是高级IDE必备的功能之一,在你输入代码的过程中,VS会自动根据你当前输入的情况给出提示信息: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382403a1.png)   (2)编译链接   按下F7,VS会对当前代码进行编译并链接相关库文件。   (3)调试   如果编译顺利通过,就可以进行调试。在调试之前需要先设置断点(当然不设置也可以)。VS设置断点的方法有三种:菜单、快捷键、鼠标单击左槽。这里推荐使用快捷键。将光标定位在指定行,按下F9设置断点: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638250a5e.png)   这里介绍一个取消所有断点的快捷键组合:ctrl+shift+F9。   断点设置完成后,按下F5,程序开始调试,并在命中断点后暂时挂起。   (4)运行   调试通过后,按下ctrl+F5运行程序即可。   三、注意事项   1、MFC相关知识   这个项目在开发过程中用到了大量的MFC编程,这在以后使用过程中在详细介绍,需要说的是MFC空间的操作都通过工具箱窗口进行,这里通过“视图—>工具箱”命令调出工具箱窗口,并将其置于窗口右侧即可: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382665d1.png)     2、调试器的版本问题   这里我的电脑是用的64位,而VS在调试是默认为win32: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638279a76.png)     考虑到我们之后要配置的opencv是64位的,一些底层的编译环境(如g++等)也是64位的,因此这里建议换成64位的调试器:单击“Win32”右侧的下拉箭头,选择配置管理器: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382894bd.png)     在弹出的窗口中,单击下拉箭头,选择新建: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638297fbe.png)   在弹出窗口中选择X64: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6382ad5b5.png)     注意左侧生成栏下面的对话框需要勾选。单击确定,关闭当前窗口。此时再次单击“Win32”右侧的下拉箭头,发现其中多了一个X64的选项,选中即可。   四、总结   这篇教程总结了VS2012的一些基本操作和注意事项,帮助之前零基础的朋友快速入门,更多有关VS2012的相关配置大家可以参考网上资源。在下一篇教程中我们将会简要介绍OpenCv在VS2012中的配置以及插件ImageWatch的使用。
';

(1)——前瞻

最后更新于:2022-04-01 20:14:08

  大四暑假的时候,帮老师指导了一个本科大学生创新实验,主要目标是通过图像处理相关技术对人脸美丽度进行分类。其中一个很重要的环节就是人脸的性别识别,这里将这个部分单独拿出来,借住OpenCv这个开源的图像处理库,在MFC框架下编写了一个人脸性别识别的程序,本套教程将详细介绍编写过程。   需要提前说的一点是这个程序是在大四的时候编写的,当时自己还没有正式的进行图像处理方面的研究,编程经验也不够丰富,因此程序只用了OpenCv提供的较为经典的人脸检测和人脸识别算法,在准确度、程序设计、异常机制处理上可能显得不够完善。在制作这套教程之前我并没有刻意去完善它,一是想好好审视下自己两年前的编程水平,二是保持程序的简洁性,使广大读者更容易看懂。教程编写完成后,我会再花些时间来完善自己当年这个处女作。   一、程序效果演示   首先给出程序的最终效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6380ace12.png)   从图中可以看出程序一共分为如下几个模块:方法选择、误差补偿及初始化模块;视频性别识别模块;图像性别识别模块;辅助功能;图片显示区域;结果显示区域。   二、功能介绍   1、方法选择、误差补偿及初始化模块   这里一共有三个控件:初始化按钮,方法选择复选框,误差补偿值。初始化按钮与初始化事件相关联,主要是加载相关分类器(会在后面教程中详细介绍),这里的分类器主要包含一个人脸检测分类器以及三个性别识别分类器。单击初始化按钮,初始化完成后程序会给出对应提示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6380d72d7.png)   注意如果用户在尚未进行初始化的情况下进行其他操作,程序会弹出对话框提示用户先进行初始化: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6380e7353.png)   紧接着是方法选择列表,这里面提供了四种人脸性别识别的方法供用户选择,分别是PCA变换、Fisher变换、LBP变换、支持向量机: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638102d3c.png)   然后是误差补偿模块,用来补偿程序存在的一些固定误差,主要用在视频性别识别中,会在以后的教程中详细介绍。这里同样以一个列表控件的形式供用户选择: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63811102a.png)   2、视频性别识别模块   这个模块主要实现视频信号的性别识别,默认由摄像头采集视频。主要包含两个按钮,一个是开始按钮“打开视频”,另一个是暂停按钮。功能很简单,单击“打开视频”按钮,程序会自动检测当前设备上所安装的摄像头,并调用指定摄像头采集视屏,同时对视频进行人脸检测、性别识别,在图片显示区域显示实时的人脸检测结果,在结果显示区域显示性别识别结果。   这里注意的是模块中的暂停按钮具有复用功能。因为这个程序具有两个功能,对摄像头视频进行性别识别以及对单张图片进行性别识别。当程序在处理视频是,这个按钮的功能是暂停/恢复当前视频;在对单张图片进行性别识别时,这个按钮的功能是自动读取文件夹下的下一张图片。在视频处理时按钮的状态为: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638122474.png)   在识别单张图片时按钮的状态为: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce6381304ca.png)   3、单张图像性别识别模块   这里同样包含两个按钮,一个“图片文件夹”按钮(这个按钮同样具有复用功能),一个静态文本框区域,注意着两个控件是相互关联的。在对图片进行性别识别时,程序提供了两个工作模式:   模式一:选定一个文件夹,通过视频性别识别模块中的“下一张”按钮自动加载文件夹下所有图片,无需重复选择。   模式二:直接选择一个图片文件进行操作。   程序初始默认为模式一,即文件夹模式。此时单击模块中的“图像文件夹”按钮,将会弹出对话框,提示我们选择一个文件夹: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63813fb95.png)   我们在“双击此处转换模式”的静态文本框区域双击鼠标,将会切换到模式二: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638157206.png)   此时按钮的文本变为“图片文件”,单击会打开一个对话框要求用户具体选择一张图片来进行处理: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce638164f34.png)   注意这里两个模式下所弹出的文件对话框是不同的。再次双击可切换回原来的模式。   4、辅助功能模块   这部分模块中主要包含三个按钮:人脸批量分割、文件名修改、方法验证。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-25_56ce63817fac6.png)   这些都是我在编写程序的过程中用到的一些辅助性功能。单击“人脸批量分割”按钮,会弹出一个文件选择对话框,提示用户选择一个文件夹,然后程序会自动检测文件夹下的所有图片文件,对其进行人脸检测,并将检测到的人脸区域图片批量保存到程序制定路径下。同样,如果单击“文件名修改”按钮,同样会弹出一个对话框,提示用户选择文件夹,然后程序会对所有图片的名称按照指定格式进行统一修改,并保存。至于“方法验证”按钮,是一个测试按钮,主要是在开发程序的过程中用作调试按钮。   5、图片显示区域和结果显示区域   主要由一个picture控件和三个编辑框组成,用来显示当前的图像信息以及识别结果。其中“男/女识别率”这两个控件是用来在仿真试验中测试识别方法的效果的,将会在后续章节进行详细介绍。   三、小结   本篇教程主要介绍了程序的最终效果,让大家对本教程的最终目的有一个大致的了解。最后再次强调,这个程序是我本科阶段编写的,肯定会有很多不足之处,在处理图像时也用的都是经典算法。如果你希望从教程中了解人脸检测、人脸识别等方面最前沿的算法,那这篇教程可能会让你失望。但如果你希望了解MFC的入门级编程方法、图像处理的基本知识以及OpenCv的基本应用,相信你能从这套教程中学到一些东西。               
';

前言

最后更新于:2022-04-01 20:14:06

> 原文出处:[C++开发人脸性别识别教程](http://blog.csdn.net/column/details/genderrecogtion.html) 作者:[u013088062](http://blog.csdn.net/u013088062) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # C++开发人脸性别识别教程 > 使用OpenCv,在MFC框架下开发的一个简易的人脸性别识别教程,大约有20篇博文组成,图文并茂,介绍较为详细。
';