1. 程式人生 > >OpenCV人臉識別實驗(一)——特徵臉(Eigenfaces)及其重構的原始碼詳解

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)對比下面兩幅圖,重構影象的範圍或數目越大,重構的效果越好,重構的圖片越清晰