【opencv-ml】主成分分析简介Principal Component Analysis (PCA)

Goal

在本教程中,您将学习如何:

使用 OpenCV 类 cv::PCA 计算对象的方向orientation of an object

What is PCA?

主成分分析 (PCA) 是一种统计过程(statistical procedure),用于提取数据集最重要的特征

93b22da5c82d315525145f32c24d1e29.png

假设您有一组 2D 点,如上图所示。每个维度对应一个您感兴趣的特征。这里有些人可能会争辩说这些点是按随机顺序设置的。但是,如果您看得更清楚,您会发现有一个难以消除的线性模式(由蓝线表示)。PCA 的一个关键点是降维。降维是减少给定数据集的维数的过程。例如,在上述情况下,可以将点集近似为一条线,因此,将给定点的维数从 2D 减少到 1D。

此外,您还可以看到,沿蓝线的点变化最大,大于沿特征 1 或特征 2 轴的变化。这意味着,如果您知道某个点沿蓝线的位置,则与仅知道它在特征 1 轴或特征 2 轴上的位置相比,您可以获得更多关于该点的信息。

因此,PCA使我们能够找到数据变化最大的方向。事实上,在图中的一组点上运行 PCA 的结果由 2 个称为特征向量的向量组成,它们是数据集的主要成分

862d431cc69ec73d5a708f110cd38c7c.png

每个特征向量的大小都编码在相应的特征值中,并指示数据沿主成分变化的程度。 特征向量的起点是数据集中所有点的中心。将PCA应用于 N 维数据集会产生 N 个 N 维特征向量N 个特征值1 个 N 维中心点。足够的理论,让我们看看如何将这些想法放入代码中。

How are the eigenvectorsand eigenvalues computed?

如何计算特征向量和特征值?

目标是将给定的数据集 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 中。

6ed76a2ad24874ecac2da16b0136c2da.png

  • 计算与平均值的偏差Calculate the deviations from the mean

均值减法Mean subtraction)是寻找主成分基(principal component basis)的求解方案的一个组成部分,该主成分基可使近似数据的均方误差square error)最小化。因此,我们将数据集中如下:

从数据矩阵 X 的每一行减去经验平均向量Subtract the empirical mean vector) u。

将减去均值的数据存储在  n×p 矩阵 B中。

f351bb27b5f809c3000ec66df383745e.png

其中 h是一个全为 1 的 n×1 列向量:

75aaf5aa2a1da7bd61a0b144c1165cf0.png

  • 找到协方差矩阵Find the covariance matrix

从矩阵 B 与其自身的外积求出 p×p 经验协方差矩阵 C:

419f590b836574c318430ac04824381c.png

其中 * 是共轭转置运算符。请注意,如果 B 完全由实数组成,在许多应用程序中都是这种情况,则“共轭转置”与常规转置相同

  • 求协方差矩阵的特征向量和特征值Find the eigenvectors and eigenvalues of the covariancematrix

计算对角化协方差矩阵 C的特征向量矩阵V

55dfc1c5c2c3f50ee2082e0856cb0d00.png

其中 D是 C 的特征值的对角矩阵。

矩阵 D将采用 p×p 对角矩阵的形式:

9824dd9729777eab3f0f4d56c5439282.png

这里,λ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 的原始教程。

Source Code

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 <iostream>


using namespace std;
using namespace cv;


// 函数声明
void drawAxis(Mat&, Point, Point, Scalar, const float);
double getOrientation(const vector<Point> &, 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<Point> &pts, Mat &img)//pts:轮廓点集     
{
    //! [pca]
    //构造 pca 分析使用的缓冲区 Construct a buffer used by the pca analysis
    int sz = static_cast<int>(pts.size());//轮廓点数
    Mat data_pts = Mat(sz, 2, CV_64F);//组织数据集Nx2矩阵
    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 分析 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)));//x,y平均值。轮廓的质心


    //存储特征值和特征向量 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));//第i个特征向量


        eigen_val[i] = pca_analysis.eigenvalues.at<double>(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<int>(eigen_vecs[0].x * eigen_val[0]), static_cast<int>(eigen_vecs[0].y * eigen_val[0]));//长度缩放0.02。特征向量1终点
    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]));//特征向量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<String>("@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<vector<Point> > 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<int>(i), Scalar(0, 0, 255), 2);
        //找到每个形状的方向
        getOrientation(contours[i], src);
    }
    //! [contours]


    imshow("output", src);


    waitKey();
    return EXIT_SUCCESS;
}

Explanation

  • 读取图像并将其转换为二进制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<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);
  • 提取感兴趣的对象Extract objects of     interest

然后按大小查找和过滤轮廓,并获得保留轮廓的方向。

// 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);
    }
  • 提取方向Extract orientation

方向是通过调用 getOrientation() 函数提取的,该函数执行所有 PCA 过程。

//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 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<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);

Results

该代码打开一个图像,找到检测到的感兴趣对象(轮廓)的方向,然后通过绘制检测到的感兴趣对象的轮廓、中心点以及与提取的方向有关的 x 轴、y 轴来可视化结果。

4628ab3620f69e53c4a61440d38b8443.png

80d96d6e8e9e56e1b958f3cc3b57dd37.png

参考:
https://docs.opencv.org/4.5.5/d1/dee/tutorial_introduction_to_pca.html 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值