在之前的博文中我們的性別識別程序已經初步成型,能夠識別某個文件夾下的圖片文件。不過這里有一個問題,假設這個文件夾下有著大量的圖片,而我們希望識別這些圖片中的某一張,此時需要我們不停的單擊“下一張”按鈕才會輪詢到對應的圖片,這是相當麻煩的,因此在這篇博客中我們向程序中添加一個功能——單張圖片的性別識別。
一、基本思想
最基本的辦法就是在主界面再添加一個按鈕控件,命名為“圖片文件”(之前的按鈕為“圖片文件夾”),不過這樣會使得界面上的按鈕控件過于繁多,給人一種“作者只會用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進行更正,同時對界面進行一點小小的美化。