[opencv][cpp] 学习手册2:透视变换

[opencv][cpp] 学习手册2:透视变换

13_透视变换.cpp
14_透视变换扭正.cpp


文章目录

  • [opencv][cpp] 学习手册2:透视变换
    • 13_透视变换.cpp
    • 14_透视变换扭正_案例.cpp
    • 15_使用 hough_line 手动拟合轮廓(todo)
    • 16_两直线交点的求解
    • &&_参考
    • &&_问题解决



13_透视变换.cpp

概念

仿射变换和透视变换更直观的叫法可以叫做「平面变换」和「空间变换」或者「二维坐标变换」和「三维坐标变换」。如果这么命名的话,其实很显然,这俩是一回事,只不过一个是二维坐标(x,y),一个是三维坐标(x,y,z)。
链接:图像处理的仿射变换与透视变换_知乎

仿射变换:
[opencv][cpp] 学习手册2:透视变换_第1张图片

透视变换:
[opencv][cpp] 学习手册2:透视变换_第2张图片

仿射变换的方程组有6个未知数,所以要求解就需要找到3组映射点,三个点刚好确定一个平面。
透视变换的方程组有8个未知数,所以要求解就需要找到4组映射点,四个点就刚好确定了一个三维空间。

需求
给定一张图像,已知这张图像上的一块区域的四点(source points),需要将这块区域转换到目标区域(target points),使用 opencv 的透视变换 api 求解变换矩阵,进行透视变换。

cv函数
[opencv][cpp] 学习手册2:透视变换_第3张图片
[opencv][cpp] 学习手册2:透视变换_第4张图片

流程

  1. 定义起始点
  2. 定义目标点
  3. 调用getPerspectiveTransform(srcPts, tarPts)求解变换矩阵 p_matrix
  4. 调用warpPerspective()进行仿射变换

在这个案例里面,原始图像的4个点是手动给定的,在更复杂的案例中,这4个点可以通过特征识别计算出,比如人脸区域,特征图案区域等。

代码

//
// Created by jacob on 12/29/20.
// enable cv-logging::https://stackoverflow.com/questions/54828885/how-to-enable-logging-for-opencv
//

#include 

#include 
#include 


using namespace std;
namespace cvlog = cv::utils::logging;


int main() {
     

    // 0. set logging
    cvlog::setLogLevel(cv::utils::logging::LOG_LEVEL_INFO);
    CV_LOG_INFO(NULL, "perspective transform")

    // 1. read src image
    string fileName = "../img/clock.jpg";
    cv::Mat src = cv::imread(fileName, cv::IMREAD_COLOR);

    // 2. do operation: perspective transformation
    std::vector<cv::Point2f> srcPts = {
     
            cv::Point2f(66, 61), cv::Point2f(70, 343),      // u-l, b-l
            cv::Point2f(379, 440), cv::Point2f(355, 165)    // b-r, u-r
    };

    std::vector<cv::Point2f> tarPts = {
     
            cv::Point2f(0, 0), cv::Point2f(0, 640),
            cv::Point2f(480, 640), cv::Point2f(480, 0)
    };

    cv::Mat pM = cv::getPerspectiveTransform(srcPts, tarPts);
    CV_LOG_INFO(NULL, "p_matrix:\n" << pM)

    cv::Mat dst;
    cv::warpPerspective(src, dst, pM, cv::Size(480, 640));

    // display & wait key press
    cv::imshow("src", src);
    cv::imshow("dst", dst);

    cv::waitKey(0);
    cv::destroyAllWindows();

    return 0;
}

运行结果

/home/jacob/CVWS/studyopencv002/cmake-build-debug/13_perspective_transform
[ INFO:0] perspective transform
[ INFO:0] p_matrix:
[1.588971099903391, -0.02253859716175018, -103.497238166757;
 -0.8805138353460487, 2.446812484759694, -91.14164843750211;
 -0.0002586959381707657, 0.0002645255256509643, 1]


14_透视变换扭正_案例.cpp

代码

//
// Created by jacob on 12/29/20.
// enable cv-logging::https://stackoverflow.com/questions/54828885/how-to-enable-logging-for-opencv
// opencv 四边形拟合_谈谈OpenCV中的四边形:https://blog.csdn.net/weixin_39540271/article/details/111284004
//

#include 

#include 
#include 


using namespace std;
namespace cvlog = cv::utils::logging;


int main() {
     

    /*
     * 0. set cv log
     */
    cvlog::setLogLevel(cv::utils::logging::LOG_LEVEL_INFO);
    CV_LOG_INFO(NULL, "perspective transform")


    /*
     * 1. read src image
     */
    string fileName = "../img/book.jpg";
    cv::Mat src = cv::imread(fileName, cv::IMREAD_COLOR);
    CV_LOG_INFO(NULL, "src size: " << src.size << ", src rows: " << src.rows << ", src cols: " << src.cols)
    cv::resize(src, src, cv::Size(int(src.cols / 2), int(src.rows / 2)));
    cv::imshow("src", src);

    
    /*
     * 2. do operation
     */
    // 1. 彩色图像转化成灰色图像: dst_gray
    cv::Mat dst_gray;
    cv::cvtColor(src, dst_gray, cv::COLOR_BGR2GRAY);
    cv::imshow("dst_gray", dst_gray);

    // 2. 转成二值图: dst_bin
    cv::Mat dst_bin;
    cv::threshold(dst_gray, dst_bin, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_TRIANGLE);
    cv::imshow("dst_bin", dst_bin);

    // 3. 找出轮廓
    cv::Mat dst_pre;
    dst_pre = dst_bin.clone();
    cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
    cv::dilate(dst_bin, dst_pre, kernel, cv::Point(-1, -1), 1);
    cv::imshow("dst_pre", dst_pre);

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(dst_pre, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
    CV_LOG_INFO(NULL, "contours: " << contours.data() << ", contours.size(): " << contours.size())

    double maxArea = 0;
    int maxIndex = 0;
    for (int i = 0; i < contours.size(); ++i) {
     
        std::vector<cv::Point> contour = contours[i];
        double area = cv::contourArea(contour);
        CV_LOG_INFO(NULL, "contour[" << i << "] area:: " << area)
        if (maxArea <= area) {
     
            maxArea = area;
            maxIndex = i;
        }
    }
    CV_LOG_INFO(NULL, "max area: " << maxArea)
    CV_LOG_INFO(NULL, "max contours: " << contours[maxIndex] << ", len: " << contours[maxIndex].size())
    cv::drawContours(src, contours, maxIndex, cv::Scalar(0, 0, 255), 2, cv::LINE_AA);

    cv::Mat ployM = cv::Mat::zeros(src.size(), src.type());
    std::vector<cv::Point> contourOut;
    std::vector<std::vector<cv::Point>> contourOuts;
    double contourLen = cv::arcLength(contours[maxIndex], true);
    cv::approxPolyDP(contours[maxIndex], contourOut, 0.02 * contourLen, true);
    CV_LOG_INFO(NULL, "contourOut.size(): " << contourOut.size() << ", contourOut: " << contourOut)
    contourOuts.push_back(contourOut);
    if (contourOuts[0].size() == 4) {
     
        CV_LOG_INFO(NULL, "contourOuts[0].size: " << contourOuts[0].size())
        cv::drawContours(ployM, contourOuts, -1, cv::Scalar(255, 0, 0), 2, cv::LINE_AA);
    }
    cv::imshow("ployM", ployM);

    // 4. 找到边缘的交点
    cv::Point2f s_u_r = contourOut[0];
    cv::Point2f s_u_l = contourOut[1];
    cv::Point2f s_b_l = contourOut[2];
    cv::Point2f s_b_r = contourOut[3];
    std::vector<cv::Point2f> sourcePts = {
     s_u_r, s_u_l, s_b_l, s_b_r};

    cv::Point2f t_u_r = cv::Point2f(src.cols, 0);
    cv::Point2f t_u_l = cv::Point2f(0, 0);
    cv::Point2f t_b_l = cv::Point2f(0, src.rows);
    cv::Point2f t_b_r = cv::Point2f(src.cols, src.rows);
    std::vector<cv::Point2f> targetPts = {
     t_u_r, t_u_l, t_b_l, t_b_r};

    // 5. 透视变换
    const cv::Mat &pMatrix = cv::getPerspectiveTransform(sourcePts, targetPts);
    CV_LOG_INFO(NULL, "pMatrix" << pMatrix)
    cv::Mat pDst;
    cv::warpPerspective(src, pDst, pMatrix, cv::Size(src.cols, src.rows));
    cv::imshow("pDst", pDst);


    /*
     * display &wait key press
     */
    cv::waitKey(0);
    cv::destroyAllWindows();

    return 0;
}


运行结果
在这里插入图片描述

代码说明

  1. 主函数流程:main
    1. 读取彩色图像
    2. 进行图像变换操作
    3. 等待按键
  1. 图像变换操作函数:doTransform()
    1. 对图像进行预处理

      1. 将图像转换成灰度图
      2. 将灰度图转换成二值图,可以使用cv::threshold()cv::adaptiveThreshold()函数
      3. 对二值图进行膨胀操作(1. 定义卷积矩阵,2. 膨胀图像)
        cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
        cv::dilate(dst_bin, dst_pre, kernel, cv::Point(-1, -1), 1);
        
    2. 找出图像中的轮廓:cv::findContours()

      cv::findContours(dst_pre, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
      
    3. 在找到的轮廓中,挑选出最大轮廓,并返回最大轮廓索引:int maxIndex = getMaxContour(src, contours);

      int getMaxContour(const cv::Mat &src, const vector<vector<cv::Point>> &contours) {
               
      	double maxArea = 0;
      	int maxIndex = 0;
      	
      	for (int i = 0; i < contours.size(); ++i) {
               
      		vector<cv::Point> contour = contours[i];
      		double area = cv::contourArea(contour);
      		CV_LOG_INFO(NULL, "contour[" << i << "] area:: " << area)
      		if (maxArea <= area) {
               
          		maxArea = area;
          		maxIndex = i;
      		}
      	}
      	CV_LOG_INFO(NULL, "max area: " << maxArea)
      	CV_LOG_INFO(NULL, "max contours: " << contours[maxIndex] << ", len: " << contours[maxIndex].size())
      	cv::drawContours(src, contours, maxIndex, cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
      	return maxIndex;
      }
      

      使用 cv::contourArea 计算轮廓向量中的每一个轮廓的面积,取最大面积,并返回最大面积的索引 maxIndex。绘制轮廓向量中的最大轮廓,使用最大索引 maxIndex。

    4. 对最大轮廓进行四边形拟合,得到四个拟合点:quadFitContours()

      vector<cv::Point> quadFitContours(const cv::Mat &src, const vector<vector<cv::Point>> &contours, int maxIndex) {
               
      	
      	cv::Mat ployM = cv::Mat::zeros(src.size(), src.type());
      	vector<cv::Point> contourOut;
      	vector<vector<cv::Point>> contourOuts;
      	
      	double contourLen = cv::arcLength(contours[maxIndex], true);
      	cv::approxPolyDP(contours[maxIndex], contourOut, 0.02 * contourLen, true);
      	CV_LOG_INFO(NULL, "contourOut.size(): " << contourOut.size() << ", contourOut: " << contourOut)
      	contourOuts.push_back(contourOut);
      	if (contourOuts[0].size() == 4) {
               
      		CV_LOG_INFO(NULL, "contourOuts[0].size: " << contourOuts[0].size())
      		cv::drawContours(ployM, contourOuts, -1, cv::Scalar(255, 0, 0), 2, cv::LINE_AA);
      	}
      	cv::imshow("ployM", ployM);
      	return contourOut;
      }
      

      使用 arcLength 求出目标轮廓的周长,再使用 approxPolyDP 对轮廓进行多边形拟合(不一定是四边形,当前案例可以用四边形拟合,还需要思考如何手动进行四边形拟合),当拟合点为四个的时候,绘制拟合多边形(区别轮廓)。周长的用处是确定拟合精度。

    5. 进行透视变换操作

      void doPerspectiveT(const cv::Mat &src, const vector<cv::Point> &contourOut) {
               
      	cv::Point2f s_u_r = contourOut[0];
      	cv::Point2f s_u_l = contourOut[1];
      	cv::Point2f s_b_l = contourOut[2];
      	cv::Point2f s_b_r = contourOut[3];
      	vector<cv::Point2f> sourcePts = {
               s_u_r, s_u_l, s_b_l, s_b_r};
      
      	cv::Point2f t_u_r = cv::Point2f(src.cols, 0);
      	cv::Point2f t_u_l = cv::Point2f(0, 0);
      	cv::Point2f t_b_l = cv::Point2f(0, src.rows);
      	cv::Point2f t_b_r = cv::Point2f(src.cols, src.rows);
      	vector<cv::Point2f> targetPts = {
               t_u_r, t_u_l, t_b_l, t_b_r};
      
      	// 5. 透视变换
      	const cv::Mat &pMatrix = cv::getPerspectiveTransform(sourcePts, targetPts);
      	CV_LOG_INFO(NULL, "pMatrix" << pMatrix)
      	cv::Mat pDst;
      	cv::warpPerspective(src, pDst, pMatrix, cv::Size(src.cols, src.rows));
      	cv::imshow("pDst", pDst);
      }
      

      通过拟合四点确定透视变换的原始点集,以及确定目标点集,使用 cv::getPerspectiveTransform 计算透视变换矩阵,然后通过 cv::warpPerspective 函数将原始图像四拟合点区域的图像透视变换到新的图像中。

需要弄清各个函数的参数返回值的数据结构,以及各个数据结构的操作函数。


15_使用 hough_line 手动拟合轮廓(todo)

意向

  1. 使用 cv 的 api HoughLines 与 HoughLinesP 拟合轮廓(涉及到调参)
  2. 再计算直线的交点,从而得出透视变换的原始点。

测试代码

//
// Created by jacob on 12/29/20.
//

#include 

#include 
#include 


using namespace std;
namespace cvlog = cv::utils::logging;



void doHoughLines(const cv::Mat &imgM);

void doHoughLinesP(const cv::Mat &imgM);



int main(int argc, char **argv) {
     

    // setting up cv log
    cvlog::setLogLevel(cvlog::LOG_LEVEL_INFO);
    CV_LOG_INFO(NULL, "perspective_T_hough_line_test.cpp")

    // read img
    string filename = "../img/imgM.jpg";
    cv::Mat imgM = cv::imread(filename, cv::IMREAD_COLOR);
    cv::imshow("imgM", imgM);

    // do operation
    doHoughLines(imgM);


    cv::waitKey(0);
    cv::destroyAllWindows();

    return 0;
}


void doHoughLines(const cv::Mat &imgM) {
     
    cv::Mat rst_s, rst_s_c3;
    cv::cvtColor(imgM, rst_s, cv::COLOR_BGR2GRAY);
    cv::cvtColor(rst_s, rst_s_c3, cv::COLOR_GRAY2BGR);

    // use standard hough lines :: houghlines
    vector<cv::Vec2f> lines; // will hold the results of the detection
    HoughLines(rst_s, lines, 1, CV_PI / 180, 20, 0, 0); // runs the actual detection

    int lineSize = lines.size();
    int threshold = 0;
    for (int i = 0; i < 500; ++i) {
     
        HoughLines(rst_s, lines, 1, CV_PI / 180, i, 0, 0); // runs the actual detection
        if (lineSize > lines.size()){
     
            lineSize = lines.size();
            threshold = i;
        }
    }
    CV_LOG_INFO(NULL, "lines:: " << lines.size() << ", min_lines_size:: " << lineSize)
    CV_LOG_INFO(NULL, "threshold:: " << threshold)

    // Draw the lines
    for (auto &line : lines) {
     
        float rho = line[0], theta = line[1];
        cv::Point pt1, pt2;
        double a = cos(theta), b = sin(theta);
        double x0 = a * rho, y0 = b * rho;
        pt1.x = cvRound(x0 + 1000 * (-b));
        pt1.y = cvRound(y0 + 1000 * (a));
        pt2.x = cvRound(x0 - 1000 * (-b));
        pt2.y = cvRound(y0 - 1000 * (a));
        cv::line(rst_s_c3, pt1, pt2, cv::Scalar(0, 0, 255), 1, cv::LINE_AA);
    }
    cv::imshow("rst_s_c3", rst_s_c3);

    // calculate four intersection points

}

void doHoughLinesP(const cv::Mat &imgM) {
     
    cv::Mat rst_p, rst_p_c3;
    cv::cvtColor(imgM, rst_p, cv::COLOR_BGR2GRAY);
    cv::cvtColor(rst_p, rst_p_c3, cv::COLOR_GRAY2BGR);

    vector<cv::Vec4i> linesP; // will hold the results of the detection
    HoughLinesP(rst_p, linesP, 1, CV_PI / 180, 250, 250, 2); 	// runs the actual detection
    CV_LOG_INFO(NULL, "linesP:: " << linesP.size()); 			// (x1, y1, x2, y2) 线段的两个端点坐标

    // Draw the lines
    for (auto l : linesP) {
     
        line(rst_p_c3, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), cv::Scalar(0, 0, 255), 1, cv::LINE_AA);
    }
    cv::imshow("rst_p_c3", rst_p_c3);

    // calculate four intersection points

}

需要参考的手册
[opencv][cpp] 学习手册2:透视变换_第5张图片
[opencv][cpp] 学习手册2:透视变换_第6张图片

相较于 cv::approxPolyDP 函数的多边形拟合,霍夫直线在此案例中理论上可以拟合所需要的四边形,但是调参很麻烦。


16_两直线交点的求解

未验证代码片段

calcPoint

Point2f calcPoint(Point2f kb1,Point2f kb2){
     
    double k1 = kb1.x;
    double b1 = kb1.y;

    double k2 = kb2.x;
    double b2 = kb2.y;

    double x = (b1-b2)/(k2-k1);
    double y = k1*x + b1;

    return Point2f(x,y);
}

main.cpp

    // [ 【Point】 ]
    vector<Vec4i> lines;
    HoughLinesP(maxGrayImg,lines,1,CV_PI/180,118,210,5);

    vector<Point2f> kbs;
    kbs.push_back(Point2f(0,0));
    cout<<lines.size()<<endl;
    for (int i = 1; i <= 4; ++i) {
     
        Vec4i lineResult = lines[i];
        Point pt1(lineResult[0],lineResult[1]);
        Point pt2(lineResult[2],lineResult[3]);

        //line(src,pt1,pt2,colors[i],3);

        double k = (lineResult[3] -lineResult[1] )/(lineResult[2]-lineResult[0]);

        double b = lineResult[1] -  lineResult[0]*k;

        Point2f kb(k,b);

        kbs.push_back(kb);
    }
    // 左上角 3,4
    Point2f top_left = calcPoint(kbs[3],kbs[4]);
    // 右上角 3,2
    Point2f top_right = calcPoint(kbs[3],kbs[2]);
    // 左下角 1,4
    Point2f bottom_left = calcPoint(kbs[1],kbs[4]);
    // 右下角 1,2
    Point2f bottom_right = calcPoint(kbs[1],kbs[2]);

理解

  1. HoughLinesP 函数返回的直线类型 vect,其为:(x1, y1, x2, y2) ,是线段的两个端点坐标。

  2. 对于每一个线段(以两个端点表示(x1, y1),(x2, y2)),端点坐标已知,可以求的线段的斜率 k 以及偏移量 b:

    1. 先求解 k,利用(x1, y1),(x2, y2):k = (y2 - y1) / (x2 - x1)
    2. 在通过得出的 k,(x1, y1)或(x2, y2)求解 b:b = y1 - k*x1
      double k = (lineResult[3] - lineResult[1] )/(lineResult[2]-lineResult[0]);
      double b = lineResult[1] - lineResult[0]*k;
      
    3. 则当前直线方程可以得出,k,b 已知。
  3. 计算两条直线的交点,两直线分别以不同的斜率、偏移量坐标表示 cv::Point2f kb(k, b)

    Point2f kb(k,b);
    kbs.push_back(kb);
    
    Point2f calcPoint(Point2f kb1,Point2f kb2);
    

    假设求解直线 y = k1x + b1 与 y = k2x + b2 的交点(x,y)
    可以先求出交点 x 坐标:x = (b2 - b1)/(k1 - k2)
    然后再通过 x,k1,b1(或 k2,b2)求解出 y 坐标:y = k1*x + b1
    返回(x,y)


&&_参考

链接:四边形拟合_谈谈OpenCV中的四边形


&&_问题解决

你可能感兴趣的:(opencv)