2018-01-15 EigenFace OpenCV 實現

EigenFace OpenCV實現

歡迎在 Yang Han's Notebook EigenFace OpenCV 實現 獲得更佳閱讀體驗。

實驗目標

自己寫代碼實現EigenFace 人臉識別的訓練與識別過程:

  1. 假設每張?臉圖像只有一張人臉,且兩只眼睛位置已知(即可人工標注給出)。每張圖像的眼睛位置存在相應目錄下的?個與圖像文件名相同但后綴名為txt的?本文件?,文本文件中用一行、以空格分隔的4個數字表示,分別對應于兩只眼睛中?在圖像中的位置;
  2. 實現兩個程序過程(兩個執行文件),分別對應訓練與識別3. 自己構建一個人臉庫(至少40個,包括自己),課程主頁提供一個人臉庫可選用。
  3. 不能直接調用OpenCV里面與Eigenface相關的一些函數,特征值與特征向量求解函數可以調用; 只能用C/C++,不能?其他編程語言; GUI只能用OpenCV自帶的HighGUI,不能用QT或其他的;平臺可以用Win/Linux/MacOS,建議Win優先;
  4. 訓練程序格式大致為: “mytrain.exe 能量百分比 model文件名 其他參數…”,用能量百分比決定取多少個特征臉,將訓練結果輸出保存到model文件中 。同時將前10個特征臉拼成一張圖像,然后顯示出來。
  5. 識別程序格式大致為: “mytest.exe ?臉圖像?件名model文件名 其他參數…”,將model文件裝載進來后,
    對輸入的人臉圖像進行識別,并將識別結果疊加在輸入的人臉圖像上顯示出來,同時顯示人臉庫中跟該人臉圖像最相似的圖像。

實驗環境

Windows 10 1709

OpenCV 3.3

實驗過程

總覽

實現了FaceEntry類來進行對每張人臉圖片的一些操作。

實現了FaceLib類來進行人臉庫的讀入和對人臉的一些統一操作。

然后train中實現了協方差矩陣和特征矩陣的計算,生成了特征臉。

test中實現了用特征矩陣對圖片進行降維,然后計算歐氏距離進行人臉類型的判斷。

實驗中使用了att_faces人臉庫,共40個人,每人10張人臉。我取每個人的前五張作為訓練使用的圖像,后面5張用于在測試中判斷程序人臉識別是否準確。

所以總共用于訓練的人臉圖像有200張。

FaceEntry

實現一個FaceEntry類來進行單張人臉的各種處理。

class FaceEntry {
public:
    Mat origin_pic;
    Mat gray_pic;
    Mat transformed_pic;
    int x1, y1, x2, y2;
    Mat trans_mat;
    Mat_<double> equalized_mat;
    Mat_<double> vect;
    void load(string& path, string ext) {
        load_eye_pos(path);
        origin_pic = imread(path + ext);
        gray_pic = imread(path + ext, IMREAD_GRAYSCALE);
        transform();
    }
    void load(string& path) {
        load_eye_pos(path.substr(0, path.length()-4));
        origin_pic = imread(path);
        gray_pic = imread(path, IMREAD_GRAYSCALE);
        transform();
    }

    void load_eye_pos(string& path) {
        ifstream file(path + ".txt", ifstream::in);
        file >> x1 >> y1 >> x2 >> y2;
    }

    void transform() {
        Point center((x1 + x2) / 2, (y1 + y2) / 2);
        double angle = atan((double)(y2 - y1) / (double)(x2 - x1)) * 180.0 / CV_PI;
        trans_mat = getRotationMatrix2D(center, angle, 1.0);
        trans_mat.at<double>(0, 2) += 37 - center.x;
        trans_mat.at<double>(1, 2) += 30 - center.y;
        warpAffine(gray_pic, transformed_pic, trans_mat, gray_pic.size()*4/5);
        equalizeHist(transformed_pic, transformed_pic);
        transformed_pic.copyTo(equalized_mat);
        vect = equalized_mat.reshape(1, 1).t();
    }
};

對于每張圖片,讀入一份3通道的原圖和一份1通道的灰度圖。

然后讀入對應文件名的txt文件(眼睛位置標注),存入結構體內。

然后就是進行transform()

先計算出兩只眼睛的中心,然后計算兩只眼睛傾斜的角度,然后通過仿射變換(旋轉+縮放+平移),使兩只眼睛水平并使兩只眼睛對齊到確定模板的位置。這里為了使圖片人臉仍然能幾乎占滿,我將模板的size確定維原圖size的0.8倍。

仿射變換完成之后,對灰度圖進行直方圖均衡化equalizeHist,使光照條件對人臉的影響變小。

然后將$M \times N$的圖片矩陣轉換為$MN \times 1$的矩陣(向量)保存起來。

至此,單張人臉處理完成。

FaceLib

class FaceLib {
public:
    int num_of_faces = 200;
    int num_of_persons = 40;
    int faces_per_person = 5;
    vector<FaceEntry*> faces;
    vector<Mat_<double>> _samples;
    Mat_<double> samples;

    void load(string& path) {
        for (int i = 1; i <= num_of_persons; i++)
        {
            for (int j = 1; j <= faces_per_person; ++j) {
                string entry_path = path + "/s" + to_string(i) + "/" + to_string(j);
                FaceEntry* face = new FaceEntry();
                face->load(entry_path, ".pgm");
                faces.push_back(face);
                _samples.push_back(face->vect);
            }
        }
        hconcat(_samples, samples);
    }
};

這個沒什么可說的,就是一張一張load進來,然后最后把所有$MN \times 1$的向量合并到一個$MN \times K$的矩陣中去。(K為實驗中訓練集的大?。?/p>

train

先解析一下命令行參數

char* model_name = argv[2];
double energy = atof(argv[1]);

然后讀入FaceLib

FaceLib facelib;
facelib.load(string("att_faces"));

然后計算協方差矩陣

calcCovarMatrix(samples, cov_mat, mean_mat, CV_COVAR_ROWS | CV_COVAR_NORMAL);
cov_mat = cov_mat / (samples.rows - 1);

這里計算協方差矩陣有一點小技巧,就是對于$MN \times K$的矩陣,因為最后的最終目的是求出特征矩陣,所以可以求出$K \times K$的協方差矩陣,可以大大加速之后的求特征矩陣過程。然后對于$1 \times K$的特征向量,可以通過與samples矩陣相乘得到$1 \times MN$的原始特征向量。(可用數學方式證明)。

然后就是計算特征矩陣

eigen(cov_mat, e_value_mat, e_vector_mat);

然后用samples矩陣減去平均值,恢復到$MN$維的向量。

for (int i = 0; i < samples.rows; ++i) {
  samples.row(i) -= mean_mat;
}
e_vector_mat = (samples * e_vector_mat.t()).t();

然后通過能量百分比來計算出應該取多少特征臉

double value_sum = sum(e_value_mat)[0];
cout << e_vector_mat.size() << endl;
double energy_level = value_sum * energy;
double energy_sum = 0;
int k = 0;
for (k = 0; k < e_value_mat.rows; k++)
{
    energy_sum += e_value_mat.at<double>(k, 0);
    if (energy_sum >= energy_level) break;
}
e_vector_mat = e_vector_mat.rowRange(0, k);
e_value_mat = e_value_mat.rowRange(0, k);

將運算的結果導出

FileStorage model(model_name, FileStorage::WRITE);
model << "e_vector_mat" << e_vector_mat;
model << "e_value_mat" << e_value_mat;
model.release();

然后選出特征值最大的10張特征臉合并到一張圖片然后顯示出來

vector<Mat> Top10EigenFace;
for (int i = 0; i < 10; ++i) {
    Top10EigenFace.push_back(toImg(e_vector_mat.row(i), WIDTH, HEIGHT));
}
Mat result;
hconcat(Top10EigenFace, result);
imshow("Top10EigenFace", result);

這里實現了一個toImg()函數,用于將$1 \times MN$的向量轉換維$M \times N$的圖像。

test

先解析了命令行參數,讀入model, 并讀入了FaceLib和需要檢測的圖片。

FaceLib facelib;
facelib.load(string("att_faces"));
char* model_name = argv[2];
char* file_name = argv[1];

FileStorage model(model_name, FileStorage::READ);
Mat e_vector_mat, e_value_mat;
model["e_vector_mat"] >> e_vector_mat;
model["e_value_mat"] >> e_value_mat;

face.load(string(file_name));

將所有人臉庫中的訓練臉都轉換成特征臉為基的地位向量:

distance = e_vector_mat * samples;

然后計算和訓練庫中各張人臉的歐式距離,找出最小的距離的對應圖片,認為與此張人臉最接近。

Mat face_vect = e_vector_mat * face.vect;
double min_d = norm(face_vect, distance.col(0), NORM_L2);
double temp_d = 0;
int min_i = 0;

for (int i = 1; i < distance.cols; ++i) {
temp_d = norm(face_vect, distance.col(i), NORM_L2);
    if (temp_d <= min_d) {
        min_d = temp_d;
        min_i = i;
    }
}

然后打上標記,顯示圖片

string text = "s" + to_string(min_i / 5 + 1) + " No." + to_string(min_i % 5 + 1);
putText(origin_mat, text, Point(10, 20), FONT_HERSHEY_COMPLEX, 0.5, (0, 255, 255), 2, 8);
imshow("FaceResult", origin_mat);
imshow("Similar Pic", similar_mat);

實驗結果

實驗中使用了att_faces人臉庫,共40個人,每人10張人臉。我取每個人的前五張作為訓練使用的圖像,后面5張用于在測試中判斷程序人臉識別是否準確。

所以總共用于訓練的人臉圖像有200張。

前十的特征臉的圖像:

Top10EigenFace

隨機選用mytest.exe att_faces\\s37\\8.pgm eigen.model進行測試:

結果是識別出此人,并認為第2張人臉與之最相近

運行結果

我還輸出了總共400張照片的測試結果(每個人10張人臉中的前五張在訓練庫中)(最后三張是我自己的臉,最終版本略去)

運行結果

可以看出識別結果有一定的偏差,但整體上還是比較準確的。

心得體會

這次實驗稍微有一些難度,需要理解PCA中的降維的思想,通過轉換基向量的形式使重要成分被當作向量的基向量,達到降維的目的。

然后整個實驗的過程中,熟悉了opencv的各種操作,對其的熟練程度有了提高。

然后就還有知道了imwrite支持的Mat類型不多,float的Mat需要先convert到乘以255之后的整數類型才能夠被正確地寫入。

附:源代碼

#pragma once
// facelib.h
#include <opencv2/opencv.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <vector>

// Bad Implementation Because of my Laziness.
// Plz Don't Use Namespace in *.h as me.
using namespace std;
using namespace cv;

const int WIDTH = 73;
const int HEIGHT = 89;


class FaceEntry {
public:
    Mat origin_pic;
    Mat gray_pic;
    Mat transformed_pic;
    int x1, y1, x2, y2;
    Mat trans_mat;
    Mat_<double> equalized_mat;
    Mat_<double> vect;
    void load(string& path, string ext) {
        load_eye_pos(path);
        origin_pic = imread(path + ext);
        gray_pic = imread(path + ext, IMREAD_GRAYSCALE);
        transform();
    }
    void load(string& path) {
        load_eye_pos(path.substr(0, path.length()-4));
        origin_pic = imread(path);
        gray_pic = imread(path, IMREAD_GRAYSCALE);
        transform();
    }

    void load_eye_pos(string& path) {
        ifstream file(path + ".txt", ifstream::in);
        file >> x1 >> y1 >> x2 >> y2;
    }

    void transform() {
        Point center((x1 + x2) / 2, (y1 + y2) / 2);
        double angle = atan((double)(y2 - y1) / (double)(x2 - x1)) * 180.0 / CV_PI;
        trans_mat = getRotationMatrix2D(center, angle, 1.0);
        trans_mat.at<double>(0, 2) += 37 - center.x;
        trans_mat.at<double>(1, 2) += 30 - center.y;
        warpAffine(gray_pic, transformed_pic, trans_mat, gray_pic.size()*4/5);
        equalizeHist(transformed_pic, transformed_pic);
        transformed_pic.copyTo(equalized_mat);
        vect = equalized_mat.reshape(1, 1).t();
    }

};

class FaceLib {
public:
    int num_of_faces = 200;
    int num_of_persons = 40;
    int faces_per_person = 5;
    vector<FaceEntry*> faces;
    vector<Mat_<double>> _samples;
    Mat_<double> samples;

    void load(string& path) {
        for (int i = 1; i <= num_of_persons; i++)
        {
            for (int j = 1; j <= faces_per_person; ++j) {
                string entry_path = path + "/s" + to_string(i) + "/" + to_string(j);
                FaceEntry* face = new FaceEntry();
                face->load(entry_path, ".pgm");
                faces.push_back(face);
                _samples.push_back(face->vect);
            }
        }
        hconcat(_samples, samples);
    }
};
// train.cpp
#include "facelib.h"

Mat toImg(Mat vect, int w, int h) {
    assert(vect.type() == 6);
    //assert(vect.rows == h);
    //cout << vect.cols << endl;
    assert(vect.cols == w*h);
    //cout << "==" << endl;
    //cout << vect << endl;
    Mat result(Size(w, h), CV_64FC1);
    for (int i = 0; i < h; ++i) {
        vect.colRange(i*w, (i + 1)*w).convertTo(result.row(i), CV_64FC1);
    }
    //equalizeHist(result, result);
    normalize(result, result, 1.0, 0.0, NORM_MINMAX);

    //imshow("iiii", result);
    return result;

}

int main(int argc, char** argv) {
    char* model_name = "eigen.model";
    double energy = 0.95;
    if (argc >= 3) {
            model_name = argv[2];
            energy = atof(argv[1]);
    }
    FaceLib facelib;
    facelib.load(string("att_faces"));
    Mat samples, cov_mat, mean_mat;
    facelib.samples.copyTo(samples);
    //cout << samples << endl;
    //cout << samples.size() << endl;
    cout << "Calculating Covariance Mat..." << endl;
    calcCovarMatrix(samples, cov_mat, mean_mat, CV_COVAR_ROWS | CV_COVAR_NORMAL);
    cout << mean_mat.size() << endl;
    ////cout << cov_mat << endl;
    //cout << cov_mat.size() << endl;
    cov_mat = cov_mat / (samples.rows - 1);
    Mat e_vector_mat, e_value_mat;
    cout << "Calculating Eigen Vector..." << endl;
    eigen(cov_mat, e_value_mat, e_vector_mat);
    cout << "eigen size " << e_value_mat.size() << endl;
    cout << e_value_mat << endl;
    for (int i = 0; i < samples.rows; ++i) {
        samples.row(i) -= mean_mat;
    }
    
    double value_sum = sum(e_value_mat)[0];
    cout << e_vector_mat.size() << endl;
    double energy_level = value_sum * energy;
    double energy_sum = 0;
    int k = 0;
    for (k = 0; k < e_value_mat.rows; k++)
    {
        energy_sum += e_value_mat.at<double>(k, 0);
        if (energy_sum >= energy_level) break;
    }
    cout << k << endl;
    e_vector_mat = (samples * e_vector_mat.t()).t();
    e_vector_mat = e_vector_mat.rowRange(0, k);
    e_value_mat = e_value_mat.rowRange(0, k);

    FileStorage model(model_name, FileStorage::WRITE);
    model << "e_vector_mat" << e_vector_mat;
    model << "e_value_mat" << e_value_mat;
    model.release();

    vector<Mat> Top10EigenFace;
    for (int i = 0; i < 10; ++i) {
        Top10EigenFace.push_back(toImg(e_vector_mat.row(i), WIDTH, HEIGHT));
    }
    Mat result;
    hconcat(Top10EigenFace, result);

    result.convertTo(result, CV_8U, 255);

    imshow("Top10EigenFace", result);
    imwrite("Top10EigenFace.png", result);


    waitKey(0);
    destroyAllWindows();
    return 0;
}
// test.cpp
#include "facelib.h"

int main(int argc, char** argv) {
    FaceLib facelib;
    facelib.load(string("att_faces"));

    char* model_name = "eigen.model";
    char* file_name = "att_faces/s27/8.png";
    if (argc >= 3) {
        model_name = argv[2];
        file_name = argv[1];
    }
    FileStorage model(model_name, FileStorage::READ);
    Mat e_vector_mat, e_value_mat;
    model["e_vector_mat"] >> e_vector_mat;
    model["e_value_mat"] >> e_value_mat;
    Mat distance;
    Mat samples;
    FaceEntry face;
    facelib.samples.copyTo(samples);
    distance = e_vector_mat * samples;
    for (int _i = 1; _i <= 40; ++_i) {
        for (int _j = 1; _j <= 10; _j++)
        {
            face.load(string("att_faces/s") + to_string(_i) + "/" + to_string(_j), ".pgm");
            Mat face_vect = e_vector_mat * face.vect;
            double min_d = norm(face_vect, distance.col(0), NORM_L2);
            double temp_d = 0;
            int min_i = 0;

            for (int i = 1; i < distance.cols; ++i) {
                temp_d = norm(face_vect, distance.col(i), NORM_L2);
                if (temp_d <= min_d) {
                    min_d = temp_d;
                    min_i = i;
                }
            }
            cout << (min_i/5)+1 << "/" << (min_i % 5)+1 << " ";
        }
        cout << endl;
    }

    face.load(string(file_name));
    
    Mat face_vect = e_vector_mat * face.vect;
    double min_d = norm(face_vect, distance.col(0), NORM_L2);
    double temp_d = 0;
    int min_i = 0;

    for (int i = 1; i < distance.cols; ++i) {
        temp_d = norm(face_vect, distance.col(i), NORM_L2);
        if (temp_d <= min_d) {
            min_d = temp_d;
            min_i = i;
        }
    }
    cout << (min_i / 5) + 1 << "/" << (min_i % 5) + 1 << " " << endl;
    Mat origin_mat = face.origin_pic;
    Mat similar_mat = facelib.faces.at(min_i)->origin_pic;
    string text = "s" + to_string(min_i / 5 + 1) + " No." + to_string(min_i % 5 + 1);
    cout << text << endl;
    putText(origin_mat, text, Point(10, 20), FONT_HERSHEY_COMPLEX, 0.5, (0, 255, 255), 2, 8);
    imshow("FaceResult", origin_mat);
    imshow("Similar Pic", similar_mat);
    waitKey(0);
    destroyAllWindows();
    return 0;
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容