摘要
上一篇详细叙述了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)