以前對PCA算法有過一段時間的研究,但沒整理成文章,最近項目又打算用到PCA算法,故趁熱打鐵整理下PCA算法的知識。本文觀點旨在拋磚引玉,不是權威,更不能盡信,只是本人的一點體會。
主成分分析(PCA)是多元統計分析中用來分析數據的一種方法,它是用一種較少數量的特征對樣本進行描述以達到降低特征空間維數的方法,它的本質實際上是K-L變換。PCA方法最著名的應用應該是在人臉識別中特征提取及數據維,我們知道輸入200*200大小的人臉圖像,單單提取它的灰度值作為原始特征,則這個原始特征將達到40000維,這給后面分類器的處理將帶來極大的難度。著名的人臉識別Eigenface算法就是采用PCA算法,用一個低維子空間描述人臉圖像,同時用保存了識別所需要的信息。下面先介紹下PCA算法的本質K-L變換。
1、K-L變換(卡洛南-洛伊(Karhunen-Loeve)變換):最優正交變換
- 一種常用的特征提取方法;
- 最小均方誤差意義下的最優正交變換;
- 在消除模式特征之間的相關性、突出差異性方面有最優的效果。
取得最小值。
向量y就是變換(降維)后的系數向量,在人臉識別Eigenface算法中就是用系數向量y代替原始特征向量x進行識別。
Mat Mat::reshape(int cn, int rows=0) const
該函數是改變Mat的尺寸,即保持尺寸大小=行數*列數*通道數 不變。其中第一個參數為變換后Mat的通道數,如果為0,代表變換前后通道數不變。第二個參數為變換后Mat的行數,如果為0也是代表變換前后通道數不變。但是該函數本身不復制數據。
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投影降維過后的矩陣dst。 因此backProject()函數的作用就是用vec來重構原始數據集(關於該函數的本質就是上面總結2的公式)。由一個圖說明如下:

另外PCA類中還有幾個成員變量,mean,eigenvectors, eigenvalues等分別對應着原始數據的均值,協方差矩陣的特征值和特征向量。
實驗結果:
實驗是用4個人人臉圖像,其中每個人分別有5張,共計20張人臉圖片。用這些圖片組成原始數據集來提取他們的PCA主特征臉。該20張圖片如下所示:

軟件運行結果:
實驗中保留4個特征向量作為人臉圖像的正交基底,運行結果如下:

其中第一行的3張人臉分別為20張原圖中的3張,這里取的是3個不同人的。
第二行中顯示的3張人臉重構的人臉圖像,可以看出由於只取了4個特征向量作為正交基底,因此重構后的人臉圖像一些細節會丟失。如果增加保留的特征向量個數,則能較好的重構出人臉圖像。
第3行的人臉圖為取的原始數據協方差矩陣特征向量的最前面3個,因此這3個人臉為最具代表人臉特征的3個PCA人臉特征。
實驗主要部分代碼:
pcaface.h
1 #ifndef PCAFACE_H
2 #define PCAFACE_H
3 #include <opencv2/core/core.hpp>
4 #include <opencv2/highgui/highgui.hpp>
5 #include <opencv2/imgproc/imgproc.hpp>
6
7 using namespace cv;
8
9 #include <QDialog>
10
11 namespace Ui {
12 class PCAFace;
13 }
14
15 class PCAFace : public QDialog
16 {
17 Q_OBJECT
18
19 public:
20 explicit PCAFace(QWidget *parent = 0);
21 ~PCAFace();
22
23 Mat normalize(const Mat& src);
24
25
26 protected:
27 void changeEvent(QEvent *e);
28
29 private slots:
30 void on_startButton_clicked();
31
32 void on_closeButton_clicked();
33
34 private:
35 Ui::PCAFace *ui;
36 Mat src_face1, src_face2, src_face3;
37 Mat project_face1, project_face2, project_face3;
38 Mat dst;
39 Mat pca_face1, pca_face2, pca_face3;
40 vector<Mat> src;
41 int total;
42 };
43
44 #endif // PCAFACE_H
pcaface.cpp
1 #include "pcaface.h"
2 #include "ui_pcaface.h"
3 #include <QString>
4 #include <iostream>
5 #include <stdio.h>
6
7 using namespace std;
8
9 PCAFace::PCAFace(QWidget *parent) :
10 QDialog(parent),
11 ui(new Ui::PCAFace)
12 {
13 ui->setupUi(this);
14 src_face1 = imread("./images/1.pgm", 0);
15
16 //下面的代碼為設置圖片顯示區域自適應圖片的大小
17 ui->face1Browser->setFixedHeight(src_face1.rows+1);
18 ui->face1Browser->setFixedWidth(src_face1.cols+1);
19 ui->face2Browser->setFixedHeight(src_face1.rows+1);
20 ui->face2Browser->setFixedWidth(src_face1.cols+1);
21 ui->face3Browser->setFixedHeight(src_face1.rows+1);
22 ui->face3Browser->setFixedWidth(src_face1.cols+1);
23
24 ui->face4Browser->setFixedHeight(src_face1.rows+1);
25 ui->face4Browser->setFixedWidth(src_face1.cols+1);
26 ui->face5Browser->setFixedHeight(src_face1.rows+1);
27 ui->face5Browser->setFixedWidth(src_face1.cols+1);
28 ui->face6Browser->setFixedHeight(src_face1.rows+1);
29 ui->face6Browser->setFixedWidth(src_face1.cols+1);
30
31 ui->face7Browser->setFixedHeight(src_face1.rows+1);
32 ui->face7Browser->setFixedWidth(src_face1.cols+1);
33 ui->face8Browser->setFixedHeight(src_face1.rows+1);
34 ui->face8Browser->setFixedWidth(src_face1.cols+1);
35 ui->face9Browser->setFixedHeight(src_face1.rows+1);
36 ui->face9Browser->setFixedWidth(src_face1.cols+1);
37
38 for(int i = 1; i <= 15; i++)
39 {
40 stringstream ss;
41 string num;
42 ss<<i; //將整數i讀入字符串流
43 ss>>num; //將字符串流中的數據傳入num,這2句代碼即把數字轉換成字符
44 string image_name = ("./images/" + num + ".pgm");//需要讀取的圖片全名
45 src.push_back(imread(image_name, 0));
46 }
47 total= src[0].rows*src[0].cols;
48 }
49
50 PCAFace::~PCAFace()
51 {
52 delete ui;
53 }
54
55 void PCAFace::changeEvent(QEvent *e)
56 {
57 QDialog::changeEvent(e);
58 switch (e->type()) {
59 case QEvent::LanguageChange:
60 ui->retranslateUi(this);
61 break;
62 default:
63 break;
64 }
65 }
66
67 //將Mat內的內容歸一化到0~255,歸一化后的類型為但通道整型
68 Mat PCAFace::normalize(const Mat& src) {
69 Mat srcnorm;
70 cv::normalize(src, srcnorm, 0, 255, NORM_MINMAX, CV_8UC1);
71 return srcnorm;
72 }
73
74
75 void PCAFace::on_startButton_clicked()
76 {
77 //先顯示3張原圖
78 ui->face1Browser->append("<img src=./images/1.pgm>");
79 ui->face2Browser->append("<img src=./images/7.pgm>");
80 ui->face3Browser->append("<img src=./images/14.pgm>");
81
82 //mat數組用來存放讀取進來的所有圖片的數據,其中mat的每一列對應1張圖片,該實現在下面的for函數中
83 Mat mat(total, src.size(), CV_32FC1);
84 for(int i = 0; i < src.size(); i++)
85 {
86 Mat col_tmp = mat.col(i);
87 src[i].reshape(1, total).col(0).convertTo(col_tmp, CV_32FC1, 1/255.);
88 }
89 int number_principal_compent = 4; //保留最大的主成分數
90
91 //構造pca數據結構
92 PCA pca(mat, Mat(), CV_PCA_DATA_AS_COL, number_principal_compent);
93
94
95 //pca.eigenvectors中的每一行代表輸入數據協方差矩陣一個特征向量,且是按照該協方差矩陣的特征值進行排序的
96 pca_face1 = normalize(pca.eigenvectors.row(0)).reshape(1, src[0].rows); //第一個主成分臉
97 imwrite("./result/pca_face1.jpg", pca_face1);//顯示主成分特征臉1
98 ui->face7Browser->append("<img src=./result/pca_face1.jpg>");
99
100 pca_face2 = normalize(pca.eigenvectors.row(1)).reshape(1, src[0].rows); //第二個主成分臉
101 imwrite("./result/pca_face2.jpg", pca_face2);//顯示主成分特征臉2
102 ui->face8Browser->append("<img src=./result/pca_face2.jpg>");
103
104 pca_face3 = normalize(pca.eigenvectors.row(2)).reshape(1, src[0].rows); //第三個主成分臉
105 imwrite("./result/pca_face3.jpg", pca_face3);//顯示主成分特征臉3
106 ui->face9Browser->append("<img src=./result/pca_face3.jpg>");
107
108 //將原始數據通過PCA方向投影,即通過特征向量的前面幾個作用后的數據,因此這里的dst的尺寸變小了
109 dst = pca.project(mat);
110
111 //通過方向投影重構原始人臉圖像
112 project_face1 = normalize(pca.backProject(dst).col(1)).reshape(1, src[0].rows);
113 imwrite("./result/project_face1.jpg", project_face1);
114 ui->face4Browser->append("<img src=./result/project_face1.jpg>");
115
116 project_face2 = normalize(pca.backProject(dst).col(7)).reshape(1, src[0].rows);
117 imwrite("./result/project_face2.jpg", project_face2);
118 ui->face5Browser->append("<img src=./result/project_face2.jpg>");
119
120 project_face3 = normalize(pca.backProject(dst).col(14)).reshape(1, src[0].rows);
121 imwrite("./result/project_face3.jpg", project_face3);
122 ui->face6Browser->append("<img src=./result/project_face3.jpg>");
123 }
124
125 void PCAFace::on_closeButton_clicked()
126 {
127 close();
128 }
實驗的工程代碼可以在上面的那個鏈接下載,環境搭建可以參考我之前寫個一個文章http://www.cnblogs.com/liu-jun/archive/2012/09/26/Jacky_Liu.html。
作者:Jacky_Liu,轉載或分享請注明出處。

