OpenCV人臉識別實驗(一)——特徵臉(Eigenfaces)及其重構的原始碼詳解
1、介紹Introduction
從OpenCV2.4開始,加入了新的類FaceRecognizer,我們可以使用它便捷地進行人臉識別實驗。
本實驗採用的程式設計環境為:opencv3.0+VS2013。人臉識別的實驗已經轉移到face模組中,
face模組在我這裡的路徑為:D:\Program Files\opencv3.0\opencv\sources\modules\opencv_contrib-master\modules\face。
程式碼在OpenCV安裝目錄下為:D:\Program Files\opencv3.0\opencv\sources\modules\opencv_contrib-master\modules\face\samples\facerec_eigenfaces.cpp.
如果需要在高版本的opencv中使用face模組,需要自己進行編譯,其編譯和配置流程請參照題目為:
face模組目前支援的演算法有:
PCA:低維子空間是使用主元分析找到的,找具有最大方差的哪個軸。
缺點:若變化基於外部(光照),最大方差軸不一定包括鑑別資訊,不能實行分類。
LDA:線性鑑別的特定類投影方法,目標:實現類內方差最小,類間方差最大。
(3)區域性二值模式(LBP)——LocalBinary Patterns Histograms
PCA和LDA採用整體方法進行人臉辨別,LBP採用區域性特徵提取,除此之外,還有的區域性特徵提取方法為:
蓋伯小波(Gabor Waelets)和離散傅立葉變換(DCT)。
2、人臉庫及資料準備
)下載一個。本實驗採用ORL人臉資料庫,40個人,每人10張照片。本次實驗ORL人臉庫影象解壓縮在的目錄路徑為:D:\Program Files\opencv3.0\opencv\sources\data\FaceData\ORL。CSV檔案的目錄路徑為:
D:\Program Files\opencv3.0\opencv\sources\data\at.txt。我們需要在程式中讀取它,我決定使用CSV檔案讀取它。一個CSV檔案包含檔名,緊跟一個標籤。
如:D:\Program Files\opencv3.0\opencv\sources\data\FaceData\ORL\s1\1.pgm;0
我們給它一個標籤0,這個標籤類似代表這個人的名字,所以同一個人的照片的標籤都一樣。建立一個CSV檔案,
at.txt檔案的部分內容的截圖如下:
3、演算法描述
影象表示的問題是他的高維問題。二維灰度影象p*q大小,是一個m=qp維的向量空間,
所以一個100*100畫素大小的影象就是10,000維的影象空間。問題是,是不是所有的維數空間對我們來說都有用?
我們可以做一個決定,如果資料有任何差異,我們可以通過尋找主元來知道主要資訊。
主成分分析(PrincipalComponent Analysis,PCA)是KarlPearson (1901)獨立發表的,
而 Harold Hotelling (1933)把一些可能相關的變數轉換成一個更小的不相關的子集。
想法是,一個高維資料集經常被相關變量表示,因此只有一些的維上資料才是有意義的,包含最多的資訊。
PCA方法尋找資料中擁有最大方差的方向,被稱為主成分。
令 表示一個隨機特徵,其中 .
1. 計算均值向量
2. 計算協方差矩陣 S
3. 計算 的特徵值 和對應的特徵向量
4. 對特徵值進行遞減排序,特徵向量和它順序一致. K個主成分也就是k個最大的特徵值對應的特徵向量。
x的K個主成份:
其中 .
PCA基的重構:
其中 .
然後特徵臉通過下面的方式進行人臉識別:
A. 把所有的訓練資料投影到PCA子空間
B. 把待識別影象投影到PCA子空間
C. 找到訓練資料投影后的向量和待識別影象投影后的向量最近的那個。4、程式碼實現及實驗結果
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/face/facerec.hpp>
#include <iostream>
#include <fstream> //檔案操作的集合,以流的方式進行
#include <sstream> //此庫定義了stringstream類,即:流的輸入輸出操作。
//使用string物件代替字元陣列,避免緩衝區溢位的危險
using namespace cv;
using namespace std;
using namespace cv::face;
//歸一化影象矩陣函式
static Mat norm_0_255(InputArray _src)
{
Mat src = _src.getMat(); //將傳入的型別為InputArray的引數轉換為Mat的結構
Mat dst; //建立和返回一個歸一化後的影象
switch (src.channels())
{
case 1:
normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC1);
break;
case 3:
normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC3);
break;
default:
src.copyTo(dst);
break;
}
return dst;
}
//使用CSV檔案讀取影象和標籤,主要使用stringstream和getline方法
static void read_csv(const string& filename, vector<Mat>& images, vector<int>& labels, char separator = ';')
{
ifstream file(filename.c_str(), ifstream::in); //以輸入方式開啟檔案
//c_str()函式將字串轉化為字元陣列,返回指標
if (!file)
{
string error_massage = "No valid input file was given,please check the given filename!";
CV_Error(CV_StsBadArg, error_massage);
}
string line, path, classlabel;
while (getline(file, line)) //getline(字元陣列,字元個數n,終止標誌字元)
{
stringstream liness(line);
getline(liness, path, separator); //遇到分號就結束
getline(liness, classlabel); //繼續從分號後邊開始,遇到換行結束
if (!path.empty() && !classlabel.empty())
{
images.push_back(imread(path, 0));
labels.push_back(atoi(classlabel.c_str())); //atoi函式將字串轉換為整數值
}
}
}
int main(int argc, const char* argv[])
{
//[1] 檢測合法的命令,顯示用法
//如果沒有引數輸入,則退出
//if (argc < 2)
//{
// cout << "usage:" << argv[0] << "<csv.ext> <output_folder>" << endl;
// exit(1);
//}
string output_folder;
output_folder = string("D:\\Program Files\\opencv3.0\\opencv\\sources\\data\\FaceData\\result6");
//[2] 讀取CSV檔案路徑
string fn_csv = string("D:\\Program Files\\opencv3.0\\opencv\\sources\\data\\at.txt");
//兩個容器來存放影象資料和對應的標籤
vector<Mat> images;
vector<int> labels;
//讀取資料,如果檔案不合法就會出錯。輸入的檔名已經有了
try{
read_csv(fn_csv, images, labels);
}
catch (Exception& e)
{
cerr << "Error opening file" << fn_csv << ".Reason:" << e.msg << endl;
exit(1);
}
//沒有讀取到足夠多的圖片,也需要退出
if (images.size() <= 1)
{
string error_message = "This demo need at least 2 images,please add more images to your data set!";
CV_Error(CV_StsError, error_message);
}
//[3] 得到第一張圖片的高度,在下面對影象變形得到他們原始大小時需要
int height = images[0].rows;
//[4]下面程式碼僅從資料集中移除最後一張圖片,用於做測試,需要根據自己的需要進行修改
Mat testSample = images[images.size() - 1];
int testLabel = labels[labels.size() - 1];
images.pop_back(); //刪除最後一張圖片
labels.pop_back(); //刪除最後一個標籤
//[5] 建立一個特徵臉模型用於人臉識別
//通過CSV檔案讀取的影象和標籤訓練它
//這裡是一個完整的PCA 變換
//如果想保留10個主成分,使用如下程式碼 cv::createEigenFaceRecognizer(10);
//如果希望使用置信度閾值來初始化,使用程式碼 cv::createEigenFaceRecognizer(10, 123.0);
//如果使用所有特徵並使用一個閾值,使用程式碼 cv::createEigenFaceRecognizer(0, 123.0);
Ptr<BasicFaceRecognizer> model = createEigenFaceRecognizer();
model->train(images, labels);
//[6] 對測試影象進行預測,predictedLabel是預測標籤結果
int predictedLabel = model->predict(testSample);
// 還有一種呼叫方式,可以獲取結果同時得到閾值:
// int predictedLabel = -1;
// double confidence = 0.0;
// model->predict(testSample, predictedLabel, confidence);
string result_message = format("Predicted class = %d / Actual class = %d.", predictedLabel, testLabel);
cout << result_message << endl;
//[7] 如何獲取特徵臉模型的特徵值例子,使用getEigenValues方法
Mat eigenvalues = model->getEigenValues();
//[8] 獲取特徵向量
Mat W = model->getEigenVectors();
//[9] 得到訓練影象的均值向量
Mat mean = model->getMean();
//[10] 顯示或儲存
imshow("mean", norm_0_255(mean.reshape(1, images[0].rows)));
imwrite(format("%s/mean.png", output_folder.c_str()), norm_0_255(mean.reshape(1, images[0].rows)));
//[11] 顯示或儲存特徵臉
for (int i = 0; i < min(10, W.cols); i++) //修改數值10可以修改特徵臉的數目
{
string msg = format("Eigenvalue #%d = %.5f", i,eigenvalues.at<double>(i));
cout << msg << endl;
//得到第i個特徵向量
Mat ev = W.col(i).clone();
//把它變成原始大小,把資料顯示歸一化到0-255
Mat grayscale = norm_0_255(ev.reshape(1, height));
//使用偽彩色來顯示結果,為了更好的觀察
Mat cgrayscale;
applyColorMap(grayscale, cgrayscale, COLORMAP_JET);
//顯示或儲存
imshow(format("eigenface_%d", i), cgrayscale);
imwrite(format("%s/eigenface_%d.png", output_folder.c_str(), i),norm_0_255(cgrayscale));
}
//[12] 預測過程中,顯示或儲存重建後的影象
//修改值300可改變重構的影象的數目
for (int num_components = min(W.cols, 10); num_components < min(W.cols,300); num_components += 15)
{
//從模型中的特徵向量擷取一部分
Mat evs = Mat(W, Range::all(), Range(0, num_components));
//在重構時,images[0]為ORL人臉庫的第一張人臉圖,
//修改此數值0的大小可對其他人臉影象進行特徵臉處理與重構的實驗
Mat projection = LDA::subspaceProject(evs, mean, images[0].reshape(1, 1));
//投影樣本到LDA子空間
Mat reconstruction = LDA::subspaceReconstruct(evs, mean,projection);
//重構來自於LDA子空間的投影
//歸一化結果
reconstruction = norm_0_255(reconstruction.reshape(1, images[0].rows));
//[13] 若不是存放到資料夾中就顯示他,使用暫定等待鍵盤輸入
imshow(format("eigenface_reconstruction_%d", num_components),reconstruction);
imwrite(format("%s/eigenface_reconstruction_%d.png",output_folder.c_str(), num_components), reconstruction);
}
waitKey(0);
return 0;
}
實驗結果截圖為:(特徵值或特徵臉數目為10,重構人臉影象為20)
修改程式碼:
(1)for (int i = 0; i < min(50, W.cols); i++)//修改數值50可以修改特徵臉的數目
(2)//修改重構影象的範圍或數目,修改值為400
for (int num_components = min(W.cols, 10); num_components < min(W.cols,400); num_components += 15)
實驗執行結果截圖為:(特徵值或特徵臉數目為50,重構人臉影象為26)
再次修改程式碼:(程式碼中紅色註解部分)
//修改此數值0的大小為其他值,可對其他人臉影象進行特徵臉處理與重構的實驗
Mat projection = LDA::subspaceProject(evs, mean, images[50].reshape(1, 1));
實驗截圖為:
5、實驗結論
(1)50個特徵向量可以有效的編碼出重要的人臉特徵。
(2)對比下面兩幅圖,重構影象的範圍或數目越大,重構的效果越好,重構的圖片越清晰!