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為實驗中訓練集的大?。?/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張。
前十的特征臉的圖像:
隨機選用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;
}