主成分分析(PCA)是一个统计过程,提取一个数据集最重要的特征。

假设有一组2D点,如上图所示。每个维度对应一个感兴趣的特征。有些人可能会说,这些点的设置顺序是随机的。然而,如果仔细观察,会发现这里有一个很难忽略的线性模式(由蓝色线表示)。主成分分析的一个关键点是降维。降维是减少给定数据集的维数的过程。例如,在上述情况下,可以将一组点近似为一条直线,从而将给定点的维数从2D降至1D。
此外还可以看到,这些点沿着蓝线变化最大,比沿着特征1或特征2轴变化更大。这意味着,如果你知道一个点沿着蓝色直线的位置,你就比只知道它在特征1轴或特征2轴上的位置有更多的信息。
因此,PCA允许我们找到数据变化最大的方向。事实上,在图中点的集合上运行主成分分析的结果由两个称为特征向量(eigenvector)的向量组成,它们是数据集的主成分。
每个特征向量的大小被编码到对应的特征值中,表明数据沿着主成分变化的程度。特征向量的起点是数据集中所有点的中心。将PCA应用于N维数据集,得到N个N维特征向量、N个特征值和1个N维中心点。
我们的目标是将一个给定的
p
p
p 维数据集
X
\mathbf{X}
X 转换为另一个更小维度
L
L
L 的数据集 Y。同样地,我们寻找矩阵 Y,其中 Y 是矩阵
X
\mathbf{X}
X 的Karhunen-Loève变换(KLT):
Y
=
K
L
T
{
X
}
\mathbf{Y}=\mathbb{K} \mathbb{L T}\{\mathbf{X}\}
Y=KLT{X}
假设你的数据包含 p p p 个变量的一组观察数据,你想要减少数据,以便每个观察数据可以只用 L L L 个变量来描述, L < p L < p L<p。进一步假设,数据被排列为 n n n 个数据向量 x 1 … x n x_1…x_n x1…xn,其中每一个 x i x_i xi 代表 p p p 个变量的一个组合观测量。
平均值减法是解决方案的一个组成部分,以找到一个主成分基(principal component basis),以最小化均方误差的逼近数据。因此,我们使用如下方法找到数据中心:
从数据矩阵 X \mathbf{X} X 的每一行中减去经验均值向量 u \mathbf{u} u。
储存减去平均值的数据进
n
×
p
n\times p
n×p 的矩阵
B
\mathbf{B}
B。
B
=
X
−
h
u
T
\mathbf{B}=\mathbf{X}-\mathbf{h u}^{\mathbf{T}}
B=X−huT
其中
h
\mathbf{h}
h 是一个值全为
1
1
1 的
n
×
1
n\times 1
n×1 的列向量:
h
[
i
]
=
1
,
i
=
1
,
…
,
n
h[i]=1, i=1, \ldots, n
h[i]=1,i=1,…,n
由矩阵
B
\mathbf{B}
B 与自身的外积求
p
×
p
p×p
p×p 经验协方差矩阵(empirical covariance matrix)
C
\mathbf{C}
C:
C
=
1
n
−
1
B
∗
⋅
B
\mathbf{C}=\frac{1}{n-1} \mathbf{B}^{*} \cdot \mathbf{B}
C=n−11B∗⋅B
其中*为共轭转置算子(conjugate transpose operator)。注意,如果
B
\mathbf{B}
B 完全由实数组成,这是许多应用中的情况,“共轭转置”与通常的转置相同。
计算将协方差矩阵
C
\mathbf{C}
C 对角化的特征向量矩阵
V
\mathbf{V}
V:
V
−
1
C
V
=
D
\mathbf{V}^{-1} \mathbf{C V}=\mathbf{D}
V−1CV=D
其中
D
\mathbf{D}
D 是
C
\mathbf{C}
C 的特征值的对角矩阵(diagonal matrix)。
矩阵
D
\mathbf{D}
D 将采用
p
×
p
p×p
p×p 对角矩阵的形式:
D
[
k
,
l
]
=
{
λ
k
,
k
=
l
0
,
k
≠
l
D[k, l]=\left\{λk,k=l0,k≠l
这里, λ j λ_j λj 是协方差矩阵 C \mathbf{C} C 的第 j j j 个特征值。
矩阵 V \mathbf{V} V 的维数也是 p × p p\times p p×p,包含 p p p 个列向量,每个列向量的长度为 p p p,它们表示协方差矩阵 C \mathbf{C} C 的 p p p 个特征向量。
特征值和特征向量是有序且配对的。第 j j j个特征值对应于第 j j j个特征向量。
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include
using namespace std;
using namespace cv;
// Function declarations
void drawAxis(Mat&, Point, Point, Scalar, const float);
double getOrientation(const vector<Point> &, Mat&);
void drawAxis(Mat& img, Point p, Point q, Scalar colour, const float scale = 0.2)
{
double angle = atan2( (double) p.y - q.y, (double) p.x - q.x ); // angle in radians
double hypotenuse = sqrt( (double) (p.y - q.y) * (p.y - q.y) + (p.x - q.x) * (p.x - q.x));
// Here we lengthen the arrow by a factor of scale
q.x = (int) (p.x - scale * hypotenuse * cos(angle));
q.y = (int) (p.y - scale * hypotenuse * sin(angle));
line(img, p, q, colour, 1, LINE_AA);
// create the arrow hooks
p.x = (int) (q.x + 9 * cos(angle + CV_PI / 4));
p.y = (int) (q.y + 9 * sin(angle + CV_PI / 4));
line(img, p, q, colour, 1, LINE_AA);
p.x = (int) (q.x + 9 * cos(angle - CV_PI / 4));
p.y = (int) (q.y + 9 * sin(angle - CV_PI / 4));
line(img, p, q, colour, 1, LINE_AA);
}
double getOrientation(const vector<Point> &pts, Mat &img)
{
//Construct a buffer used by the pca analysis
int sz = static_cast<int>(pts.size());
Mat data_pts = Mat(sz, 2, CV_64F);
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;
}
//Perform PCA analysis
PCA pca_analysis(data_pts, Mat(), PCA::DATA_AS_ROW);
//Store the center of the object
Point cntr = Point(static_cast<int>(pca_analysis.mean.at<double>(0, 0)),
static_cast<int>(pca_analysis.mean.at<double>(0, 1)));
//Store the eigenvalues and eigenvectors
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);
}
// Draw the principal components
circle(img, cntr, 3, Scalar(255, 0, 255), 2);
Point p1 = cntr + 0.02 * Point(static_cast<int>(eigen_vecs[0].x * eigen_val[0]), static_cast<int>(eigen_vecs[0].y * eigen_val[0]));
Point p2 = cntr - 0.02 * Point(static_cast<int>(eigen_vecs[1].x * eigen_val[1]), static_cast<int>(eigen_vecs[1].y * eigen_val[1]));
drawAxis(img, cntr, p1, Scalar(0, 255, 0), 1);
drawAxis(img, cntr, p2, Scalar(255, 255, 0), 5);
double angle = atan2(eigen_vecs[0].y, eigen_vecs[0].x); // orientation in radians
return angle;
}
int main(int argc, char** argv)
{
// Load image
CommandLineParser parser(argc, argv, "{@input | pca_test1.jpg | input image}");
parser.about( "This program demonstrates how to use OpenCV PCA to extract the orientation of an object.\n" );
parser.printMessage();
Mat src = imread( samples::findFile( parser.get<String>("@input") ) );
// Check if image is loaded successfully
if(src.empty())
{
cout << "Problem loading image!!!" << endl;
return EXIT_FAILURE;
}
imshow("src", src);
// Convert image to grayscale
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
// Convert image to binary
Mat bw;
threshold(gray, bw, 50, 255, THRESH_BINARY | THRESH_OTSU);
// Find all the contours in the thresholded image
vector<vector<Point> > contours;
findContours(bw, contours, RETR_LIST, CHAIN_APPROX_NONE);
for (size_t i = 0; i < contours.size(); i++)
{
// Calculate the area of each contour
double area = contourArea(contours[i]);
// Ignore contours that are too small or too large
if (area < 1e2 || 1e5 < area) continue;
// Draw each contour only for visualisation purposes
drawContours(src, contours, static_cast<int>(i), Scalar(0, 0, 255), 2);
// Find the orientation of each shape
getOrientation(contours[i], src);
}
imshow("output", src);
waitKey();
return EXIT_SUCCESS;
}
// Load image
CommandLineParser parser(argc, argv, "{@input | pca_test1.jpg | input image}");
parser.about( "This program demonstrates how to use OpenCV PCA to extract the orientation of an object.\n" );
parser.printMessage();
Mat src = imread( samples::findFile( parser.get<String>("@input") ) );
// Check if image is loaded successfully
if(src.empty())
{
cout << "Problem loading image!!!" << endl;
return EXIT_FAILURE;
}
imshow("src", src);
// Convert image to grayscale
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
// Convert image to binary
Mat bw;
threshold(gray, bw, 50, 255, THRESH_BINARY | THRESH_OTSU);
// Find all the contours in the thresholded image
vector<vector<Point> > contours;
findContours(bw, contours, RETR_LIST, CHAIN_APPROX_NONE);
for (size_t i = 0; i < contours.size(); i++)
{
// Calculate the area of each contour
double area = contourArea(contours[i]);
// Ignore contours that are too small or too large
if (area < 1e2 || 1e5 < area) continue;
// Draw each contour only for visualisation purposes
drawContours(src, contours, static_cast<int>(i), Scalar(0, 0, 255), 2);
// Find the orientation of each shape
getOrientation(contours[i], src);
}
//Construct a buffer used by the pca analysis
int sz = static_cast<int>(pts.size());
Mat data_pts = Mat(sz, 2, CV_64F);
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;
}
//Perform PCA analysis
PCA pca_analysis(data_pts, Mat(), PCA::DATA_AS_ROW);
//Store the center of the object
Point cntr = Point(static_cast<int>(pca_analysis.mean.at<double>(0, 0)),
static_cast<int>(pca_analysis.mean.at<double>(0, 1)));
//Store the eigenvalues and eigenvectors
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);
}
首先,数据需要排列在一个大小为 n × 2 n × 2 n×2 的矩阵中,其中 n n n 是我们所拥有的数据点的数量。然后我们就可以进行主成分分析。计算出的均值(即质心)存储在cntr变量中,特征向量和特征值存储在相应的 std::vector 中。
// Draw the principal components
circle(img, cntr, 3, Scalar(255, 0, 255), 2);
Point p1 = cntr + 0.02 * Point(static_cast<int>(eigen_vecs[0].x * eigen_val[0]), static_cast<int>(eigen_vecs[0].y * eigen_val[0]));
Point p2 = cntr - 0.02 * Point(static_cast<int>(eigen_vecs[1].x * eigen_val[1]), static_cast<int>(eigen_vecs[1].y * eigen_val[1]));
drawAxis(img, cntr, p1, Scalar(0, 255, 0), 1);
drawAxis(img, cntr, p2, Scalar(255, 255, 0), 5);
double angle = atan2(eigen_vecs[0].y, eigen_vecs[0].x); // orientation in radians
double angle = atan2( (double) p.y - q.y, (double) p.x - q.x ); // angle in radians
double hypotenuse = sqrt( (double) (p.y - q.y) * (p.y - q.y) + (p.x - q.x) * (p.x - q.x));
// Here we lengthen the arrow by a factor of scale
q.x = (int) (p.x - scale * hypotenuse * cos(angle));
q.y = (int) (p.y - scale * hypotenuse * sin(angle));
line(img, p, q, colour, 1, LINE_AA);
// create the arrow hooks
p.x = (int) (q.x + 9 * cos(angle + CV_PI / 4));
p.y = (int) (q.y + 9 * sin(angle + CV_PI / 4));
line(img, p, q, colour, 1, LINE_AA);
p.x = (int) (q.x + 9 * cos(angle - CV_PI / 4));
p.y = (int) (q.y + 9 * sin(angle - CV_PI / 4));
line(img, p, q, colour, 1, LINE_AA);
该代码打开一个图像,找到检测到的感兴趣的对象的方向,然后通过绘制检测到的感兴趣的对象的轮廓、中心点和关于提取的方向的x轴、y轴来可视化结果。