在之前的博文中已经将性别识别部分叙述的基本完整,整个程序的开发也接近尾声,在这篇博文中我们再为程序添加小的辅助功能:人脸批量分割。
一、人脸批量分割
在前面的博文中提到过,进行性别识别训练所用到的训练样本是分割好的男性人脸样本和女性人脸样本,那么如何去制作这些训练样本呢?这就需要进行人脸图像的批量人脸分割。
1.1 添加控件
首先添加一个“人脸批量分割”的按钮,ID采用默认值即可:
1.2 批量读取图片
双击按钮,生成对应的事件处理函数OnBnClickedButton1(),在其中加入对应代码。人脸批量分割的主要工作在于图片的批量读取,这里先给出整体代码,稍后解释:
void CGenderRecognitionMFCDlg::OnBnClickedButton1() { // TODO: 在此添加控件通知处理程序代码 /**********判断是否已经加载好了分类器**********/ if (m_boolInitOK == FALSE) { MessageBox("请先进行初始化"); return; } /**********打开图片文件夹**********/ CString str; //存储图像路径 BROWSEINFO bi; //用来存储用户选中的目录信息 TCHAR name[MAX_PATH];//存储路径 name[0]='d'; 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 if(idl==NULL) return; 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 < 2; i ++) //过滤目录 .. 和 . { m_pEnt = readdir(m_pDir); } int SaveNum = 0; while (m_pDir && (m_pEnt = readdir(m_pDir)) != NULL) { //判断名字中有没有 .jpg .bmp .png char* pJpg = strstr(m_pEnt->d_name,".jpg"); char* pBmp = strstr(m_pEnt->d_name,".bmp"); char* pPng = strstr(m_pEnt->d_name,".tif"); char* pJPG = strstr(m_pEnt->d_name,".JPG"); if(pJpg==NULL && pBmp==NULL && pPng==NULL && pJPG==NULL) { continue; } SaveNum = SaveNum + 1; char imageFullName[500]; //拼出文件的全路径 sprintf_s(imageFullName,"%s%s",m_ImageDir,m_pEnt->d_name); IplImage* src; CvvImage srcCvvImg; //加载图像 src = cvLoadImage(imageFullName); /**********人脸检测并保存**********/ //绘制图像到控件 srcCvvImg.CopyOf(src); srcCvvImg.DrawToHDC(m_pPicCtlHdc,&m_PicCtlRect); cvReleaseImage(&src); } }
这里在进行图片批量读取时主要借用了之前C++开发人脸性别识别教程(8)——搭建MFC框架之读取文件夹信息和C++开发人脸性别识别教程(9)——搭建MFC框架之显示图片这两篇博客中的文件批量读取方法,只是这里通过“while (m_pDir && (m_pEnt = readdir(m_pDir)) != NULL)”语句自动完成了“读取下一张图片”的操作(之前是通过单击“下一张”按钮来手动完成文件夹下下一张图片的读取),这样程序就能够自动实现文件夹下所有图片文件的遍历。还有一点需要注意的就是在批量保存分割后的人脸过程中,需要对待保存的文件进行批量的、统一格式的命名,这里采用变量SaveNum的累加来对当前图片进行计数,同时为命名提供依据。
1.3 人脸分割与保存
这里需要稍稍对人脸检测函数detect_and_draw()做一点改进,为其加入一个新的功能:批量保存。实在这个目的的方法有很多种,这里采用增加函数的形参并将其作为标志位的方法,具体方法如下:
首先,为detect_and_draw()函数再增加一个形参int number2save,用来接受待保存的图像编号。注意这里在对函数进行更改时,需要在两处进行更改,一是函数声明的部分,二是函数定义部分。首先在类视图中找到这个函数的定义,将其更改为detect_and_draw(IplImage* img,int number2save = 0);然后右击该函数,选择“转到定义”,将函数的声明形式也改为detect_and_draw(IplImage* img,int number2save = 0),至于这里为何需要将number2save的形参缺省值设置为0,稍后给出解释。
然后,开始改造detect_and_draw()的函数体。这里给出number2save缺省值的好处就是能够根据它的值来判断当前是否需要对分割后的人脸进行批量保存。由于我们在进行人脸批量保存的过程是在调用detect_and_draw()函数时同时向其中传入当前图片的计数值,因此接下来只需在函数内部来判断number2save是否为零,如果其为缺省值零,则说明当前值需要进行人脸检测,无需分割;若为非零则说明当前需要进行批量分割与保存。接下来向detect_and_draw()其中加入批量保存的代码:
/**********如果手工传入第二个参数,则说明需要进行脸部图像的保存**********/ if (0 != number2save) { Mat image(faceImage); stringstream ss; ss<<number2save; string s1 = ss.str(); string str = "E:\\教学视频1\\" + s1 + ".bmp"; imwrite(str,image); }
将这部分代码添加到性别识别操作之后、faceImage变量被释放之前即可。这里进行了一步IplImage*类型到Mat类型的转换,是为了方便使用2.x版本中的图像保存函数imwrite()。当然在这里需要指定人脸图片批量保存的位置,这里将其放在E盘的“教学视频1”文件夹下。
改造完成之后,在“人脸批量检测”按钮对应的事件触发函数OnBnClickedButton1()中调用这个函数(绘制图像到控件之前)即可,同时向其中传入图片计数值:
/**********人脸检测并保存**********/ detect_and_draw(src,SaveNum);
OK,此时运行程序,初始化,单击“人脸批量检测”按钮,选择待进行人脸检测的文件夹,程序会自动开始进行人脸检测,并将检测到的人脸分割并保存在指定文件夹下。
二 注意事项
1、默认形参的意外收获
这里在更改人脸检测识别函数detect_and_draw()时将第二个形参设置了缺省值,其实这种做法无形中给我们提供了很大方便。因为程序编写到现在已经在很多地方用到了这个函数,而这些已有的用法无一例外的都是采用的detect_and_draw((IplImage* img)的形式,如果我们这时候对函数重新添加形参,理论上对这些就的函数用法都应该进行更改,但是如果为这个形参指定了缺省值,则无需再对其已有的函数调用进行更改,因为detect_and_draw((IplImage* img)这种调用形式只是采用了默认的形参值,依然合法。
2、C++缺省参数设置规范
在添加形参缺省值需要注意,默认参数只可在函数声明中设定一次,只有在没有函数声明时,才可以在函数定义中设定。因此在函数声明时采用detect_and_draw(IplImage* img,int number2save = 0);在函数定义时则应为detect_and_draw(IplImage* img,int number2save)的形式,这个小问题一定要注意。
3、int类型转换为string类型
在对人脸图片进行批量保存时,关键的一步是将计数值(int型)转换为文件名(字符创string类型),这里采用了stringstream方法,当然也可以用其他方法,具体参见C++ int与string的转化。不过这里有一个问题需要强调,就是有关format方法的知识。format同样可以将整形值转换成字符串类型,但这里转换后的字符串是CString类型的,在这里直接使用CString类型的话会报错,具体原因我会专门写一篇博文来说明,总之这里不推荐使用format方法。