這兩天課比較多,上次的兩步法人臉識別代碼一直沒有補充完整,今天將整個實驗代碼show一下,同時將該方法的主要思想介紹下:
上一節我們已經將圖片進行降維處理,這樣做的目的就是要在保持對象間差異的同時降低處理數據量。除了PCA外,LDA也是一種比較簡單實用的降維方法,大家可以對比兩種降維方法;基於PCA降維后的數據,我們接着要做的是用訓練數據將測試數據表示出來
接着通過以下的誤差判別式來找到M近鄰(誤差值越小說明該訓練樣本跟測試樣本的相似度越大)
以上就完成了兩步法中的第一步,第二步中用M近鄰樣本將測試樣本再次標出(實際上這里的本質還是稀疏表示的方法,但是改進之處是單純的稀疏法中稀疏項不確定,兩步法中通過第一步的誤差篩選確定了貢獻度較大的訓練樣本)
在M近鄰中包含多個類的訓練樣本,我們要將每個類的訓練樣本累加起來,分別同測試樣本做誤差對比,將測試樣本判定給誤差最下的類
OK,主要思想介紹了,下面就看代碼實現
/************************************************************************/ /* ZhaoChaofeng */ 2013.4.16 /************************************************************************/ #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <fstream> #include <sstream> #include <iostream> #include <string> using namespace cv; using namespace std; const double u=0.01f; const double v=0.01f;//the global parameter const int MNeighbor=40;//the M nearest neighbors // Number of components to keep for the PCA const int num_components = 100; //the M neighbor mats vector<Mat> MneighborMat; //the class index of M neighbor mats vector<int> MneighborIndex; //the number of object which used to training const int Training_ObjectNum=40; //the number of image that each object used const int Training_ImageNum=7; //the number of object used to testing const int Test_ObjectNum=40; //the image number const int Test_ImageNum=3; // Normalizes a given image into a value range between 0 and 255. Mat norm_0_255(const Mat& src) { // Create and return normalized image: 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; } // Converts the images given in src into a row matrix. Mat asRowMatrix(const vector<Mat>& src, int rtype, double alpha = 1, double beta = 0) { // Number of samples: size_t n = src.size(); // Return empty matrix if no matrices given: if(n == 0) return Mat(); // dimensionality of (reshaped) samples size_t d = src[0].total(); // Create resulting data matrix: Mat data(n, d, rtype); // Now copy data: 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); } // Make sure data can be reshaped, throw a meaningful exception if not! 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); } // Get a hold of the current row: Mat xi = data.row(i); // Make reshape happy by cloning for non-continuous matrices: 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; } //convert int to string string Int_String(int index) { stringstream ss; ss<<index; return ss.str(); } ////show the element of mat(used to test code) //void showMat(Mat RainMat) //{ // for (int i=0;i<RainMat.rows;i++) // { // for (int j=0;j<RainMat.cols;j++) // { // cout<<RainMat.at<float>(i,j)<<" "; // } // cout<<endl; // } //} // ////show the element of vector //void showVector(vector<int> index) //{ // for (int i=0;i<index.size();i++) // { // cout<<index[i]<<endl; // } //} // //void showMatVector(vector<Mat> neighbor) //{ // for (int e=0;e<neighbor.size();e++) // { // showMat(neighbor[e]); // } //} //Training function void Trainging() { // Holds some training images: vector<Mat> db; // This is the path to where I stored the images, yours is different! for (int i=1;i<=Training_ObjectNum;i++) { for (int j=1;j<=Training_ImageNum;j++) { string filename="s"+Int_String(i)+"/"+Int_String(j)+".pgm"; db.push_back(imread(filename,IMREAD_GRAYSCALE)); } } // Build a matrix with the observations in row: Mat data = asRowMatrix(db, CV_32FC1); // Perform a PCA: PCA pca(data, Mat(), CV_PCA_DATA_AS_ROW, num_components); // And copy the PCA results: Mat mean = pca.mean.clone(); Mat eigenvalues = pca.eigenvalues.clone(); Mat eigenvectors = pca.eigenvectors.clone(); // The mean face: //imshow("avg", norm_0_255(mean.reshape(1, db[0].rows))); // The first three eigenfaces: //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)); ////get and save the training image information which decreased on dimensionality Mat mat_trans_eigen; Mat temp_data=data.clone(); Mat temp_eigenvector=pca.eigenvectors.clone(); gemm(temp_data,temp_eigenvector,1,NULL,0,mat_trans_eigen,CV_GEMM_B_T); //save the eigenvectors FileStorage fs(".\\eigenvector.xml", FileStorage::WRITE); fs<<"eigenvector"<<eigenvectors; fs<<"TrainingSamples"<<mat_trans_eigen; fs.release(); } //Line combination of test sample used by training samples //parameter:y stand for the test sample column vector; //x stand for the training samples matrix Mat LineCombination(Mat y,Mat x) { //the number of training samples size_t col=x.cols; //the result mat Mat result=cvCreateMat(col,1,CV_32FC1); //the transposition of x and also work as a temp matrix Mat trans_x_mat=cvCreateMat(col,col,CV_32FC1); //construct the identity matrix Mat I=Mat::ones(col,col,CV_32FC1); //solve the Y=XA //result=x.inv(DECOMP_SVD); //result*=y; Mat temp=(x.t()*x+u*I); Mat temp_one=temp.inv(DECOMP_SVD); Mat temp_two=x.t()*y; result=temp_one*temp_two; return result; } //Error test //parameter:y stand for the test sample column vector; //x stand for the training samples matrix //coeff stand for the coefficient of training samples void ErrorTest(Mat y,Mat x,Mat coeff) { //the array store the coefficient map<double,int> Efficient; //compute the error for (int i=0;i<x.cols;i++) { Mat temp=x.col(i); double coefficient=coeff.at<float>(i,0); temp=coefficient*temp; double e=norm((y-temp),NORM_L2); Efficient[e]=i;//insert a new element } //select the minimum w col as the w nearest neighbors map<double,int>::const_iterator map_it=Efficient.begin(); int num=0; //the map could sorted by the key one while (map_it!=Efficient.end() && num<MNeighbor) { MneighborMat.push_back(x.col(map_it->second)); MneighborIndex.push_back(map_it->second); ++map_it; ++num; } //return MneighborMat; } //error test of two step //parameter:MneighborMat store the class information of M nearest neighbor samples int ErrorTest_Two(Mat y,Mat x,Mat coeff) { int result; bool flag=true; double minimumerror; // map<int,vector<Mat>> ErrorResult; //count the class of M neighbor for (int i=0;i<x.cols;i++) { //compare //Mat temp=x.col(i)==MneighborMat[i]; //showMat(temp); //if (temp.at<float>(0,0)==255) //{ int classinf=MneighborIndex[i]; double coefficient=coeff.at<float>(i,0); Mat temp=x.col(i); temp=coefficient*temp; ErrorResult[classinf/Training_ImageNum].push_back(temp); //} } // map<int,vector<Mat>>::const_iterator map_it=ErrorResult.begin(); while(map_it!=ErrorResult.end()) { vector<Mat> temp_mat=map_it->second; int num=temp_mat.size(); Mat temp_one; temp_one=Mat::zeros(temp_mat[0].rows,temp_mat[0].cols,CV_32FC1); while (num>0) { temp_one+=temp_mat[num-1]; num--; } double e=norm((y-temp_one),NORM_L2); if (flag) { minimumerror=e; result=map_it->first+1; flag=false; } if (e<minimumerror) { minimumerror=e; result=map_it->first+1; } ++map_it; } return result; } //testing function //parameter:y stand for the test sample column vector; //x stand for the training samples matrix int testing(Mat x,Mat y) { // the class that test sample belongs to int classNum; //the first step: get the M nearest neighbors Mat coffecient=LineCombination(y.t(),x.t()); //cout<<"the first step coffecient"<<endl; //showMat(coffecient); //map<Mat,int> MneighborMat=ErrorTest(y,x,coffecient); ErrorTest(y.t(),x.t(),coffecient); //cout<<"the M neighbor index"<<endl; //showVector(MneighborIndex); //cout<<"the M neighbor mats"<<endl; //showMatVector(MneighborMat); //the second step: //construct the W nearest neighbors mat int row=x.cols;//should be careful Mat temp(row,MNeighbor,CV_32FC1); for (int i=0;i<MneighborMat.size();i++) { Mat temp_x=temp.col(i); if (MneighborMat[i].isContinuous()) { MneighborMat[i].convertTo(temp_x,CV_32FC1,1,0); } else { MneighborMat[i].clone().convertTo(temp_x,CV_32FC1,1,0); } } //cout<<"the second step mat"<<endl; //showMat(temp); Mat coffecient_two=LineCombination(y.t(),temp); //cout<<"the second step coffecient"<<endl; //showMat(coffecient_two); classNum=ErrorTest_Two(y.t(),temp,coffecient_two); return classNum; } int main(int argc, const char *argv[]) { //the number which test true int TrueNum=0; //the Total sample which be tested int TotalNum=Test_ObjectNum*Test_ImageNum; //if there is the eigenvector.xml, it means we have got the training data and go to the testing stage directly; FileStorage fs(".\\eigenvector.xml", FileStorage::READ); if (fs.isOpened()) { //if the eigenvector.xml file exist,read the mat data Mat mat_eigenvector; fs["eigenvector"] >> mat_eigenvector; Mat mat_Training; fs["TrainingSamples"]>>mat_Training; for (int i=1;i<=Test_ObjectNum;i++) { int ClassTestNum=0; for (int j=Training_ImageNum+1;j<=Training_ImageNum+Test_ImageNum;j++) { string filename="s"+Int_String(i)+"/"+Int_String(j)+".pgm"; Mat TestSample=imread(filename,IMREAD_GRAYSCALE); Mat TestSample_Row; TestSample.reshape(1,1).convertTo(TestSample_Row,CV_32FC1,1,0);//convert to row mat Mat De_deminsion_test; gemm(TestSample_Row,mat_eigenvector,1,NULL,0,De_deminsion_test,CV_GEMM_B_T);// get the test sample which decrease the dimensionality //cout<<"the test sample"<<endl; //showMat(De_deminsion_test.t()); //cout<<"the training samples"<<endl; //showMat(mat_Training); int result=testing(mat_Training,De_deminsion_test); //cout<<"the result is"<<result<<endl; if (result==i) { TrueNum++; ClassTestNum++; } MneighborIndex.clear(); MneighborMat.clear();//及時清除空間 } cout<<"第"<<Int_String(i)<<"類測試正確的圖片數: "<<Int_String(ClassTestNum)<<endl; } fs.release(); } else { Trainging(); } // Show the images: waitKey(0); // Success! return 0; }
在以上的實現中,有些opencv的實現需要特別注意一下:
(1)坑爹的Mat類型,它雖然可以方便的讓我們實現圖像數據的矩陣化,並給出了一系列的操作方法,但是,在調試中,它卻不能像一般變量一樣,讓我們直觀的看到;我用一個比較笨的方法:自己寫一個方法,在調試中調用,呈現關鍵矩陣的數據
(2)另外一個就是將訓練數據做一個保存,用到了opencv中的FileStorage類;有關對中間數據的存儲通常會用到.xml或者.yml文件,以下對其做個簡單介紹
新版本的OpenCV的C++接口中,imwrite()和imread()只能保存整數數據,且需要以圖像格式。當需要保存浮點數據或者XML/YML文件時,之前的C語言接口cvSave()函數已經在C++接口中被刪除,代替它的是 FileStorage類。這個類非常的方便,封裝了很多數據結構的細節,編程的時候可以根據統一的接口對數據結構進行保存。
1. FileStorage類寫XML/YML文件
• 新建一個FileStorage對象,以FileStorage::WRITE的方式打開一個文件。
• 使用 << 操作對該文件進行操作。
• 釋放該對象,對文件進行關閉。
例子如下:
FileStorage fs("test.yml", FileStorage::WRITE); fs << "frameCount" << 5; time_t rawtime; time(&rawtime); fs << "calibrationDate" << asctime(localtime(&rawtime)); Mat cameraMatrix = (Mat_<double>(3,3) << 1000, 0, 320, 0, 1000, 240, 0, 0, 1); //又一種Mat初始化方式 Mat distCoeffs = (Mat_<double>(5,1) << 0.1, 0.01, -0.001, 0, 0); fs << "cameraMatrix" << cameraMatrix << "distCoeffs" << distCoeffs; //features為一個大小為3的向量,其中每個元素由隨機數x,y和大小為8的uchar數組組成 fs << "features" << "["; for( int i = 0; i < 3; i++ ) { int x = rand() % 640; int y = rand() % 480; uchar lbp = rand() % 256; fs << "{:" << "x" << x << "y" << y << "lbp" << "[:"; for( int j = 0; j < 8; j++ ) fs << ((lbp >> j) & 1); fs << "]" << "}"; } fs << "]"; fs.release();
2. FileStorage類讀XML/YML文件
FileStorage對存儲內容在內存中是以層次的節點組成的,每個節點類型為FileNode,FileNode可以使單個的數值、數組或者一系列FileNode的集合。FileNode又可以看做是一個容器,使用iterator接口可以對該節點內更小單位的內容進行訪問,例如訪問到上面存儲的文件中"features"的內容。步驟與寫文件類似:
• 新建FileStorage對象,以FileStorage::READ 方式打開一個已經存在的文件
• 使用FileStorage::operator []()函數對文件進行讀取,或者使用FileNode和FileNodeIterator
• 使用FileStorage::release()對文件進行關閉
例子如下:
FileStorage fs("test.yml", FileStorage::READ); //方式一: []操作符 int frameCount = (int)fs["frameCount"]; //方式二: FileNode::operator >>() string date; fs["calibrationDate"] >> date; Mat cameraMatrix2, distCoeffs2; fs["cameraMatrix"] >> cameraMatrix2; fs["distCoeffs"] >> distCoeffs2; //注意FileNodeIterator的使用,似乎只能用一維數組去讀取里面所有的數據 FileNode features = fs["features"]; FileNodeIterator it = features.begin(), it_end = features.end(); int idx = 0; std::vector<uchar> lbpval; for( ; it != it_end; ++it, idx++ ) { cout << "feature #" << idx << ": "; cout << "x=" << (int)(*it)["x"] << ", y=" << (int)(*it)["y"] << ", lbp: ("; (*it)["lbp"] >> lbpval; //直接讀出一維向量 for( int i = 0; i < (int)lbpval.size(); i++ ) cout << " " << (int)lbpval[i]; cout << ")" << endl; } fs.release();
另外,注意在新建FileStorage對象之后,並以READ或WRITE模式打開文件之后,可以用FileStorage::isOpened()查看文件狀態,判斷是否成功打開了文件。
有關FileStorage類的相關內容引用自:http://www.cnblogs.com/summerRQ/articles/2524560.html
我使用的是opencv2.4.0版本實現的方法,opencv有個欠缺的地方就是版本間的兼容性,雖然做了些工作,但是使用起來還是有些不流暢。不過,值得稱贊的是其將OOP的思想應用到庫的開發中,很多核心對象和相關操作被封裝起來,方便使用。