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为实验中训练集的大小)。

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;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容