最近一直在忙課程,老師讓我看看他的論文也沒放在心上。總算閑下來,看了他在Face Service方面的相關論文,拿出一篇放在部落格上跟大家共同分析下。在看以下內容前,首先要閱讀下徐勇老師的這篇論文
A Two-Phase Test Sample Sparse Representation Method for Use With Face Recognition;當前Face Service方面最熱的方法就是稀疏表示方法(sparse represent),其主要思想是利用線性或者非線性表示方法將檢查樣本用訓練樣本表示出來,訓練樣本前的係數為代表比重,選取比重較大的訓練樣本所屬的類來標記測試樣本。這種方法在某些模式識別中效果較好,但是其原理並不明確,沒有很好的理論基礎,所以就方法的科學性而言相對欠缺。徐老師提出兩步法,第一步利用所有訓練樣本來標示出測試樣本,並提取M近鄰訓練樣本;第二步利用第一步中提取的M近鄰樣本表出測試樣本,選取代表比重大的訓練樣本所屬於的類來標記測試樣本。
關於該方法的理論,希望大家去下載論文閱讀,這裡就不在多說,重點在於演算法的實現上:演算法中將實現分為兩步,第一步是用所有訓練樣本表示出測試樣本,可以用SVD來計算出係數陣,但在這之前要通過PCA或者LDA的方法給特徵向量降維;
opencv中PCA有現成的方法,具體代碼如下(我的風格是先給出代碼,在代碼中介紹實現邏輯)
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include <fstream>#include <sstream>using namespace cv;using namespace std;
//將給出的映像迴歸為範圍在0~255之間的正常映像
Mat norm_0_255(const Mat& src) { // 構建返回映像矩陣 Mat dst; switch(src.channels()) { case 1://根據映像通道情況選擇不同的迴歸函數 cv::normalize(src, dst, 0, 255, NORM_MINMAX, CV_8UC1); break; case 3: cv::normalize(src, dst, 0, 255, NORM_MINMAX, CV_8UC3); break; default: src.copyTo(dst); break; } return dst;}// 將一副映像的資料轉換為Row Matrix中的一行;這樣做是為了跟opencv給出的PCA類的介面對應
//參數中最重要的就是第一個參數,表示的是訓練映像樣本集合
Mat asRowMatrix(const vector<Mat>& src, int rtype, double alpha = 1, double beta = 0) { // 樣本個數 size_t n = src.size(); // 如果樣本為空白,返回空矩陣 if(n == 0) return Mat(); // 樣本的維度 size_t d = src[0].total(); // 構建返回矩陣 Mat data(n, d, rtype); // 將映像資料複製到結果矩陣中 for(int i = 0; i < n; i++) { //如果資料為空白,拋出異常 if(src[i].empty()) { string error_message = format("Image number %d was empty, please check your input data.", i); CV_Error(CV_StsBadArg, error_message); } // 映像資料的維度要是d,保證可以複製到返回矩陣中 if(src[i].total() != d) { string error_message = format("Wrong number of elements in matrix #%d! Expected %d was %d.", i, d, src[i].total()); CV_Error(CV_StsBadArg, error_message); } // 獲得返回矩陣中的當前行矩陣: Mat xi = data.row(i); // 將一副影像地圖到返回矩陣的一行中: if(src[i].isContinuous()) { src[i].reshape(1, 1).convertTo(xi, rtype, alpha, beta); } else { src[i].clone().reshape(1, 1).convertTo(xi, rtype, alpha, beta); } } return data;}int main(int argc, const char *argv[]) { // 訓練映像集合 vector<Mat> db; // 本例中使用的是ORL人臉庫,可以自行在網上下載
//將資料讀入到集合中
db.push_back(imread("s1/1.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s1/2.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s1/3.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s2/1.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s2/2.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s2/3.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s3/1.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s3/2.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s3/3.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s4/1.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s4/2.pgm", IMREAD_GRAYSCALE)); db.push_back(imread("s4/3.pgm", IMREAD_GRAYSCALE)); // 將訓練資料讀入到資料集合中,實現PCA類的介面 Mat data = asRowMatrix(db, CV_32FC1); // PCA中設定的主成分的維度,這裡我們設定為10維度 int num_components = 10; // 構建一份PCA類 PCA pca(data, Mat(), CV_PCA_DATA_AS_ROW, num_components); // 複製PCA方法獲得的結果 Mat mean = pca.mean.clone(); Mat eigenvalues = pca.eigenvalues.clone(); Mat eigenvectors = pca.eigenvectors.clone(); // 平均臉: imshow("avg", norm_0_255(mean.reshape(1, db[0].rows))); // 前三個訓練人物的特徵臉 imshow("pc1", norm_0_255(pca.eigenvectors.row(0)).reshape(1, db[0].rows)); imshow("pc2", norm_0_255(pca.eigenvectors.row(1)).reshape(1, db[0].rows)); imshow("pc3", norm_0_255(pca.eigenvectors.row(2)).reshape(1, db[0].rows)); // Show the images: waitKey(0); // Success! return 0;}
以上代碼中主要用到的opencv函數介紹:
Mat Mat::reshape(int cn, int rows=0) const
opencv手冊上的解釋為:Changes the shape and/or the number of channels of a 2D matrix without copying the data.
參數cn:新的通道數;如果cn值為0表示變換前後通道數不變
參數rows:新的行數;如果rows值為0表示變換後矩陣的行數不變
該函數會為當前矩陣建立一個新的矩陣頭(指標),新的矩陣擁有不同的尺寸或者不同的通道數,其優點在於運算複雜度為O(1),不用複製矩陣資料.正是因為不用複製資料,所以在轉變過程中要保證原資料矩陣在資料上的連續性(這裡的連續性是相對於原矩陣來說)為了更好的說明,舉個例子:
std::vector<Point3f> vec;//一個3D資料點的集合
...
Mat pointMat = Mat(vec). // 將這個三維向量集合轉換為矩陣,複製度為O(1);實際上形成的矩陣為一個N*1的3通道映像陣
reshape(1). // 用reshape方法將其映射為N*3的1通道映像陣,同樣運算複雜度為O(1)
boolMat::isContinuous() const
opencv手冊上的解釋:Reports whether the matrix is continuous or not.
如果矩陣元素相對於原始矩陣在元素儲存上是連續的,行與行之間沒有間隙,那麼就返回true否則就返回false;很顯然如果是1*1或者1*N矩陣,那麼其傳回值永遠是true。這個矩陣的連續性比較晦澀,我們看下該方法的可替代方法的實現
// 替代 Mat::isContinuous()的方法
bool myCheckMatContinuity(const Mat& m)
{
return m.rows == 1 || m.step == m.cols * m.elemSize();//如果矩陣只有一行就不會出現行與行之間的間斷;如果為多行,矩陣的步階應該是列數*元素尺寸
}
void Mat::convertTo(OutputArray m, int rtype, double alpha=1, double beta=0 ) const
該函數其實是對原Mat的每一個值做一個線性變換。參數1為目的矩陣,參數2為目d矩陣的類型,參數3和4變換的係數,看完下面的公式就明白了:
PCA::PCA(InputArray data, InputArray mean, int flags, int maxComponents=0)
該建構函式的第一個參數為要進行PCA變換的輸入Mat;參數2為該Mat的均值向量;參數3為輸入矩陣資料的儲存方式,如果其值為CV_PCA_DATA_AS_ROW則說明輸入Mat的每一行代表一個樣本,同理當其值為CV_PCA_DATA_AS_COL時,代表輸入矩陣的每一列為一個樣本;最後一個參數為該PCA計算時保留的最大主成分的個數。如果是預設值,則表示所有的成分都保留。
Mat PCA::project(InputArray vec) const
該函數的作用是將輸入資料vec(該資料是用來提取PCA特徵的未經處理資料)投影到PCA主成分空間中去,返回每一個樣本主成分特徵組成的矩陣。因為經過PCA處理後,未經處理資料的維數降低了,因此未經處理資料集中的每一個樣本的維數都變了,由改變後的樣本集就組成了本函數的傳回值。
Mat PCA::backProject(InputArray vec) const
一般調用backProject()函數前需調用project()函數,因為backProject()函數的參數vec為經過PCA投影降維過後的矩陣。 因此backProject()函數的作用就是用vec來重構未經處理資料集(關於該函數的本質數學實現暫時還不是很瞭解)。
另外PCA類中還有幾個成員變數,mean,eigenvectors, eigenvalues等分別對應著未經處理資料的均值,共變數矩陣的特徵值和特徵向量。
獲得的結果如下:
avrageface
EignFace
OK,我們已經可以獲得ORL資料庫中每個人物的PCA特徵臉,下一步也是我們下一節要研究的就是用訓練樣本表示出測試樣本,從而找到M近鄰樣本;