摘要
上一篇詳細敘述了PCA的數學原理opencv——PCA(主要成分分析)數學原理推導 - 唯有自己強大 - 博客園 (cnblogs.com)
本篇就來說一說PCA在opencv項目中的應用:
- 獲取物體主要方向(形心)
- 對數據集降維處理
1️⃣什么是PCA?
PCA的主要思想是尋找到數據的主軸方向,由主軸構成一個新的坐標系,這里的維數可以比原維數低,然后數據由原坐標系向新的坐標系投影,這個投影的過程就可以是降維的過程。
PCA 是一種非監督的算法, 能找到很好地代表所有樣本的方向, 但這個方向對於分類未必是最有利的,通過下圖可以更直觀地了解PCA的作用:
假設有上圖所示的一組2維點,其中每個維度與您感興趣的功能相對應。有些人可能會爭辯說,這些點是隨機的,但有一個線性模式(由藍線表示),這是很難忽視的。可以將一組點近似於單行,即將點的尺寸從2維降低到1維。維度降低是人工智能和數據挖掘的關鍵技術。你還可以看到,這些點沿藍線變化最大,比沿Feature1 軸或Feature2軸變化的要多。這意味着,如果你知道沿藍線的點的位置,則你掌握的關於該點的信息比你只知道它在Feature1 軸或Feature2軸上的位置要多。
因此,PCA 是一種數學工具,它使我們能夠找到數據變化最大的方向。事實上,在圖表中的一組點上運行 PCA 的結果由 2 個稱為eigenvector(特征向量) 組成,這些載體是數據集的主要組件。
每個 eigenvector(特征向量) 的大小被編碼在相應的eigenvalue(特征值)中,並指示數據沿主要組件變化的程度。(通過這個特性可以獲取物體(輪廓)的主要方向)
eigenvectors(特征向量) 的開頭是數據集中所有點的中心。(通過這個特性可以獲取物體(輪廓)的形心)
2️⃣opencv中的PCA類
PCA類的成員函數包括構造函數、運算符重載()、project、backProject這幾個函數,還包括成員變量eigenvectors、eigenvalues、mean。使用也很方便。比如我要計算一組向量的PCA,我們只需要定義個PCA實例,獲得主成分,調用project測試新樣本,也可以再調用backProject重建原始向量,是project的一個逆運算。
🎈opencv中PCA類的主要函數有:
- 構造函數PCA
PCA::PCA(InputArray data, InputArray mean, int flags, int maxComponents=0) data //輸入數據(可以是輪廓點集) mean //數據零均值,為空(Mat())時自動計算 flag //表示數據提供的方式(0表示按行輸入,1表示按列輸入) maxComponents //保留多少特征值(默認全保留)
- 原圖像,投影到新的空間
Mat PCA::project(InputArray vec) const
- 進行project之后的數據,反映攝到原始圖像
Mat PCA::backProject(InputArray vec) const
變量值有:mean--------原始數據的均值
eigenvalues--------協方差矩陣的特征值
eigenvectors--------特征向量
3️⃣PCA獲取物體主要方向(形心)
opencv實現:
int main(int argc, char** argv) { double getOrientation(vector<Point> &pts, Mat &img); Mat src = imread("D:/opencv練習圖片/PCA分析1.png"); imshow("輸入圖像", src); Mat gray,binary; cvtColor(src, gray, COLOR_BGR2GRAY); //閾值處理 threshold(gray, binary, 150, 255, THRESH_BINARY); imshow("二值化", binary); //尋找輪廓 vector<vector<Point> > contours; vector<Vec4i> hierarchy; findContours(binary, contours, hierarchy, RETR_LIST, CHAIN_APPROX_NONE); //輪廓分析,找到工件 for (size_t i = 0; i < contours.size(); ++i) { //計算輪廓大小 double area = contourArea(contours[i]); //去除過小或者過大的輪廓區域(科學計數法表示le2表示1X10的2次方) if (area < 1e2 || 1e4< area) continue; //繪制輪廓 drawContours(src, contours, i, Scalar(0, 0, 255), 2, 8, hierarchy, 0); //尋找每一個輪廓的方向 double angle= getOrientation(contours[i], src); cout << angle << endl; } imshow("結果", src); waitKey(0); return 0; } //獲得構建的主要方向 double getOrientation(vector<Point> &pts, Mat &img) { //構建pca數據。這里做的是將輪廓點的x和y作為兩個維壓到data_pts中去。 Mat data_pts = Mat(pts.size(), 2, CV_64FC1);//使用mat來保存數據,也是為了后面pca處理需要 for (int i = 0; i < data_pts.rows; ++i) { data_pts.at<double>(i, 0) = pts[i].x; data_pts.at<double>(i, 1) = pts[i].y; } //執行PCA分析 PCA pca_analysis(data_pts, Mat(), 0); //獲得最主要分量(均值),在本例中,對應的就是輪廓中點,也是圖像中點 Point pos = Point(pca_analysis.mean.at<double>(0, 0), pca_analysis.mean.at<double>(0, 1)); //存儲特征向量和特征值 vector<Point2d> eigen_vecs(2); vector<double> eigen_val(2); for (int i = 0; i < 2; ++i) { eigen_vecs[i] = Point2d(pca_analysis.eigenvectors.at<double>(i, 0), pca_analysis.eigenvectors.at<double>(i, 1)); eigen_val[i] = pca_analysis.eigenvalues.at<double>(i, 0);//在輪廓/圖像中點繪制小圓 circle(img, pos, 3, CV_RGB(255, 0, 255), 2); //計算出直線,在主要方向上繪制直線(每個特征向量乘以其特征值並轉換為平均位置。有一個 0.02 的縮放系數,它只是為了確保矢量適合圖像並且沒有 10000 像素的長度) line(img, pos, pos + 0.02 * Point(eigen_vecs[0].x * eigen_val[0], eigen_vecs[0].y * eigen_val[0]), CV_RGB(255, 255, 0)); line(img, pos, pos + 0.02 * Point(eigen_vecs[1].x * eigen_val[1], eigen_vecs[1].y * eigen_val[1]), CV_RGB(0, 255, 255)); //最終計算並返回一個最強的(即具有最大特征值)的特征向量的角度 return atan2(eigen_vecs[0].y, eigen_vecs[0].x); }
在圖像上運行 PCA 后的結果如圖,由此產生的軸是數據點差異最大的軸,這不需要反映形狀的關鍵結構特征,盡管如此,它還是對方向的有效描述,可以獲取任何形狀。
4️⃣對數據集降維處理
對一副寬p、高q的二維灰度圖,要完整表示該圖像,需要m = p*q維的向量空間,比如100*100的灰度圖像,它的向量空間為100*100=10000。下圖是一個3*3的灰度圖和表示它的向量表示:
該向量為行向量,共9維,用變量表示就是[v0, v1, v2, v3, v4, v5, v6, v7, v8],其中v0...v8,的范圍都是0-255。
現在的問題是假如我們用1*10000向量,表示100*100的灰度圖,是否向量中的10000維對我們同樣重要?肯定不是這樣的,有些維的值可能對圖像更有用,有些維相對來說作用小些。為了節省存儲空間,我們需要對10000維的數據進行降維操作,這時就用到了PCA算法,該s算法主要就是用來處理降維的,降維后會盡量保留更有意義的維數,它的思想就是對於高維的數據集來說,一部分維數表示大部分有意義的數據。
🎈下面我們在OpenCV中看一個計算PCA的例子:
1.首先讀入10副人臉圖像,這些圖像大小相等,是一個人的各種表情圖片。
2.把圖片轉為1*pq的一維形式,p是圖像寬,q是圖像高。這時我們的S矩陣就是10行,每行是pq維的向量。
3.然后我們在S上執行PCA算法,設置K=5,求得5個特征向量,這5個特征向量就是我們求得的特征臉,用這5個特征臉圖像,可以近似表示之前的十副圖像。
我們輸入的10副圖像為:
opencv實現:
//把圖像歸一化為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; } //轉化給定的圖像為行矩陣 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++) { Mat xi = data.row(i); //轉化為1行,n列的格式 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; db.push_back(imread("D:/opencv練習圖片/s1/1.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/2.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/3.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/4.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/5.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/6.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/7.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/8.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/9.png", IMREAD_GRAYSCALE)); db.push_back(imread("D:/opencv練習圖片/s1/10.png", IMREAD_GRAYSCALE)); // Build a matrix with the observations in row: Mat data = asRowMatrix(db, CV_32FC1); // PCA算法保持5主成分分量 int num_components = 5; //執行pca算法 PCA pca(data, Mat(), 0, num_components); //copy 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)); imshow("pc4", norm_0_255(pca.eigenvectors.row(3)).reshape(1, db[0].rows)); imshow("pc5", norm_0_255(pca.eigenvectors.row(4)).reshape(1, db[0].rows)); waitKey(0); return 0; }
得到的5副特征臉為:
得到的一副均值臉:
參考博文:OpenCV學習(35) OpenCV中的PCA算法 - 邁克老狼2012 - 博客園 (cnblogs.com)
Object Orientation, Principal Component Analysis & OpenCV | Robospace (wordpress.com)