經過之前幾篇博客的講解,我們已經成功搭建了MFC應用框架,並實現了基本的圖像顯示和人臉檢測程序,在這篇博文中我們要向其中添加性別識別代碼。
關於性別識別,之前已經專門拿出兩篇博客的篇幅來進行講解,這裡不再贅述,具體參見:C++開發人臉性別識別教程(5)——通過FaceRecognizer類實現性別識別和C++開發人臉性別識別教程(6)——通過SVM實現性別識別。
一、分類器訓練
在進行人臉性別識別之前需要訓練性別識別的分類器,而分類器的訓練過程是相對耗時的(大約五分鐘),因此這裡我們采用離線訓練在線識別的模式,即提前將分類器訓練好,作為程序的數據進行保存,程序運行過程中直接加載已經訓練好的分類器進行性別分類,這樣速度就會大大提高。
在上面提供的兩篇博客中都詳細介紹了性別識別分類器的訓練方法,這裡一共需要訓練四種分類器,分別是PCA、Fisher、LBP、SVM:
二、添加下拉列表控件
1、繪制控件
由於這裡有四種性別識別的方法,因此在程序運行時,需要用戶指定一種性別識別的方法,這裡提供一個下拉選擇列表(Combo Box)控件來供用戶選擇。首先從工具箱中選中該控件,在MFC主窗口的合適位置進行繪制,並將ID更改為IDC_COMBO_FUNCTION:
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變換“:
這裡有兩個小細節需要注意:
(1)需要提前指定Combo Box的下拉范圍,這樣才能保證在單擊下拉按鈕時控件能夠將所有選項全部顯示出來:
(2)Combo Box控件的”sort“屬性,應該置為”false“:
三、添加性別識別算法
繪制完ComboBox控件之後,開始向其中填入性別識別算法。
1、全局變量聲明
在之前性別識別的博客中介紹得很清楚,在使用OpenCv封裝的分類器之前,需要聲明幾個靜態的模板變量,我們這裡將其聲明為全局變量,放在GenderRecognitionMFCDlg.cpp文件的開頭部分:
/************初始化性別分類器************/ static Ptrmodel_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類中:
然後再向CGenderRecognitionMFCDlg類中添加一個int類型的標簽,用來保存對當前圖片的預測結果(“1”代表男性,“2”代表女性):
接下來開始編寫性別識別函數,與之前加載分類器的流程類似,這裡同樣需要判斷用戶所選擇的方法的標號,然後調用對應的分類器對輸入圖片進行預測,不過這裡需要先將輸入的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實現性別識別。同時要將測試樣本先歸一化到和訓練樣本相同的尺寸,這裡為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 總結。
考慮到在進行人臉檢測時會出現檢測失敗的情況,如果我們在人臉檢測失敗的情況下仍堅持啟用人臉分割及性別識別程序,程序就會因為各種變量的未定義而崩潰,因此我們這裡選擇將這段人臉分割、性別識別的代碼放在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,大功告成:
四、總結
經過這篇博客之後,可以說我們的性別識別MFC程序已經基本成型,擁有了圖片讀取與顯示,人臉檢測、性別識別等基本功能,在接下來的博文中我們將介紹如何進行攝像頭視頻流的人臉性別識別。不過這裡有幾個問題需要再次強調一下。
1、分類器種類
之前我們說程序中用到了四種性別識別分類器:PCA、Fisher、LBP、SVM。其實這種說法是不嚴謹的,這裡只是有四種API函數,而從分類器層面上將只有兩種分類器。前面三個本質上都是用的K近鄰分類器,只是提取了三種不同的特征而已。
2、MFC教程
在這個程序的開發過程中用到了很多MFC的相關知識,如果大家希望系統了解MFC開發的相關注意事項及技巧的話,推薦大家參考孫鑫老師的MFC視頻教程。這個視頻教程比較長,大家有選擇性的學習即可。
3、添加初始化完成的提示對話框
這裡我們向“初始化”按鈕的響應函數中添加了初始化完成的提示對話框,原因是加載分類器的過程需要大約5秒左右的時間,添加一個完成提示對話框會使得程序顯得更有提示性,更友好。
4、resource.h文件的功能
resource.h保存了當前資源(各種空間,圖片,字符串)的ID號,必要時大家可以從這個文件中查找:
5、全局變量
程序中不推薦使用靜態的全局變量,會降低程序的安全性。