在本教程中,您将学习如何:
使用 OpenCV 类 cv::PCA 计算对象的方向orientation of an object。
主成分分析 (PCA) 是一种统计过程(statistical procedure),用于提取数据集最重要的特征。
假设您有一组 2D 点,如上图所示。每个维度对应一个您感兴趣的特征。这里有些人可能会争辩说这些点是按随机顺序设置的。但是,如果您看得更清楚,您会发现有一个难以消除的线性模式(由蓝线表示)。PCA 的一个关键点是降维。降维是减少给定数据集的维数的过程。例如,在上述情况下,可以将点集近似为一条线,因此,将给定点的维数从 2D 减少到 1D。
此外,您还可以看到,沿蓝线的点变化最大,大于沿特征 1 或特征 2 轴的变化。这意味着,如果您知道某个点沿蓝线的位置,则与仅知道它在特征 1 轴或特征 2 轴上的位置相比,您可以获得更多关于该点的信息。
因此,PCA使我们能够找到数据变化最大的方向。事实上,在图中的一组点上运行 PCA 的结果由 2 个称为特征向量的向量组成,它们是数据集的主要成分。
每个特征向量的大小都编码在相应的特征值中,并指示数据沿主成分变化的程度。 特征向量的起点是数据集中所有点的中心。将PCA应用于 N 维数据集会产生 N 个 N 维特征向量、N 个特征值和 1 个 N 维中心点。足够的理论,让我们看看如何将这些想法放入代码中。
如何计算特征向量和特征值?
目标是将给定的数据集 X的维度p 转换为较小维度 L的替代数据集 Y。等效地,我们正在寻找矩阵 Y,其中 Y 是矩阵 X 的 Karhunen-Loève 变换 (KLT):
Y=KLT{X}
组织数据集Organize the data set
假设您的数据包含一组 p 个变量的观察值,并且您希望减少数据,以便可以仅用 L 个变量来描述每个观察值,L < p。进一步假设,数据被排列为一组 n 个数据向量 x1...xn,每个xi 代表 p 个变量的单个分组观察。
将 x1...xn 写为行向量,每个向量都有p 列。
将行向量放入维度为n×p的单个矩阵 X 中
计算经验平均值Calculate the empirical mean
求沿每个维度 j=1,...,p 的经验平均值。
将计算的平均值放入维度为 p×1 的经验平均向量 u 中。
计算与平均值的偏差Calculate the deviations from the mean
均值减法(Mean subtraction)是寻找主成分基(principal component basis)的求解方案的一个组成部分,该主成分基可使近似数据的均方误差(square error)最小化。因此,我们将数据集中如下:
从数据矩阵 X 的每一行中减去经验平均向量(Subtract the empirical mean vector) u。
将减去均值的数据存储在 n×p 矩阵 B中。
其中 h是一个全为 1 的 n×1 列向量:
找到协方差矩阵Find the covariance matrix
从矩阵 B 与其自身的外积求出 p×p 经验协方差矩阵 C:
其中 * 是共轭转置运算符。请注意,如果 B 完全由实数组成,在许多应用程序中都是这种情况,则“共轭转置”与常规转置相同。
求协方差矩阵的特征向量和特征值Find the eigenvectors and eigenvalues of the covariancematrix
计算对角化协方差矩阵 C的特征向量矩阵V:
其中 D是 C 的特征值的对角矩阵。
矩阵 D将采用 p×p 对角矩阵的形式:
这里,λj 是协方差矩阵 C 的第 j 个特征值
矩阵 V的维度也是 p x p,包含 p 个列向量,每个列向量的长度为 p,它们表示协方差矩阵 C 的 p 个特征向量。
特征值和特征向量是有序和配对的。第 j 个特征值对应于第 j 个特征向量。
note:
来源 [1] robospace.wordpress.com 、[2] https://en.wikipedia.org/wiki/Principal_component_analysis并特别感谢 Svetlin Penkov 的原始教程。
https://github.com/opencv/opencv/tree/4.x/samples/cpp/tutorial_code/ml/introduction_to_pca/introduction_to_pca.cpp
/**
* @file introduction_to_pca.cpp
* @brief This program demonstrates how to use OpenCV PCA to extract the orientation of an object
* 该程序演示了如何使用 OpenCV PCA 来提取对象的方向
* @author OpenCV team
*/
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include
using namespace std;
using namespace cv;
// 函数声明
void drawAxis(Mat&, Point, Point, Scalar, const float);
double getOrientation(const vector &, Mat&);
/**
* @function drawAxis
*/
void drawAxis(Mat& img, Point p, Point q, Scalar colour, const float scale = 0.2)
{
//! [可视化1]
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));//斜边 长度
// 在这里,我们将箭头延长了一个比例因子scale 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);//单箭头
//! [visualization1]
}
/**
* @function getOrientation
*/
double getOrientation(const vector &pts, Mat &img)//pts:轮廓点集
{
//! [pca]
//构造 pca 分析使用的缓冲区 Construct a buffer used by the pca analysis
int sz = static_cast(pts.size());//轮廓点数
Mat data_pts = Mat(sz, 2, CV_64F);//组织数据集Nx2矩阵
for (int i = 0; i < data_pts.rows; i++)
{
data_pts.at(i, 0) = pts[i].x;
data_pts.at(i, 1) = pts[i].y;
}
//执行 PCA 分析 Perform PCA analysis
PCA pca_analysis(data_pts, Mat(), PCA::DATA_AS_ROW);
//存储(轮廓)对象的中心 Store the center of the object
Point cntr = Point(static_cast(pca_analysis.mean.at(0, 0)),
static_cast(pca_analysis.mean.at(0, 1)));//x,y平均值。轮廓的质心
//存储特征值和特征向量 Store the eigenvalues and eigenvectors
vector eigen_vecs(2);//两个特征向量
vector eigen_val(2);//两个特征值
for (int i = 0; i < 2; i++)
{
eigen_vecs[i] = Point2d(pca_analysis.eigenvectors.at(i, 0),
pca_analysis.eigenvectors.at(i, 1));//第i个特征向量
eigen_val[i] = pca_analysis.eigenvalues.at(i);//第i个特征值
}
//! [pca]
//! [visualization]
// 画出主成分 Draw the principal components
circle(img, cntr, 3, Scalar(255, 0, 255), 2);//轮廓质心画圆
Point p1 = cntr + 0.02 * Point(static_cast(eigen_vecs[0].x * eigen_val[0]), static_cast(eigen_vecs[0].y * eigen_val[0]));//长度缩放0.02。特征向量1终点
Point p2 = cntr - 0.02 * Point(static_cast(eigen_vecs[1].x * eigen_val[1]), static_cast(eigen_vecs[1].y * eigen_val[1]));//特征向量2终点
drawAxis(img, cntr, p1, Scalar(0, 255, 0), 1);//主成分1
drawAxis(img, cntr, p2, Scalar(255, 255, 0), 5);//主成分2
double angle = atan2(eigen_vecs[0].y, eigen_vecs[0].x); // (弧度值)方向 orientation in radians
//! [visualization]
return angle;
}
/**
* @function main
*/
int main(int argc, char** argv)
{
//! [pre-process]
//加载图像
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("@input") ) );
// Check if image is loaded successfully
if(src.empty())
{
cout << "Problem loading image!!!" << endl;
return EXIT_FAILURE;
}
imshow("src", src);
// 转换为灰度图
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
// 图像二值化 Convert image to binary
Mat bw;
threshold(gray, bw, 50, 255, THRESH_BINARY | THRESH_OTSU);
//! [pre-process]
//! [contours]
//找到阈值化图像中的所有轮廓
vector > contours;
findContours(bw, contours, RETR_LIST, CHAIN_APPROX_NONE);
for (size_t i = 0; i < contours.size(); i++)
{
// 计算每个轮廓的面积
double area = contourArea(contours[i]);
// 忽略过小或过大的轮廓
if (area < 1e2 || 1e5 < area) continue;
// 仅出于可视化目的绘制每个轮廓
drawContours(src, contours, static_cast(i), Scalar(0, 0, 255), 2);
//找到每个形状的方向
getOrientation(contours[i], src);
}
//! [contours]
imshow("output", src);
waitKey();
return EXIT_SUCCESS;
}
读取图像并将其转换为二进制Read image and convert it to binary
在这里,我们应用必要的预处理程序(pre-processing procedures),以便能够检测到感兴趣的对象。
// 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("@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);
提取感兴趣的对象Extract objects of interest
然后按大小查找和过滤轮廓,并获得保留轮廓的方向。
// Find all the contours in the thresholded image
vector > 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(i), Scalar(0, 0, 255), 2);
// Find the orientation of each shape
getOrientation(contours[i], src);
}
提取方向Extract orientation
方向是通过调用 getOrientation() 函数提取的,该函数执行所有 PCA 过程。
//Construct a buffer used by the pca analysis
int sz = static_cast(pts.size());
Mat data_pts = Mat(sz, 2, CV_64F);
for (int i = 0; i < data_pts.rows; i++)
{
data_pts.at(i, 0) = pts[i].x;
data_pts.at(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(pca_analysis.mean.at(0, 0)),
static_cast(pca_analysis.mean.at(0, 1)));
//Store the eigenvalues and eigenvectors
vector eigen_vecs(2);
vector eigen_val(2);
for (int i = 0; i < 2; i++)
{
eigen_vecs[i] = Point2d(pca_analysis.eigenvectors.at(i, 0),
pca_analysis.eigenvectors.at(i, 1));
eigen_val[i] = pca_analysis.eigenvalues.at(i);
}
首先,数据需要排列在一个大小为 n x 2 的矩阵中,其中 n 是我们拥有的数据点的数量。然后我们可以执行 PCA 分析。计算的平均值(即质心)存储在cntr变量中,特征向量和特征值存储在相应的 std::vector 中。
可视化结果Visualize result
最终结果通过 drawAxis() 函数可视化,其中主成分以直线绘制,每个特征向量乘以其特征值并转换为平均位置。
// Draw the principal components
circle(img, cntr, 3, Scalar(255, 0, 255), 2);
Point p1 = cntr + 0.02 * Point(static_cast(eigen_vecs[0].x * eigen_val[0]), static_cast(eigen_vecs[0].y * eigen_val[0]));
Point p2 = cntr - 0.02 * Point(static_cast(eigen_vecs[1].x * eigen_val[1]), static_cast(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 轴来可视化结果。
参考:
https://docs.opencv.org/4.5.5/d1/dee/tutorial_introduction_to_pca.html