【計算機視覺】OpenCV人臉識別facerec原始碼分析2——LBPH概述

weixin_34148340發表於2015-03-04

人臉識別

從OpenCV2.4開始,加入了新的類FaceRecognizer,我們可以使用它便捷地進行人臉識別實驗。其原始碼可以在OpenCV中的opencv\modules\contrib\doc\facerec\src下找到。
目前支援的演算法有:

Eigenfaces特徵臉createEigenFaceRecognizer()
Fisherfaces createFisherFaceRecognizer()
Local Binary Patterns Histograms區域性二值直方圖 createLBPHFaceRecognizer()

自動人臉識別就是如何從一幅影像中提取有意義的特徵,把它們放入一種有用的表示方式,然後對他們進行一些分類。
特徵臉方法描述了一個全面的方法來識別人臉:面部影像是一個點,這個點是從高維影像空間找到它在低維空間的表示,這樣分類變得很簡單。低維子空間低維是使用主元分析(Principal Component Analysis,PCA)找到的,它可以找擁有最大方差的那個軸。雖然這樣的轉換是從最佳重建角度考慮的,但是他沒有把標籤問題考慮進去。想象一個情況,如果變化是基於外部來源,比如光照。軸的最大方差不一定包含任何有鑑別性的資訊,因此此時的分類是不可能的。因此,一個使用線性鑑別(Linear Discriminant Analysis,LDA)的特定類投影方法被提出來解決人臉識別問題。其中一個基本的想法就是,使類內方差最小的同時,使類外方差最大。
近年來,各種區域性特徵提取方法出現。為了避免輸入的影像的高維資料,僅僅使用的區域性特徵描述影像的方法被提出,提取的特徵(很有希望的)對於區域性遮擋、光照變化、小樣本等情況更強健。有關區域性特徵提取的方法有蓋伯小波(Gabor Waelets),離散傅立葉變換(Discrete Cosinus Transform,DCT),區域性二值模式(Local Binary Patterns,LBP)。使用什麼方法來提取時域空間的區域性特徵依舊是一個開放性的研究問題,因為空間資訊是潛在有用的資訊。

區域性二值模式直方圖Local Binary Patterns Histograms

由於Eigenfaces和Fisherfaces兩種方法當引入新的人臉資料時需要重新進行訓練,所以這裡我著重介紹LBP特徵的有關內容。

Eigenfaces和Fisherfaces使用整體方法來進行人臉識別[gm:直接使用所有的畫素]。你把你的資料當作影像空間的高維向量。我們都知道高維資料是糟糕的,所以一個低維子空間被確定,對於資訊儲存可能很好。Eigenfaces是最大化總的散度,這樣可能導致,當方差由外部條件產生時,最大方差的主成分不適合用來分類。所以為使用一些鑑別分析,我們使用了LDA方法來優化。Fisherfaces方法可以很好的運作,至少在我們假設的模型的有限情況下。
現實生活是不完美的。你無法保證在你的影像中光照條件是完美的,或者說1個人的10張照片。所以,如果每人僅僅只有一張照片呢?我們的子空間的協方差估計方法可能完全錯誤,所以識別也可能錯誤。
一些研究專注於影像區域性特徵的提取。主意是我們不把整個影像看成一個高維向量,僅僅用區域性特徵來描述一個物體。通過這種方式提取特徵,你將獲得一個低維隱式。一個好主意!但是你很快發現這種影像表示方法不僅僅遭受光照變化。你想想影像中的尺度變化、形變、旋轉—我們的區域性表示方式起碼對這些情況比較穩健。正如SIFT,LBP方法在2D紋理分析中舉足輕重。LBP的基本思想是對影像的畫素和它區域性周圍畫素進行對比後的結果進行求和。把這個畫素作為中心,對相鄰畫素進行閾值比較。如果中心畫素的亮度大於等於他的相鄰畫素,把他標記為1,否則標記為0。你會用二進位制數字來表示每個畫素,比如11001111。因此,由於周圍相鄰8個畫素,你最終可能獲取2^8個可能組合,被稱為區域性二值模式,有時被稱為LBP碼。第一個在文獻中描述的LBP運算元實際使用的是3*3的鄰域。


演算法描述

一個更加正式的LBP操作可以被定義為:



其中(xc,yc)是中心畫素,亮度是ic;而in是相鄰畫素的亮度。s是一個符號函式。
這種描述方法使得你可以很好的捕捉到影像中的細節。實際上,研究者們可以用它在紋理分類上得到最先進的水平。正如剛才描述的方法被提出後,固定的近鄰區域對於尺度變化的編碼失效。所以,使用一個變數的擴充套件方法是使用可變半徑的圓對近鄰畫素進行編碼,這樣可以捕捉到如下的近鄰:



對一個給定的點(xc,yc),他的近鄰點(xp,yp),p∈P可以由如下計算:

其中,R是圓的半徑,而P是樣本點的個數。

這個操作是對原始LBP運算元的擴充套件,所以有時被稱為擴充套件LBP(又稱為圓形LBP)。如果一個在圓上的點不在影像座標上,我們使用他的內插點。電腦科學有一堆聰明的插值方法,而OpenCV使用雙線性插值。


LBP運算元,對於灰度的單調變化很穩健。我們可以看到手工改變後的影像的LBP影像。


那麼剩下來的就是如何合併空間資訊用於人臉識別模型。對LBP影像成m個塊,每個塊提取直方圖。通過連線區域性特直方圖(而不是合併)然後就能得到空間增強的特徵向量。這些直方圖被稱為區域性二值模式直方圖。

原始碼分析

LBPH類宣告

class LBPH : public FaceRecognizer
{
private:
    int _grid_x;
    int _grid_y;
    int _radius;
    int _neighbors;
    double _threshold;

    vector<Mat> _histograms;
    Mat _labels;

    // Computes a LBPH model with images in src and
    // corresponding labels in labels, possibly preserving
    // old model data.
    void train(InputArrayOfArrays src, InputArray labels, bool preserveData);

public:
    using FaceRecognizer::save;
    using FaceRecognizer::load;

    // Initializes this LBPH Model. The current implementation is rather fixed
    // as it uses the Extended Local Binary Patterns per default.
    //
    // radius, neighbors are used in the local binary patterns creation.
    // grid_x, grid_y control the grid size of the spatial histograms.
    LBPH(int radius_=1, int neighbors_=8,
            int gridx=8, int gridy=8,
            double threshold = DBL_MAX) :
        _grid_x(gridx),
        _grid_y(gridy),
        _radius(radius_),
        _neighbors(neighbors_),
        _threshold(threshold) {}

    // Initializes and computes this LBPH Model. The current implementation is
    // rather fixed as it uses the Extended Local Binary Patterns per default.
    //
    // (radius=1), (neighbors=8) are used in the local binary patterns creation.
    // (grid_x=8), (grid_y=8) controls the grid size of the spatial histograms.
    LBPH(InputArrayOfArrays src,
            InputArray labels,
            int radius_=1, int neighbors_=8,
            int gridx=8, int gridy=8,
            double threshold = DBL_MAX) :
                _grid_x(gridx),
                _grid_y(gridy),
                _radius(radius_),
                _neighbors(neighbors_),
                _threshold(threshold) {
        train(src, labels);
    }

    ~LBPH() { }

    // Computes a LBPH model with images in src and
    // corresponding labels in labels.
    void train(InputArrayOfArrays src, InputArray labels);

    // Updates this LBPH model with images in src and
    // corresponding labels in labels.
    void update(InputArrayOfArrays src, InputArray labels);

    // Predicts the label of a query image in src.
    int predict(InputArray src) const;

    // Predicts the label and confidence for a given sample.
    void predict(InputArray _src, int &label, double &dist) const;

    // See FaceRecognizer::load.
    void load(const FileStorage& fs);

    // See FaceRecognizer::save.
    void save(FileStorage& fs) const;

    // Getter functions.
    int neighbors() const { return _neighbors; }
    int radius() const { return _radius; }
    int grid_x() const { return _grid_x; }
    int grid_y() const { return _grid_y; }

    AlgorithmInfo* info() const;
};

構建LBPH例項

//宣告
Ptr<FaceRecognizer> createLBPHFaceRecognizer(int radius=1, int neighbors=8, int grid_x=8, int grid_y=8, double threshold=DBL_MAX);

//定義
Ptr<FaceRecognizer> createLBPHFaceRecognizer(int radius, int neighbors,
                                             int grid_x, int grid_y, double threshold)
{
    return new LBPH(radius, neighbors, grid_x, grid_y, threshold);
}

引數說明:

  • radius :該引數用於構建圓LBP特徵。
  • neighbors :該引數是構建圓LBP特徵所需要的近鄰畫素的個數,常用是8個取樣點。取樣點越多,計算代價越大。
  • grid_x : 該引數是水平方向上劃分的格子塊個數,常規是8個。區塊越多,最終構建結果的特徵向量的維度越高。
  • grid_y : 該引數是垂直方向上劃分的格子塊個數,常規是8個。
  • threshold : 該閾值用於預測。如果最近鄰的距離大於該閾值,預測的方法返回-1。

LBPH的訓練過程

下面給出了LBPH訓練函式train的原始碼,再進行分析。

void LBPH::train(InputArrayOfArrays _in_src, InputArray _in_labels, bool preserveData) {
    if(_in_src.kind() != _InputArray::STD_VECTOR_MAT && _in_src.kind() != _InputArray::STD_VECTOR_VECTOR) {
        string error_message = "The images are expected as InputArray::STD_VECTOR_MAT (a std::vector<Mat>) or _InputArray::STD_VECTOR_VECTOR (a std::vector< vector<...> >).";
        CV_Error(CV_StsBadArg, error_message);
    }
    if(_in_src.total() == 0) {
        string error_message = format("Empty training data was given. You'll need more than one sample to learn a model.");
        CV_Error(CV_StsUnsupportedFormat, error_message);
    } else if(_in_labels.getMat().type() != CV_32SC1) {
        string error_message = format("Labels must be given as integer (CV_32SC1). Expected %d, but was %d.", CV_32SC1, _in_labels.type());
        CV_Error(CV_StsUnsupportedFormat, error_message);
    }
    // get the vector of matrices
    vector<Mat> src;
    _in_src.getMatVector(src);
    // get the label matrix
    Mat labels = _in_labels.getMat();
    // check if data is well- aligned
    if(labels.total() != src.size()) {
        string error_message = format("The number of samples (src) must equal the number of labels (labels). Was len(samples)=%d, len(labels)=%d.", src.size(), _labels.total());
        CV_Error(CV_StsBadArg, error_message);
    }
    // if this model should be trained without preserving old data, delete old model data
    if(!preserveData) {
        _labels.release();
        _histograms.clear();
    }
    // append labels to _labels matrix
    for(size_t labelIdx = 0; labelIdx < labels.total(); labelIdx++) {
        _labels.push_back(labels.at<int>((int)labelIdx));
    }
    // store the spatial histograms of the original data
    for(size_t sampleIdx = 0; sampleIdx < src.size(); sampleIdx++) {
        // calculate lbp image
        Mat lbp_image = elbp(src[sampleIdx], _radius, _neighbors);
        // get spatial histogram from this lbp image
        Mat p = spatial_histogram(
                lbp_image, /* lbp_image */
                static_cast<int>(std::pow(2.0, static_cast<double>(_neighbors))), /* number of possible patterns */
                _grid_x, /* grid size x */
                _grid_y, /* grid size y */
                true);
        // add to templates
        _histograms.push_back(p);
    }
}

訓練過程分為以下幾個過程:

  1. 首先進行必要的錯誤檢查,得到人臉影像向量和標籤向量
  2. 計算lbp影像
  3. 根據lbp影像得到空間直方圖
  4. 將空間直方圖矩陣納入到私有變數_histograms向量中

生成lbp空間直方圖的過程:

  • elbp函式用於生成lbp影像。
  • spatial_histogram函式用於將lbp影像分塊,對每一個區塊進行直方圖統計。

LBPH的預測過程

下面給出了LBPH預測函式predict的原始碼,再進行分析。

void LBPH::predict(InputArray _src, int &minClass, double &minDist) const {
    if(_histograms.empty()) {
        // throw error if no data (or simply return -1?)
        string error_message = "This LBPH model is not computed yet. Did you call the train method?";
        CV_Error(CV_StsBadArg, error_message);
    }
    Mat src = _src.getMat();
    // get the spatial histogram from input image
    Mat lbp_image = elbp(src, _radius, _neighbors);
    Mat query = spatial_histogram(
            lbp_image, /* lbp_image */
            static_cast<int>(std::pow(2.0, static_cast<double>(_neighbors))), /* number of possible patterns */
            _grid_x, /* grid size x */
            _grid_y, /* grid size y */
            true /* normed histograms */);
    // find 1-nearest neighbor
    minDist = DBL_MAX;
    minClass = -1;
    for(int sampleIdx = 0; sampleIdx < _histograms.size(); sampleIdx++) {
        double dist = compareHist(_histograms[sampleIdx], query, CV_COMP_CHISQR);
        if((dist < minDist) && (dist < _threshold)) {
            minDist = dist;
            minClass = _labels.at<int>(sampleIdx);
        }
    }
}

預測過程就比較簡單了,首先將待查詢點影像進行lbp編碼並生成空間直方圖,然後線性暴力的計算直方圖的距離,最終輸出距離最小的預測類別。

compareHist函式
通過cv::compareHist函式來評估兩個直方圖有多麼不同、或者多麼相似,返回測量距離。
相似度衡量的辦法目前支援4種:
– CV_COMP_CORREL Correlation相關係數,相同為1,相似度範圍為[ 1, 0 )
– CV_COMP_CHISQR Chi-Square卡方,相同為0,相似度範圍為[ 0, +inf )
– CV_COMP_INTERSECT Intersection直方圖交,數越大越相似,,相似度範圍為[ 0, +inf )
– CV_COMP_BHATTACHARYYA Bhattacharyya distance做常態分別比對的Bhattacharyya 距離,相同為0,,相似度範圍為[ 0, +inf )

save和load函式

OpenCV中有一套自己的處理檔案儲存的類,可以以key-value的形式存取相應的引數。

void LBPH::load(const FileStorage& fs) {
    fs["radius"] >> _radius;
    fs["neighbors"] >> _neighbors;
    fs["grid_x"] >> _grid_x;
    fs["grid_y"] >> _grid_y;
    //read matrices
    readFileNodeList(fs["histograms"], _histograms);
   fs["labels"] >> _labels;
}

// See cv::FaceRecognizer::save.
void LBPH::save(FileStorage& fs) const {
    fs << "radius" << _radius;
    fs << "neighbors" << _neighbors;
    fs << "grid_x" << _grid_x;
    fs << "grid_y" << _grid_y;
    // write matrices
    writeFileNodeList(fs, "histograms", _histograms);
    fs << "labels" << _labels;
}

轉載請註明作者Jason Ding及其出處
Github主頁(http://jasonding1354.github.io/)
CSDN部落格(http://blog.csdn.net/jasonding1354)
簡書主頁(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)

相關文章