美文网首页
CV03-01:PCA应用

CV03-01:PCA应用

作者: 杨强AT南京 | 来源:发表于2019-11-29 15:32 被阅读0次

  PCA的算法应该是比较简单的,一个基于方差的投影降维方式,降维的维度采用方差最大的方式(方差大小反应样本的离散程度,越离散可分性理论上应该越好),PCA降维可以提高分类器的分类效果。本主题使用OpenCV提供的例子,说明PCA可以判别物体的方向。本主题包含两个例子的实现代码:
  1. 鸢尾花的2个特征数据集的分散方向可视化;
  2. 物体方向判别可视化;


提示:OpenCV中PCA不在ml模块,而是在core模块中。


一.鸢尾花的数据集分散方向可视化

效果

鸢尾花第1与3个特征的离散方向可视化效果

代码

/*
    这个例子中,高宽的比例最好为1,因为高宽比例不一致,主成分的向量的正交,在变换后,会变成不正交的效果
 */

#include <opencv2/opencv.hpp>
#include <iostream>
#include <cmath>
#include <fstream>
#include <sstream>
// 函数定义
void drawAxis(cv::Mat&, cv::Point, cv::Point, cv::Scalar, const float);
// 加载鸢尾花数据集
void loadIrisFromTxt(cv::String, cv::Mat&, cv::Mat&);
void visual_pca(cv::Mat,cv::Mat&, int=512,int h=512);

// 主程序
int main(int argc, const char** argv) {
    // 读取鸢尾花数据集
    cv::Mat data,label;
    loadIrisFromTxt("../iris.txt", data, label);
    // std::cout << data << std::endl;
    // 取其中两列处理
    cv::Mat newData;
    cv::hconcat(data.rowRange(50,100).col(0), data.rowRange(50,100).col(2), newData);

    // 用来缓存所有的图像
    cv::Mat img_out;
    visual_pca(newData,img_out, 600, 600);   // 如果高宽不一致,会产生仿射变换
    cv::imwrite("../iris.png",img_out);
    return 0;
}

void drawAxis(cv::Mat& img, cv::Point p, cv::Point q, cv::Scalar colour, const float scale = 0.2){
    /*
        cv::Mat& img        :需要绘制的图像
        cv::Point p         :绘制坐标轴线段的两个点之一:坐标轴的起点
        cv::Point q         :绘制坐标轴线段的两个点之二:坐标轴的终点
        cv::Scalar colour   :绘制的颜色
        const float scale   :坐标轴的放大因子
     */

    // 计算两个点与原点形成的向量的夹角(用于延长线段的计算)
    double angle = atan2( (double) p.y - q.y, (double) p.x - q.x );
    // 计算两个点之间距离(用于延长线段的计算)
    double hypotenuse = sqrt( (double) (p.y - q.y) * (p.y - q.y) + (p.x - q.x) * (p.x - q.x));

    // 把坐标轴按照放大因子scale延长,并绘制(p不动,按照原来pq的联线方向,增加线段长度)
    q.x = (int) (p.x - scale * hypotenuse * cos(angle));
    q.y = (int) (p.y - scale * hypotenuse * sin(angle));
    line(img, p, q, colour, 1, cv::LINE_AA);

    // 绘制坐标轴的箭头(以终点q为起点的两个小线段,与原来线段成45°夹角),箭头线段9像素
    int len = 9;
    // 箭头:第一个线段(逆时针旋转45°)
    p.x = (int) (q.x + len * cos(angle + CV_PI / 4));
    p.y = (int) (q.y + len * sin(angle + CV_PI / 4));
    line(img, p, q, colour, 1, cv::LINE_AA);
    // 箭头:第二个线段(顺时针旋转45°)
    p.x = (int) (q.x + len * cos(angle - CV_PI / 4));
    p.y = (int) (q.y + len * sin(angle - CV_PI / 4));
    line(img, p, q, colour, 1, cv::LINE_AA);
}


// 从txt文件加载鸢尾花数据
void loadIrisFromTxt(cv::String filename, cv::Mat &data, cv::Mat &label){
    int rows = 150;
    int cols = 4;
    data.create(rows, cols, CV_32FC1);
    label.create(rows, 1, CV_32SC1);
    // 打开文件
    std::ifstream ifs(filename, std::ifstream::in);
    char buf[256];
    int row_id = 0;
    while(ifs.good()){
        bzero(buf, sizeof(buf));
        ifs.getline(buf, sizeof(buf)-1);
        std::istringstream istr(buf);
        float a, b, c, d;
        char buffer[256] = {0};
        char _;
        istr >> a >> _ >> b >> _ >> c >> _ >> d >> _ >> buffer;
        // std::cout << a << "," << b << "," << c << "," << d << std::endl;
        cv::Mat row_one = (cv::Mat_<float>(1,4) << a, b, c, d);
        row_one.copyTo(data.row(row_id));
        
        if(strcmp(buffer, "Iris-setosa") == 0){
            label.at<int>(row_id, 0) = 0;
        }
        else if (strcmp(buffer, "Iris-versicolor") == 0){
            label.at<int>(row_id, 0) = 1;
        }
        else if (! strcmp(buffer, "Iris-virginica")){
            label.at<int>(row_id, 0) = 2;
        }
        row_id ++;
    }
    // 关闭文件
    ifs.close();
};

// 可视化分类器与数据集
void visual_pca(cv::Mat data,      // 可视化数据集,必须2两个特征
                cv::Mat &out,       // 输出的可视化图
                int w,          // 默认可视化区域
                int h){         // 默认可视化区域
    out = cv::Mat(h, w, CV_8UC3, cv::Scalar(255, 255, 255));
    // 计算缩放比例
    double scale_x, scale_y; 
    // 计算数据集的x,y最大最小值
    double max_x, min_x;
    double max_y, min_y;
    cv::minMaxIdx(data.col(0), &min_x, &max_x);
    cv::minMaxIdx(data.col(1), &min_y, &max_y);
    // 扩边,避免点绘制在边界上
    min_x -= 0.5;
    max_x += 0.5;
    min_y -= 0.5;
    max_y += 0.5;

    scale_x = w / (max_x - min_x);
    scale_y = h / (max_y - min_y);
    // 定义绘制的颜色
    cv::Vec3b r(0, 0, 255), g(0, 255, 0), b(255, 0, 0);
    // 绘制样本点
    for(int i = 0; i < data.rows; i++){
        double d_x = data.at<float>(i, 0);
        double d_y = data.at<float>(i, 1);
        // 先平移到0点再放大
        d_x -= min_x;
        d_y -= min_y;
        d_x *= scale_x;
        d_y *= scale_y; 
        // 取整绘制
        circle(out, cv::Point( (int) d_x,  (int) d_y ), 1, r, -1);
    }

    // 计算主成份
    cv::PCA pca(data, cv::Mat(), cv::PCA::DATA_AS_ROW);
    // 获取中心点
    double c_x = pca.mean.at<float>(0, 0);
    double c_y = pca.mean.at<float>(0, 1);
    c_x -= min_x;
    c_y -= min_y;
    c_x *= scale_x;
    c_y *= scale_y; 
    // 平移方法到(520,520)区域
    cv::Point cntr = cv::Point(
        static_cast<int>(c_x),
        static_cast<int>(c_y)
    );
    // 绘制中心点(均值点)
    circle(out, cntr, 5, cv::Scalar(255, 0, 255), 2);

    // 获取特征值与特征向量
    std::vector<cv::Point2d> eigen_vecs(2);
    std::vector<double> eigen_val(2);
    for (int i = 0; i < 2; i++){
        // 特征向量
        double e_x = pca.eigenvectors.at<float>(i, 0);
        double e_y = pca.eigenvectors.at<float>(i, 1);
        // 主成分向量
        eigen_vecs[i] = cv::Point2d(e_x,e_y);
        // 特征值(不放大改变)
        eigen_val[i] = pca.eigenvalues.at<float>(i);
    }
    
    // 计算两个主成分向量相对均值点的坐标(平移到均值点为中心):主成分需要变换后根据均值点平移
    cv::Point p1 = cntr + cv::Point(
        static_cast<int>(eigen_vecs[0].x * eigen_val[0] * scale_x), 
        static_cast<int>(eigen_vecs[0].y * eigen_val[0] * scale_y));
    cv::Point p2 = cntr - cv::Point(                            // 加减仅仅改变主成分向量相对均值点的方向
        static_cast<int>(eigen_vecs[1].x * eigen_val[1] * scale_x), 
        static_cast<int>(eigen_vecs[1].y * eigen_val[1] * scale_y));
    // 绘制主成分向量
    
    drawAxis(out, cntr, p1, cv::Scalar(0, 255, 0), 1.5);   // 放大1.5倍
    drawAxis(out, cntr, p2, cv::Scalar(255, 255, 0), 5.0); // 放大5倍
}

二.物体方向判别可视化

效果

物体方向判别可视化效果

代码

#include <opencv2/opencv.hpp>
#include <iostream>
#include <cmath>
// 函数定义
void drawAxis(cv::Mat&, cv::Point, cv::Point, cv::Scalar, const float);
// 使用PCA计算方向
double getOrientation(const std::vector<cv::Point> &, cv::Mat&);

int main(int argc, const char** argv) {
    // 读取图像
    cv::Mat src = cv::imread("../pca.jpg");
    // 使用灰度图像来处理,所以先转换为灰度图像
    cv::Mat gray;
    cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
    // 把图像分成2值矩阵0-1
    cv::Mat bw;
    cv::threshold(gray, bw, 50, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);

    // 按照轮廓,把图像分成多个数据集
    std::vector<std::vector<cv::Point> > contours;
    findContours(bw, contours, cv::RETR_LIST, cv::CHAIN_APPROX_NONE);
    // 循环对每个数据集做主成分分析,并可视化
    for (size_t i = 0; i < contours.size(); i++){
        // 计算轮廓的面积
        double area = cv::contourArea(contours[i]);
        // 过滤掉异常区域的数据集
        if (area < 1e2 || 1e5 < area) continue;

        // 在原图像上绘制轮廓
        drawContours(src, contours, static_cast<int>(i), cv::Scalar(0, 0, 255), 2);
        // 绘制每个数据集的主成分到源图像。
        getOrientation(contours[i], src);

        cv::imwrite("../pca_out.png", src);

    }

    return 0;
}

void drawAxis(cv::Mat& img, cv::Point p, cv::Point q, cv::Scalar colour, const float scale = 0.2){
    /*
        cv::Mat& img        :需要绘制的图像
        cv::Point p         :绘制坐标轴线段的两个点之一:坐标轴的起点
        cv::Point q         :绘制坐标轴线段的两个点之二:坐标轴的终点
        cv::Scalar colour   :绘制的颜色
        const float scale   :坐标轴的放大因子
     */

    // 计算两个点与原点形成的向量的夹角(用于延长线段的计算)
    double angle = atan2( (double) p.y - q.y, (double) p.x - q.x );
    // 计算两个点之间距离(用于延长线段的计算)
    double hypotenuse = sqrt( (double) (p.y - q.y) * (p.y - q.y) + (p.x - q.x) * (p.x - q.x));

    // 把坐标轴按照放大因子scale延长,并绘制(p不动,按照原来pq的联线方向,增加线段长度)
    q.x = (int) (p.x - scale * hypotenuse * cos(angle));
    q.y = (int) (p.y - scale * hypotenuse * sin(angle));
    line(img, p, q, colour, 1, cv::LINE_AA);

    // 绘制坐标轴的箭头(以终点q为起点的两个小线段,与原来线段成45°夹角),箭头线段9像素
    int len = 9;
    // 箭头:第一个线段(逆时针旋转45°)
    p.x = (int) (q.x + len * cos(angle + CV_PI / 4));
    p.y = (int) (q.y + len * sin(angle + CV_PI / 4));
    line(img, p, q, colour, 1, cv::LINE_AA);
    // 箭头:第二个线段(顺时针旋转45°)
    p.x = (int) (q.x + len * cos(angle - CV_PI / 4));
    p.y = (int) (q.y + len * sin(angle - CV_PI / 4));
    line(img, p, q, colour, 1, cv::LINE_AA);
}

// 计算图像中PCA降维向量
double getOrientation(const std::vector<cv::Point> &pts, cv::Mat &img){
    /*
        std::vector<cv::Point> &pts     :数据集(从图像处理中获取)
        cv::Mat &img                    :显示数据集的图像
     */

    // 先把std::vector<cv::Point> &pts 转换为cv::Mat,这样便于操作
    int sz = static_cast<int>(pts.size());
    cv::Mat data_pts(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;
    }

    // 调用PCA降维
    cv::PCA pca(data_pts, cv::Mat(), cv::PCA::DATA_AS_ROW);  // 第二个参数为空,表示均值从第一个参数指定的数据计算。
    // 均值就是数据集的中心点
    cv::Point cntr = cv::Point(
        static_cast<int>(pca.mean.at<double>(0, 0)),    // 均值点第一个坐标
        static_cast<int>(pca.mean.at<double>(0, 1)));   // 均值点第二个坐标

    // 特征向量与特征值-因为是二维,所以是两个特征向量与特征值(特征值有可能是0)
    std::vector<cv::Point2d> eigen_vecs(2);     // 特征向量是二个特征
    std::vector<double> eigen_val(2);           // 特征值对应特征向量
    for (int i = 0; i < 2; i++){
        // 特征向量
        eigen_vecs[i] = cv::Point2d(
            pca.eigenvectors.at<double>(i, 0),
            pca.eigenvectors.at<double>(i, 1)
        );
        // 特征值
        eigen_val[i] = pca.eigenvalues.at<double>(i);
    }
    // 开始绘制
    // 1. 绘制中心点
    circle(img, cntr, 3, cv::Scalar(255, 0, 255), 2);
    // 2. 绘制第一个特征向量(主成分)
    // 计算绘制点
    cv::Point p1 = cntr + 0.02 * cv::Point(         // 像素太大,所以缩小到0.2
        static_cast<int>(eigen_vecs[0].x * eigen_val[0]), 
        static_cast<int>(eigen_vecs[0].y * eigen_val[0])
    );
    // 绘制
    drawAxis(img, cntr, p1, cv::Scalar(0, 255, 0), 1);   // 放大1倍(因为第一个主成分本身就很大,不再放大)

    // 3. 绘制第二个特征向量(主成分)
    cv::Point p2 = cntr - 0.02 * cv::Point(
        static_cast<int>(eigen_vecs[1].x * eigen_val[1]), 
        static_cast<int>(eigen_vecs[1].y * eigen_val[1])
    );
    drawAxis(img, cntr, p2, cv::Scalar(255, 255, 0), 5);  // 放大5倍(第二个特征向量太小,随意放大5倍)
    // 返回方向:第一个特征向量的方向
    double angle = atan2(eigen_vecs[0].y, eigen_vecs[0].x); 
    return angle;
}

cmake编译脚本

# 限制cmake的版本
cmake_minimum_required(VERSION 3.15)

# 设置C++标准的版本,OpenCV需要C++11支持
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 项目名
project(PCA)

# 加载OpenCV提供的cmake变量OpenCV_LIBS等,可以阅读OpenCV的cmake文件查阅其中的cmake变量清单
# set(CMAKE_PREFIX_PATH /usr/local)   
find_package(OpenCV REQUIRED)

# 设置编译链接的执行文件
add_executable(pca pca_img.cpp)
# 编译需要的动态库 或者 framework(Mac OS)
target_link_libraries(pca ${OpenCV_LIBS})

# #################第二个程序###########
add_executable(iris pca_iris.cpp)
target_link_libraries(iris ${OpenCV_LIBS})


相关文章

  • CV03-01:PCA应用

      PCA的算法应该是比较简单的,一个基于方差的投影降维方式,降维的维度采用方差最大的方式(方差大小反应样本的离散...

  • PCA算法推导

    PCA理解与应用。 Motivation PCA与Factor analysis非常相似,都是主要用于reduct...

  • 机器学习(5)PCA

    PCA简介 PCA(Principal Component Analysis):也是一个梯度分析的应用,不仅是机器...

  • PCA应用--二

    参考:PCA降维算法总结以及matlab实现PCA(个人的一点理解) 一、大致流程 PCA的应用范围:数据压缩或者...

  • PCA原理及实现-R

    PCA分析与解释 PCA是一种无参数的数据降维方法,常用的机器学习算法一种,这篇文章主要从PCA应用于解释两个方面...

  • 应用PCA的建议(Advice of applying PCA)

    降维可以减少硬盘存储空间、提高算法的速度,如果是降到2或3维,还可以可视化。 使用PCA降维去避免过拟合,这是一种...

  • PCA(主成分分析)

    PCA 参考资料 强大的矩阵奇异值分解(SVD)及其应用

  • 2019-01-23 PCA应用

    PCA 有时可以用来提高机器学习算法的速度 从数据组中x(1) 到 x(m) 中提取出输入向量,然后应用PCA会得...

  • 特征脸算法

    前言 特征脸算法使经典的人脸识别算法,特征脸算法使用了PCA方法。本文介绍了PCA算法和其应用特征脸算法 算法流程...

  • 主成份分析算法 PCA

    PCA 算法主要是把高维度的数据降为低维度数据。典型地应用包括数据压缩和数据可视化。本文介绍 PCA 算法及其典型...

网友评论

      本文标题:CV03-01:PCA应用

      本文链接:https://www.haomeiwen.com/subject/pylgwctx.html