1. 程式人生 > >C++開發人臉性別識別教程(6)——通過SVM實現性別識別

C++開發人臉性別識別教程(6)——通過SVM實現性別識別

  上一篇教程中我們介紹瞭如何使用OpenCv封裝的FaceRecognizer類實現簡單的人臉性別識別,這裡我們為大家提供另外一種基本的性別識別手段——支援向量機(SVM)。

  支援向量機在解決二分類問題方面有著強大的威力(當然也可以解決多分類問題),性別識別是典型的二分類模式識別問題,因此很適合用SVM進行處理,同時OpenCv又對SVM進行了很好的封裝,呼叫非常方便,因此我們在這個性別識別程式中考慮加入SVM方法。

  在這裡我們採用了HOG+SVM的模式來進行,即先提取影象的HOG特徵,然後將這些HOG特徵輸入SVM中進行訓練。

  一、SVM概述

  SVM的數學原理十分複雜,我們不在這裡過多討論,有關OpenCv中SVM的用法,這裡為大家提供兩篇部落格以供參考:

OpenCV的SVM用法以及OpenCV 2.4+ C++ SVM介紹

  二、HOG特徵概述

  三、建立訓練集

  這裡繼續沿用上一篇博文中提到的性別識別訓練集,400張男性人臉樣本400張女性人臉樣本,下載地址:性別識別資料集

  四、演算法的訓練與測試

  1、建立控制檯工程,配置OpenCv環境

  這裡將工程命名為:GenderSVM。

  2、編寫批量讀取函式read_csv()

  只要涉及到訓練,都需要批量讀取訓練樣本的操作,SVM也不例外,因此需要先編寫批量讀取函式read_csv()。考慮到之前的批量讀取函式必須一次性將所有訓練樣本讀入記憶體中,記憶體消耗較大,在這裡做一個小小的改進:

void read_csv(String& csvPath,Vector<String>& trainPath,Vector<int>& label,char separator = ';')
{
    string line,path,classLabel;
    ifstream file(csvPath.c_str(),ifstream::in);
    while (getline(file,line))
    {
        stringstream lines(line);
        getline(lines,path,separator);
        getline(lines,classLabel);
        
if (!path.empty()&&!classLabel.empty()) { trainPath.push_back(path); label.push_back(atoi(classLabel.c_str())); } } }

  可見這裡我們將輸入引數由vector<Mat>改為vector<String>,然後返回裝有訓練樣本的所有路徑的容器,需要時在根據其中的路徑進行讀取,降低了記憶體佔用量。

  3、讀入訓練樣本路徑

    string trainCsvPath = "E:\\性別識別資料庫—CAS-PEAL\\at.txt";
    vector<String> vecTrainPath;
    vector<int> vecTrainLabel;
    read_csv(trainCsvPath,vecTrainPath,vecTrainLabel);

  順利批量讀入路徑:

  4、訓練初始化

  在提取HOG特徵之前,需要初始化訓練資料矩陣:

    /**********初始化訓練資料矩陣**********/
    int iNumTrain = 800;
    Mat trainDataHog;
    Mat trainLabel = Mat::zeros(iNumTrain,1,CV_32FC1);

  需要強調的是SVM的訓練資料必須都是CV_32FC1格式,因此這裡顯式的將標籤矩陣trainLabel初始化為CV_32FC1格式,trainDataHog稍後進行初始化。

  5、提取影象HOG特徵

  接下來迴圈讀入所有的訓練樣本,提取HOG特徵,放在訓練資料矩陣中。考慮巢狀程式碼的複雜性,這裡先給出整體程式碼,稍後解釋:

    /**********提取HOG特徵,放入訓練資料矩陣中**********/
    Mat imageSrc;
    for (int i = 0; i < iNumTrain; i++)
    {
        imageSrc = imread(vecTrainPath[i].c_str(),1);
        resize(imageSrc,imageSrc,Size(64,64));
        HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
                                               cvSize(8,8),cvSize(8,8),9); 
        vector<float> descriptor;
        hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0));

        if (i == 0)
        {
            trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1);
        }

        int n = 0;
        for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
        {
            trainDataHog.at<float>(i,n) = *iter;
            n++;
        }
        trainLabel.at<float>(i,0) = vecTrainLabel[i];
    }

  接下來我們對這段程式碼進行詳細解釋。

  (1)迴圈讀入訓練樣本

  從vecTrainPath容器中逐條取出訓練樣本路徑,然後讀取:

        imageSrc = imread(vecTrainPath[i].c_str(),1);

  (2)尺寸歸一化

  我們這裡將影象尺寸歸一化為64*64,這是因為當時在寫程式時參考了一篇關於HOG特徵的部落格。這裡的尺寸大家可以隨意設定,當然也會影響最終的識別效率,64*64可能並不是一個最優的尺寸:

        imageSrc = imread(vecTrainPath[i].c_str(),1);
        resize(imageSrc,imageSrc,Size(64,64));

  (3)計算HOG特徵

  OpenCv給出的HOG特徵計算介面非常簡潔,三句話即完成:

        HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
                                               cvSize(8,8),cvSize(8,8),9); 
        vector<float> descriptor;
        hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0));

  提取的特徵以容器的資料 結構形式給出。至於計算時的引數設定,參見我之前提供的那兩篇部落格即可。

  (4)初始化資料矩陣trainDataHog

  前面提到,SVM中用到的訓練資料矩陣必須是CV_32FLOAT形式的,因此需要對資料矩陣顯示的指定其尺寸和型別。然後由於trainDataHog行數為訓練樣本個數,而列數為圖片HOG特徵的維數,因此無法在進行HOG特徵提取之前確定其尺寸,因此這裡選擇在進行完第一張樣本的HOG特徵、得到對應維數之後,在進行初始化:

        if (i == 0)
        {
            trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1);
        }

  (5)將得到的HOG特徵存入資料矩陣

  得到的HOG特徵是浮點數容器的形式,我們需要將其轉換成矩陣的形式以便於訓練SVM,這就涉及到了vector和Mat兩個資料結構的遍歷。vector遍歷這裡推薦使用迭代器的方式,而Mat遍歷這裡則選擇了相對耗時但是最簡單的方式——直接使用at函式:

        int n = 0;
        for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
        {
            trainDataHog.at<float>(i,n) = *iter;
            n++;
        }
        trainLabel.at<float>(i,0) = vecTrainLabel[i];

  訓練得到的HOG特徵如圖所示:

  可見在當前的引數設定下,提取到的HOG特徵為1764維,共800張訓練樣本,每一行代表一個圖片的HOG特徵向量。通過“ctrl+滑鼠滾輪”放大觀察特徵向量的具體引數:

  6、訓練SVM分類器

  有關OpenCv中SVM分類器的使用可以參見以下部落格:OpenCV 2.4+ C++ SVM介紹

  首先,初始化相關引數:

    /**********初始化SVM分類器**********/
    CvSVM svm;  
    CvSVMParams param;    
    CvTermCriteria criteria;      
    criteria = cvTermCriteria( CV_TERMCRIT_EPS, 1000, FLT_EPSILON );      
    param = CvSVMParams(CvSVM::C_SVC, CvSVM::RBF, 
                        10.0, 0.09, 1.0, 10.0, 0.5, 1.0, NULL, criteria );  

  開始訓練、訓練完成後儲存分類器:

    /**********訓練並儲存SVM**********/
    svm.train(trainDataHog,trainLabel,Mat(),Mat(),param);
    svm.save("E:\\性別識別資料庫—CAS-PEAL\\SVM_SEX_Model.txt");

  注意我們這裡選擇將分類器儲存為txt形式:

  當然,我們可以開啟這個txt檔案,檢視裡面的引數:

  7、測試分類效果

  測試過程和訓練過程基本相同,讀取圖片、尺寸歸一化、提取HOG特徵、預測:

    /**********測試SVM分類效能**********/
    Mat testImage = imread("E:\\性別識別資料庫—CAS-PEAL\\測試樣本\\女性測試樣本\\face_35.bmp");
    resize(testImage,testImage,Size(64,64));

    HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
        cvSize(8,8),cvSize(8,8),9); 
    vector<float> descriptor;
    hog->compute(testImage,descriptor,Size(1,1),Size(0,0));

    Mat testHog = Mat::zeros(1,descriptor.size(),CV_32FC1);
    int n = 0;
    for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
    {
        testHog.at<float>(0,n) = *iter;
        n++;
    }

    int predictResult = svm.predict(testHog);

  8、完整程式碼

 這裡給出HOG+SVM進行性別識別的完整程式碼:

// GenderSVM.cpp : 定義控制檯應用程式的入口點。
//

#include "stdafx.h"
#include <opencv2\opencv.hpp>
#include <iostream>
#include <sstream>
#include <fstream>

using namespace std;
using namespace cv;

void read_csv(String& csvPath,vector<String>& trainPath,vector<int>& label,char separator = ';')
{
    string line,path,classLabel;
    ifstream file(csvPath.c_str(),ifstream::in);
    while (getline(file,line))
    {
        stringstream lines(line);
        getline(lines,path,separator);
        getline(lines,classLabel);
        if (!path.empty()&&!classLabel.empty())
        {
            trainPath.push_back(path);
            label.push_back(atoi(classLabel.c_str()));
        }
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    /**********批量讀入訓練樣本路徑**********/
    string trainCsvPath = "E:\\性別識別資料庫—CAS-PEAL\\at.txt";
    vector<String> vecTrainPath;
    vector<int> vecTrainLabel;
    read_csv(trainCsvPath,vecTrainPath,vecTrainLabel);

    /**********初始化訓練資料矩陣**********/
    int iNumTrain = 800;
    Mat trainDataHog;
    Mat trainLabel = Mat::zeros(iNumTrain,1,CV_32FC1);

    /**********提取HOG特徵,放入訓練資料矩陣中**********/
    Mat imageSrc;
    for (int i = 0; i < iNumTrain; i++)
    {
        imageSrc = imread(vecTrainPath[i].c_str(),1);
        resize(imageSrc,imageSrc,Size(64,64));
        HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
            cvSize(8,8),cvSize(8,8),9); 
        vector<float> descriptor;
        hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0));

        if (i == 0)
        {
            trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1);
        }

        int n = 0;
        for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
        {
            trainDataHog.at<float>(i,n) = *iter;
            n++;
        }
        trainLabel.at<float>(i,0) = vecTrainLabel[i];
    }

    /**********初始化SVM分類器**********/
    CvSVM svm;  
    CvSVMParams param;    
    CvTermCriteria criteria;      
    criteria = cvTermCriteria( CV_TERMCRIT_EPS, 1000, FLT_EPSILON );      
    param = CvSVMParams(CvSVM::C_SVC, CvSVM::RBF, 
        10.0, 0.09, 1.0, 10.0, 0.5, 1.0, NULL, criteria );     

    /**********訓練並儲存SVM**********/
    svm.train(trainDataHog,trainLabel,Mat(),Mat(),param);
    svm.save("E:\\性別識別資料庫—CAS-PEAL\\SVM_SEX_Model.txt");

    /**********測試SVM分類效能**********/
    Mat testImage = imread("E:\\性別識別資料庫—CAS-PEAL\\測試樣本\\女性測試樣本\\face_35.bmp");
    resize(testImage,testImage,Size(64,64));

    HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
        cvSize(8,8),cvSize(8,8),9); 
    vector<float> descriptor;
    hog->compute(testImage,descriptor,Size(1,1),Size(0,0));

    Mat testHog = Mat::zeros(1,descriptor.size(),CV_32FC1);
    int n = 0;
    for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
    {
        testHog.at<float>(0,n) = *iter;
        n++;
    }
    int predictResult = svm.predict(testHog);

    return 0;
}

  五、總結

  以上就是通過HOG特徵+SVM進行性別識別的完整程式碼,在編寫程式碼的過程中遇到了一些有趣的問題,這裡稍作總結。

  1、變數命名格式

  當代碼量很大的時候,變數的命名格式就顯得十分重要,相信大家早已不用那種a、b、m、n這種簡單的無意義的命名方法了。在C++中推薦大家使用匈牙利命名法,即“型別縮寫+變數名縮寫”的命名格式。例如vecTrainPath這個變數名,字首“vec”表明這個變數是一個vector格式的變數,而“TrainPath”則表明這個容器中存放的是訓練樣本的路徑。這種命名方式在大型工程中非常重要,還有一點需要注意的是當變數名中出現多個縮略短語時,推薦第一個短語小寫,其他短語的首字母大寫。

  2、為何選擇HOG特徵

  通過實驗發現,直接將影象向量化後輸入SVM(不經過特徵提取)的方式的正確率將不理想。雖然本質上畫素本身最能代表影象的語義資訊,但由於SVM並不具備特徵提取能力,因此效果不佳。確切的說,特徵提取是模式分類的必要過程,即便是深度學習也不例外,因為深度學習(DeepLearning)本質上也是一種特徵提取的手段,只不過提取得到的特徵更深層,更抽象,表現力更強。為此我之前曾專門寫過一篇部落格進行闡述:淺談模式識別中的特徵提取

  當然這裡大家可以嘗試提取其他特徵之後再進行分類,甚至可以考慮通過提起深度特徵來進行分類,這裡只是以HOG特徵為例而已。

  4、有關vector的一些使用(為什麼不用int型陣列)

  在這段程式碼中我們大量用到了vector結構,這是C++11的新特性。仔細觀察,其實vector結構的最明顯的一個優勢就是能夠動態分配大小,實時新增/刪除元素,這點是陣列所不能實現的。雖然可以通過new操作符來實現陣列的動態分配,但我們仍推薦大家在需要使用可動態變化的陣列的場合,使用vector。

  5、Vectot和vector

  在編寫程式碼是仔細留心編譯器給出的拼寫提示,會發現這樣一現象:

  那麼vector和Vector有什麼區別呢?一句話,Vector是OpenCv中的vector,類似的還有String和string等。Vector和String這類結構是隸屬於OpenCv的:

  OK,以上就是這次博文的所有內容,在接下來的博文中我們將開始進入MFC程式設計階段,歡迎大家討論:http://blog.csdn.net/u013088062