2018-01-15 EigenFace OpenCV 实现
EigenFace OpenCV实现
欢迎在 Yang Han's Notebook EigenFace OpenCV 实现 获得更佳阅读体验。
实验目标
自己写代码实现EigenFace 人脸识别的训练与识别过程:
- 假设每张⼈脸图像只有一张人脸,且两只眼睛位置已知(即可人工标注给出)。每张图像的眼睛位置存在相应目录下的㇐个与图像文件名相同但后缀名为txt的⽂本文件⾥,文本文件中用一行、以空格分隔的4个数字表示,分别对应于两只眼睛中⼼在图像中的位置;
- 实现两个程序过程(两个执行文件),分别对应训练与识别3. 自己构建一个人脸库(至少40个,包括自己),课程主页提供一个人脸库可选用。
- 不能直接调用OpenCV里面与Eigenface相关的一些函数,特征值与特征向量求解函数可以调用; 只能用C/C++,不能⽤其他编程语言; GUI只能用OpenCV自带的HighGUI,不能用QT或其他的;平台可以用Win/Linux/MacOS,建议Win优先;
- 训练程序格式大致为: “mytrain.exe 能量百分比 model文件名 其他参数…”,用能量百分比决定取多少个特征脸,将训练结果输出保存到model文件中 。同时将前10个特征脸拼成一张图像,然后显示出来。
- 识别程序格式大致为: “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;
}