基於 OpenCV PCA實現過程

基於 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×nm 次觀測,n 個變量
    Coeff :主成分系數(特徵值/投影矩陣)n×(m1) ,已經降序排列,已經單位化正交化
    Scorex 在新空間的座標m×(m1) ,即score=Xcoeff
    Latent :特徵值(m1)×(1)
    tsquared :不清楚
    Explained=latent./sum(latent)
    Mu : 樣本均值

發佈了39 篇原創文章 · 獲贊 122 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章