基於 OpenCV PCA實現過程
前言
PCA是一種用於數據降維的方法,常用於圖像的壓縮、人臉識別等。其原理並不複雜,但是其中的思想還是很有用的。詳細的PCA的數學原理推薦訪問https://zhuanlan.zhihu.com/p/21580949
作爲練手,用MATLAB和OpenCV實現PCA還是挺有幫助的,畢竟看別人的代碼,總不如自己將一個算法看懂後努力去實現的收穫大。
目錄
數據準備
40組人臉圖像,類標號爲1-40,每組6張,總計240張。訓練樣本爲每組的5張,共200張。測試樣本爲每組一張,共40張。
函數設計
我編寫的代碼用到了以下函數:
//獲取文本文件路徑,將每一行的文字作爲一個元素添加到vector中
vector<string> getList(const string& file);
//用於顯示Mat數據的函數(調試用)
void printEle(Mat_<float> m, int x, int y, int n);
//保存pca訓練數據
void savePCAdata(PCA pca,Mat eigenface, Size size, vector<string> type, string file);
//pca訓練函數
void pcaTrain(const string& data, const string& path, const string& namelist, Size size, const string& typelist, int num = 0);
//pca測試函數
vector<string> pcaTest(const string& data, const string& path, const string& namelist, const string truetype = "");
//讀取圖像整理成標準的矩陣
Mat arrMat(const string& path, const string& namelist, Size size);
//原圖像與pca重建後圖像顯示
void compareFace(int i, const string& path,const string& file);
函數詳解
vector<string> getList(const string& file);
輸入參數:
file:txt文本文件,裏面是提前做好的用於訓練或測試的圖像名稱。
輸出:
vector的向量:該函數將每一個圖像名稱作爲一個元素放進vector中,爲了以後讀取圖像時提供方便。
void savePCAdata(PCA pca,Mat eigenface, Size size, vector<string> type, string file);
輸入:
pca:PCA對象
eigenface:特徵臉
size:縮減後的圖像尺寸,因爲原圖像尺寸很大,不進行縮減,運算量會很大
type:與每一幅圖像對應,爲類標號
file:保存的想XML文件名
該函數將輸入的參數保存在XML文件中,因此,只需訓練一次就可以。
void pcaTrain(const string& data, const string& path, const string& namelist, Size size, const string& typelist, int num = 0);
該函數會調用arrMat函數、getList函數、savePCAdata函數。
執行過程:
- 調用arrMat函數,將圖像整理成PCA所需要的矩陣
- 調用getList函數,獲取每幅圖想的類別
- 調用OpenCV的PCA函數,構造PCA對象
- 調用PCA::project函數,將原始圖像投影到特徵空間
- 調動savePCAdata函數保存訓練數據
輸入:
data:XML文件名
path:圖像存儲路徑
namelist:txt文件,即圖像名稱列表
size:縮減後圖像尺寸
typelist:存儲類別的txt文件
num:要保留的主成分個數,默認num=0表示全部保留
vector<string> pcaTest(const string& data, const string& path, const string& namelist, const string truetype = "");
函數執行過程:
- 構造FileStorage對象,加載XML中的數據
- 調用arrMat函數,將測試圖像整理成標準矩陣
- 重構PCA對象
- 對測試數據投影到特徵空間
- 構造測試樣本特徵臉與訓練樣本特徵臉的距離矩陣
- 按照最鄰近原則歸類
輸入參數:
data:XML文件名
path:測試圖像路徑
namelist:測試圖像名稱txt清單
truetype:可選的測試樣本的真實類別,若有該參數,會根據測試結果與真實結果計算識別的準確度
主要函數就這些了。
源代碼
// cv3_pca.cpp : 定義控制檯應用程序的入口點。
//
#include "stdafx.h"
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/videoio.hpp"
#include "iostream"
#include "fstream"
#include "string"
#include "vector"
using namespace cv;
using namespace std;
vector<string> getList(const string& file); //獲取文本文件路徑,將每一行的文字作爲一個元素添加到vector中
void showEle(Mat_<float> m); //用於顯示Mat數據的函數(調試用)
void savePCAdata(PCA pca,Mat eigenface, Size size, vector<string> type, string file); //保存pca訓練數據
void pcaTrain(const string& data, const string& path, const string& namelist, Size size, const string& typelist, int num = 0); //pca訓練函數
vector<string> pcaTest(const string& data, const string& path, const string& namelist, const string truetype = ""); //pca測試函數
Mat arrMat(const string& path, const string& namelist, Size size); //讀取圖像整理成標準的矩陣
void compareFace(int i, const string& path,const string& file); //原圖像與pca重建後圖像顯示
int main()
{
string trainNameList = "D:\\我的文檔\\Visual Studio 2013\\Projects\\cv3_pca\\cv3_pca\\trainNameList.txt"; //圖像名稱列表路徑
string typelist = "D:\\我的文檔\\Visual Studio 2013\\Projects\\cv3_pca\\cv3_pca\\trainClassType.txt";
string path1 = "D:\\我的文檔\\Visual Studio 2013\\Projects\\cv3_pca\\cv3_pca\\train\\";
Size size;
size.width = size.height = 50; //縮減後的圖像尺寸,因圖像尺寸很大,不進行縮減,運算量太大
string data = "pca.xml";
pcaTrain(data, path1, trainNameList, size, typelist,0); //只需訓練一遍,之後調用時,不用在執行,因爲訓練的數據已經保存
string testNameList = "D:\\我的文檔\\Visual Studio 2013\\Projects\\cv3_pca\\cv3_pca\\testNameList.txt";
string testTrueType = "D:\\我的文檔\\Visual Studio 2013\\Projects\\cv3_pca\\cv3_pca\\testTrueType.txt";
string path2 = "D:\\我的文檔\\Visual Studio 2013\\Projects\\cv3_pca\\cv3_pca\\test\\";
vector<string> testType = pcaTest(data, path2, testNameList, testTrueType);
compareFace(1, path1, trainNameList);
return 0;
}
vector<string> getList(const string& file)
{
ifstream ifs(file);
string temp;
vector<string> list;
for (;!ifs.eof();)
{
getline(ifs, temp);
list.push_back(temp);
}
ifs.close();
return list;
}
void showEle(Mat_<float> m)
{
float** tempt = new float*[m.rows];
for (int i = 0; i < m.rows; i++)
{
float* num = m.ptr<float>(i);
tempt[i] = new float[m.cols];
for (int j = 0; j < m.cols; j++)
{
tempt[i][j] = num[j];
}
}
for (int i = 0; i < m.rows; i++)
{
delete[] tempt[i];
}
delete[] tempt;
}
void savePCAdata(PCA pca, Mat eigenface, Size imgsize, vector<string> traintype, string file)
{
FileStorage fs(file, FileStorage::WRITE); //創建XML文件
if (!fs.isOpened())
{
cerr << "failed to open " << "pca.xml" << endl;
}
fs <<"eigenvalues" << pca.eigenvalues;
fs << "eigenvectors" << pca.eigenvectors;
fs << "mean" << pca.mean;
fs << "eigenface" << eigenface;
fs << "imgsize" << imgsize;
fs << "traintype" << "[";
for (int i = 0; i < traintype.size(); i++)
{
fs << traintype[i];
}
fs << "]";
fs.release();
}
void pcaTrain(const string& data, const string& path, const string& namelist, Size size, const string& typelist, int num)
{
//step1:加載訓練圖像進行預處理(線性變換,灰度化,類型轉換)
Mat trainMat = arrMat(path, namelist, size);
vector<string> type = getList(typelist);
//step2:調用pca的構造函數,
PCA pca(trainMat, Mat(), CV_PCA_DATA_AS_ROW,num);
string outfile = data;
//求特徵臉
Mat traineigenface = pca.project(trainMat);
savePCAdata(pca,traineigenface, size, type, outfile);
}
Mat arrMat(const string& path, const string& namelist, Size size)
{
vector<string> list;
list = getList(namelist);
//step1:加載訓練圖像進行預處理(線性變換,灰度化,類型轉換)
int Mat_rows = list.size(); //圖片總數,亦trainMat的行
int Mat_cols = size.height * size.width; //trainMat的列
Mat xMat(Mat_rows, Mat_cols, CV_32FC1); //爲trainMat開闢空間
for (int i = 0; i < list.size(); i++)
{
Mat temp = imread(path + list[i], 0); //加載圖像單通道
Mat temp_s;
cv::resize(temp, temp_s, size, 0, 0, CV_INTER_AREA);
Mat temp2;
cv::normalize(temp_s, temp2, 0, 255, cv::NORM_MINMAX, CV_8UC1); //歸一化處理
Mat temp3;
temp2.convertTo(temp3, CV_32FC1, 1.0 / 255.0); //轉化爲浮點數
Mat temp4 = temp3.reshape(0, 1); //reshape
xMat.row(i) = temp4 + 0; //注意!!!!
}
return xMat;
}
vector<string> pcaTest(const string& data, const string& path, const string& namelist, const string truetype)
{
FileStorage f(data, FileStorage::READ);
Mat eigenvalues, eigenvectors, mean, traineigenface;
Size imgsize;
vector<string> traintype;
f["eigenvalues"] >> eigenvalues;
f["eigenvectors"] >> eigenvectors;
f["mean"] >> mean;
f["eigenface"] >> traineigenface;
f["imgsize"] >> imgsize;
FileNode n = f["traintype"];
if (n.type() != FileNode::SEQ)
{
cerr << "發生錯誤,字符串不是一個序列" << endl;
exit(1);
}
FileNodeIterator it = n.begin(), it_end = n.end();
for (; it != it_end; ++it)
{
traintype.push_back((string)*it);
}
f.release();
Mat testMat = arrMat(path, namelist,imgsize);
PCA pca;
pca.eigenvalues = eigenvalues;
pca.eigenvectors = eigenvectors;
pca.mean = mean;
Mat testeigenface = pca.project(testMat);
vector<string> testTrueType;
if (truetype != "")
{
testTrueType = getList(truetype);
}
vector<string> testType;
for (int i = 0; i < testeigenface.rows; i++)
{
double min_dis = cv::norm(testeigenface.row(i), traineigenface.row(0), NORM_L2);
int min_index = 0;
for (int j = 0; j < traineigenface.rows; j++)
{
double dis = cv::norm(testeigenface.row(i), traineigenface.row(j), NORM_L2);
if (dis < min_dis)
{
min_dis = dis;
min_index = j;
}
}
testType.push_back(traintype[min_index]);
}
int count = 0;
for (int i = 0; i < testType.size(); i++)
{
if (testType[i] == testTrueType[i])
{
count++;
}
}
float rate = (float)count / testType.size();
int k = 1;
for (vector<string>::iterator it = testType.begin(); it < testType.end(); it++)
{
cout << "第" << k << "張人臉屬於第" << *it << "個人" << endl;
k++;
}
cout << "The accurate rate is " << rate << endl;
return testType;
}
void compareFace(int i,const string& path, const string& file)
{
vector<string> trainNameList = getList(file);
FileStorage fs("pca.xml",FileStorage::READ);
Size size;
PCA pca;
Mat trianEigenface;
fs["imgsize"] >> size;
fs["eigenface"] >> trianEigenface;
fs["eigenvalues"] >> pca.eigenvalues;
fs["eigenvectors"] >> pca.eigenvectors;
fs["mean"] >> pca.mean;
fs.release();
Mat trainOriginFace = imread(path+trainNameList[i]);
namedWindow("trainOriginFace");
imshow("trainOriginFace",trainOriginFace);
Mat trainReconstFaceV, trainReconstFace;
pca.backProject(trianEigenface.row(i), trainReconstFaceV);
cv::resize(trainReconstFaceV.reshape(0,size.height), trainReconstFace, Size(trainOriginFace.cols, trainOriginFace.rows), 0.0, 0.0, cv::INTER_LINEAR);
namedWindow("trainReconstFace");
imshow("trainReconstFace", trainReconstFace);
char key = cv::waitKey(0);
if (key == 27)
{
return;
}
}
總結
通過編寫代碼,熟悉了OpenCV的一些函數的用法:
關於PCA類
- 構造函數
PCA (InputArray data, InputArray mean, int flags, int maxComponents=0)
輸入參數依次是:
輸入的PCA樣本矩陣,
樣本均值(可以不計算,寫成Mat())
flags標誌是一行是一個樣本(DATA_AS_ROW),還是一列 (DATA_AS_COL )
主成分個數,0爲全部保留,即等於樣本數
- PCA成員:
public 屬性 eigenvalues 特徵值
N×1 列,eigenvectors 特徵向量(按行排列),mean 樣本均值常用函數:
Mat project(InputArray vec) const //將原始樣本投影到特徵空間 Mat backProject(InputArray vec) const //將特徵臉重構原圖像,肯定跟原圖像有差別
關於
cv::normalize 函數void cv::normalize ( InputArray src, InputOutputArray dst, double alpha = 1, double beta = 0, int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray() ) //對矩陣中的元素進行歸一化,如果是矩陣的化是對整個矩陣歸一化
normType=NORM_INF,NORM_L1,orNORM_L2 ,使src 的無窮範數、1範數、或2範數等於alpha normType=NORM_MINMAX ,進行區間的歸一化到[alpha,beta]
關於
imread 函數
Mat cv::imread(const string& filename, int MODE = IMREAD_COLOR);
MODE=IMREAD_UNCHANGE //不經處理加載原圖像
IMREAD_GRAYSCALE //強制轉化爲單通道灰度圖
IMREAD_COLOR //強制轉化成3通道BGR圖
...
關於
cv::norm 函數double cv::norm ( InputArray src1, int normType = NORM_L2, InputArray mask = noArray() ) //求src1的範數,2範數相當於歐式距離 double cv::norm ( InputArray src1, InputArray src2, int normType = NORM_L2, InputArray mask = noArray() ) //求src1與src2差的範數
關於
Mat::convertTo 函數void cv::Mat::convertTo (OutputArray m, int rtype, double alpha = 1, double beta = 0 ) const //對矩陣元素進行線性變換按照如下公式:
m(x,y)=saturatecast<rType>(α(∗this)(x,y)+β) 關於XML數據的存儲
//存儲數據 Mat img = imread("lena.jpg", IMREAD_COLOR); FileStorage fs("xxx.xml", FileStorage::WRITE); if (!fs.isOpened()) { cerr << "failed to open " << "xxx.xml" << endl; } fs<<"img"<<img; //存Mat //存vector fs<<"vec"<<"["; for (int i = 0; i < vec.size(); i++) { fs << vec[i]; } fs << "]"; //釋放 fs.release(); //讀取xml數據 FileStorage fs("xxx.xml", FileStorage::READ); Mat img; fs["img"]>>img; //讀取Mat //讀取vector,要藉助FileNode和FileNodeIterator FileNode n = fs["traintype"]; if (n.type() != FileNode::SEQ) { cerr << "發生錯誤,字符串不是一個序列" << endl; exit(1); } FileNodeIterator it = n.begin(), it_end = n.end(); for (; it != it_end; ++it) { vec.push_back((string)*it); } fs.release();
關於
cv::resize 函數void cv::resize ( InputArray src, OutputArray dst, Size dsize,//輸出圖像尺寸,若爲Size(0,0),則根據以下兩個參數確定 double fx = 0, //寬度放大倍數 double fy = 0, //高度放大倍數 int interpolation = INTER_LINEAR //圖像插值算法 )
interpolation=INTER_AREA,INTER_LINEAR,INTER_CUBIC
縮小用area,放大用linear
關於
Mat::reshape 函數//矩陣元素不變,改變行數、列數 Mat cv::Mat::reshape ( int cn, //通道 int rows = 0 //返回矩陣的行數 ) const
關於
Mat::row 函數Mat cv::Mat::row ( int y ) const //注意該函數只創建一個Mat頭信息,不復制數據 Mat A; ... A.row(i) = A.row(j); // 不會改變第i行的值 A.row(i) = A.row(j) + 0; // 會改變第i行的值 A.row(j).copyTo(A.row(i)); // 會改變第i行的值
C++中
ifstream 讀取文件ifstream ifs(file); string temp; vector<string> list; for (;!ifs.eof();) { getline(ifs, temp); //調用string全局函數getline list.push_back(temp); } ifs.close();
補充
在MATLAB中也實現了pca,其具體用法:
[coeff,score,latent,tsquared,explained,mu] = pca(X)
X :輸入數據陣m×n ,m 次觀測,n 個變量
Coeff :主成分系數(特徵值/投影矩陣)n×(m−1) ,已經降序排列,已經單位化正交化
Score :x 在新空間的座標m×(m−1) ,即score=X⋅coeff
Latent :特徵值(m−1)×(1)
tsquared :不清楚
Explained=latent./sum(latent)
Mu : 樣本均值