在之前的博文中已經將性別識別部分敘述的基本完整,整個程序的開發也接近尾聲,在這篇博文中我們再為程序添加小的輔助功能:人臉批量分割。
一、人臉批量分割
在前面的博文中提到過,進行性別識別訓練所用到的訓練樣本是分割好的男性人臉樣本和女性人臉樣本,那麼如何去制作這些訓練樣本呢?這就需要進行人臉圖像的批量人臉分割。
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()函數再增加一個形參intnumber2save,用來接受待保存的圖像編號。注意這裡在對函數進行更改時,需要在兩處進行更改,一是函數聲明的部分,二是函數定義部分。首先在類視圖中找到這個函數的定義,將其更改為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<將這部分代碼添加到性別識別操作之後、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方法。