在之前的博文中我们的性别识别程序已经初步成型,能够识别某个文件夹下的图片文件。不过这里有一个问题,假设这个文件夹下有着大量的图片,而我们希望识别这些图片中的某一张,此时需要我们不停的单击“下一张”按钮才会轮询到对应的图片,这是相当麻烦的,因此在这篇博客中我们向程序中添加一个功能——单张图片的性别识别。
一、基本思想
最基本的办法就是在主界面再添加一个按钮控件,命名为“图片文件”(之前的按钮为“图片文件夹”),不过这样会使得界面上的按钮控件过于繁多,给人一种“作者只会用button控件”的感觉。这里我们决定用一种相对巧妙的方式来解决这个问题,即向之前的“图片文件夹”button控件在添加一个读取单张图片的功能,然后再通过某一操作来进行两个功能间的切换,这里我们使用鼠标双击的操作。所以最终的效果就是:我们双击一下鼠标,就会从“图片文件夹”模式转换到“单张图片”模式(或者从“单张图片”模式转换为“图片文件夹”模式)。
二、添加鼠标双击的响应事件
MFC程序是基于消息响应机制的,关于这点一两句话也说不清楚,推荐大家去看孙鑫老师的MFC教学视频。在这里我们需要通过双击鼠标左键来触发一个消息,在消息响应函数中进行图片读取的模式反转。首先,我们向类中添加双击鼠标左键的事件响应函数。在类视图窗口中,右击CGenderRecognitionMFCDlg类,选择属性:
在打开的属性对话框中,单击“消息”图标,在消息列表中找到WM_LBUTTONDBLCLK消息,单击右侧的下拉按钮,选择“add OnLButtonDblClk”命令:
此时,鼠标的双击消息响应函数添加完成:
三、编写双击消息响应函数
1、添加模式标记
接下来我们开始编写鼠标双击后对应的执行代码OnLButtonDblClk()。首先,我们需要一个布尔变量来记录当前处于何种模式(是“图片文件夹”模式还是“单张图片”模式),这里有两个选择,一是将这个变量定义为全局布尔变量,二是定义为类的成员变量,首选成员变量。因此需要向CGenderRecognitionMFCDlg添加一个bool变量m_boolFolderOrImage来进行标记:
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,此时运行程序,如预期结果:
五、总结
在写这篇博文所对应的程序中,我发现了一个非常严重,同时隐藏的也非常深的BUG,这个BUG直接导致我们的程序的一部分逻辑出现错误,确切的说是有一部分代码被架空,更为严重的是这个BUG并不会导致程序运行的问题,很难被发现,在此先向各位关注《C++开发人脸性别识别教程》系列博客的读者表示歉意,我会在下一篇博文中专门对这个BUG进行更正,同时对界面进行一点小小的美化。