C++ OpenCV透视变换改进---直线拟合的应用

学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为4379,预计阅读11分钟

前言

前一篇《C++ OpenCV透视变换综合练习》中针对透视变换做了一个小练习,上篇中我们用多边形拟合的点集来计算离最小旋转矩形最近的点来定义为透视变换的点,效果是有,无意间又想了一个新的思路,在原来的点的基础上效果会更好一点,其中就用到了直线拟合的方法,今天这篇就说一下优化的思路及直线拟合的函数。

format,png

实现效果

format,png

format,png

放大图

直线拟合函数

format,png

微卡智享

  void cv::fitLine(
    cv::InputArray points, // 二维点的数组或vector
    cv::OutputArray line, // 输出直线,Vec4f (2d)或Vec6f (3d)的vector
    int distType, // 距离类型,要使输入点到拟合直线的距离和最小化
    double param, // 距离参数,一般设为0
    double reps, // 径向的精度参数,通常情况下两个值均被设定为1e-2
    double aeps // 角度精度参数
  );

参数说明:

  • points: 用于拟合直线的输入点集,可以是二维点的cv::Mat数组,也可以是二维点的STL vector。

  • line: 输出的直线,对于二维直线而言类型为cv::Vec4f,对于三维直线类型则是cv::Vec6f,输出参数的前半部分给出的是直线的方向,而后半部分给出的是直线上的一点(即通常所说的点斜式直线)。

  • distType: 距离类型,拟合直线时,要使输入点到拟合直线的距离和最小化(即下面公式中的cost最小化),可供选的距离类型如下表所示,ri表示的是输入的点到直线的距离。

CV_DIST_USER =-1, /* User defined distance */
CV_DIST_L1 =1, /* distance = |x1-x2| + |y1-y2| */
CV_DIST_L2 =2, /* the simple euclidean distance */
CV_DIST_C =3, /* distance = max(|x1-x2|,|y1-y2|) */
CV_DIST_L12 =4, /* L1-L2 metric: distance = 2(sqrt(1+x*x/2) - 1)) */
CV_DIST_FAIR =5, /* distance = c^2(|x|/c-log(1+|x|/c)), c = 1.3998 */
CV_DIST_WELSCH =6, /* distance = c^2/2(1-exp(-(x/c)^2)), c = 2.9846 */
CV_DIST_HUBER =7 /* distance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345 */
  • param:距离参数,跟所选的距离类型有关,值可以设置为0,cv::fitLine()函数本身会自动选择最优化的值。

  • reps:拟合直线所需要的径向精度,一般设置为0.01或1e-2。

  • aeps:拟合直线所需要的角度精度,一般设置为0.01或1e-2。

实现思路

format,png

微卡智享

 

#步骤
1旋转矩形的点和上一步获取的最近点设置一个阈值距离,在距离内的都列入当前区域的直线拟合点,超过阈值的用最近点加上阈值重新算为计算点来进行拟合
2根据不同区域计算直线拟合
3求到的直线拟合点实现每两条求交点
4得到的4个交点做为透视变换的坐标点

 

01

阈值范围内的直线拟合

format,png

先以左边区域为例,首先我们设定了一个距离为15的阈值,白色的是我们上一篇中求到的最近的点(点1和2),蓝色为最小旋转矩形的角点(点3和4),我们通过计算点1到点3的距离,还有点2到点4的距离都小于15,那我们把这4个点都列入到左边区域,用来计算直接拟合。

format,png紫色线即为上面4个点采用直线拟合后的结果

左边的区域拟合直线,因为都在阈值内,所以拟合出的直线比原来只求最近点连起来的效果要更好一点。接下来我们看看超过阈值的处理。

02

超出阈值的直线拟合

format,png

上图中可以看到,右下的区域点在阈值范围内是无问题了,右上的旋转矩形角点(点4)与最近点(点2)距离挺远,肯定超出阈值了,如果还把点4也加入到拟合点计算的话,直线会多出来不少,所以我们就在根据(点2)的坐标,在X轴和Y轴都加上阈值的范围,计算出新的拟合点,即上图红圈标识的,用点1,点2,点3和红色拟合点来进行直线拟合,得到的效果如下:

format,png

03

每两条直线拟合求交点

format,png

直线拟合的函数,输出的参数line里面有说到了是Vec4f的类型,输出参数的前半部分给出的是直线的方向,而后半部分给出的是直线上的一点(即通常所说的点斜式直线)。

方程式:y-y1=k(x-x1)

其中(x1,y1)为坐标系上过直线的一点的坐标,k为该直线的斜率。

推导:若直线L1经过点P1(x1,y1),且斜率为k,求L1方程。

设点P(x,y)是直线上不同于点P1的任意一点,直线PP1的斜率应等于直线L1的斜率,根据经过两点的直线的斜率公式得k=(y-y1)/(x-x1) (且:x≠x1)

所以,直线L1:y-y1=k(x-x1)

说明:

(1)这个方程是由直线上一点和斜率确定的,这一点必须在直线上,否则点斜式方程不成立;

(2)当直线l的倾斜角为0°时,直线方程为y=y1;

(3)当直线倾斜角为90°时,直线没有斜率,它的方程不能用点斜式表示,这时直线方程为x=x1。

我们直线拟合的得到的4个Vec4f就需要每两个求交点最后得到上图中红圈的1,2,3,4的4个交点。根据斜率和点的计算

kk1*x+b1=kk2*x+b2

可以得到:x=(b2-b1)/(kk1-kk2) 

进而得到:y=(kk1*(b2-b1)/(kk1-kk2))+b1

核心代码

format,png

微卡智享

重新计算区域及直线拟合的函数

定义的默认阈值是15。

//采用重新定义点做直线拟合后找到的对应点
void GetPointsFromFitline(Point2f newPoints[], Point2f vetPoints[], Point2f rectPoints[], float dist = 15.0f);
//重新计算距离变换的4个坐标点
//思路:旋转矩形的点和上一步获取的临近点判断距离,如果小于阈值都列入,大于阈值按最近距离的阈值处理
void GetPointsFromFitline(Point2f newPoints[], Point2f vetPoints[], Point2f rectPoints[], float dist)
{
  //1.重新规划区域点
  float curdist = CalcPointDistance(rectPoints[0], vetPoints[0]);
  newPoints[0] = curdist <= dist ? rectPoints[0] : vetPoints[0] + Point2f(-dist, -dist);


  curdist = CalcPointDistance(rectPoints[1], vetPoints[1]);
  newPoints[1] = curdist <= dist ? rectPoints[1] : vetPoints[1] + Point2f(dist, -dist);


  curdist = CalcPointDistance(rectPoints[2], vetPoints[2]);
  newPoints[2] = curdist <= dist ? rectPoints[2] : vetPoints[2] + Point2f(dist, dist);


  curdist = CalcPointDistance(rectPoints[3], vetPoints[3]);
  newPoints[3] = curdist <= dist ? rectPoints[3] : vetPoints[3] + Point2f(-dist, dist);


    //左侧区域点为最小旋转矩形的左上左下和离的最近的左上左下组成
  vector<Point2f> lArea;
  lArea.push_back(newPoints[0]);
  lArea.push_back(vetPoints[0]);
  lArea.push_back(vetPoints[3]);
  lArea.push_back(newPoints[3]);


  //顶部区域点为最小外接矩形左上右上和最近点的左上右上组成
  vector<Point2f> tArea;
  tArea.push_back(newPoints[0]);
  tArea.push_back(newPoints[1]);
  tArea.push_back(vetPoints[1]);
  tArea.push_back(vetPoints[0]);


  //右侧区域点为最近点的右上右下和最小外接矩形的右上和右下组成
  vector<Point2f> rArea;
  rArea.push_back(vetPoints[1]);
  rArea.push_back(newPoints[1]);
  rArea.push_back(newPoints[2]);
  rArea.push_back(vetPoints[2]);


  //底部区域点为最近点的左下右下和最小外接矩形的左下右下组成
  vector<Point2f> bArea;
  bArea.push_back(vetPoints[3]);
  bArea.push_back(vetPoints[2]);
  bArea.push_back(newPoints[2]);
  bArea.push_back(newPoints[3]);


  //2.根据区域做直线拟合
  //左边区域
  Vec4f lLine;
  fitLine(lArea, lLine, DIST_L2, 0, 0.01, 0.01);
  //顶部区域
  Vec4f tLine;
  fitLine(tArea, tLine, DIST_L2, 0, 0.01, 0.01);
  //右边区域
  Vec4f rLine;
  fitLine(rArea, rLine, DIST_L2, 0, 0.01, 0.01);
  //顶部区域
  Vec4f bLine;
  fitLine(bArea, bLine, DIST_L2, 0, 0.01, 0.01);


  //3.根据直线拟合的求每两条直线的交叉点为我们的多边形顶点
    //左上顶点
  newPoints[0] = GetCrossPoint(lLine, tLine);
  //右上顶点
  newPoints[1] = GetCrossPoint(tLine, rLine);
  //右下顶点
  newPoints[2] = GetCrossPoint(rLine, bLine);
  //左下顶点
  newPoints[3] = GetCrossPoint(bLine, lLine);
}


根据两点及斜率求交点

//求两条直线的交叉点
Point2f GetCrossPoint(Vec4f Line1, Vec4f Line2)
{
  double k1, k2;
  //Line1的斜率
  k1 = Line1[1] / Line1[0];
  //Line2的斜率
  k2 = Line2[1] / Line2[0];


  Point2f crossPoint;
  crossPoint.x = (k1 * Line1[2] - Line1[3] - k2 * Line2[2] + Line2[3]) / (k1 - k2);
  crossPoint.y = (k1 * k2 * (Line1[2] - Line2[2]) + k1 * Line2[3] - k2 * Line1[3]) / (k1 - k2);
  return crossPoint;
}

透视变换的新坐标代码

上一篇中透视变换的新坐标我们直接是用的最小外接矩形的4个点,不过个别图中会矩形特别大,整个透视变换后的拉伸有点太夸张了,所以这里我们改了方法,先求出最小旋转矩形中最左和最上的坐标,然后计算出最小旋转矩形的长和高,来定义一个新的矩形进行透视变换。

        //根据最小矩形和多边形拟合的最大四个点计算透视变换矩阵    
        Point2f rectPoint[4];
        //计算旋转矩形的宽和高
        float rWidth = CalcPointDistance(vertices[0], vertices[1]);
        float rHeight = CalcPointDistance(vertices[1], vertices[2]);
        //计算透视变换的左上角起始点
        float left = gray.cols;
        float top = gray.rows;
        for (int i = 0; i < 4; ++i) {
          if (left > newPoints[i].x) left = newPoints[i].x;
          if (top > newPoints[i].y) top = newPoints[i].y;
        }


        rectPoint[0] = Point2f(left, top);
        rectPoint[1] = rectPoint[0] + Point2f(rWidth, 0);
        rectPoint[2] = rectPoint[1] + Point2f(0, rHeight);
        rectPoint[3] = rectPoint[0] + Point2f(0, rHeight);

format,png

format,png

扫描二维码

获取更多精彩

微卡智享

format,png

「 往期文章 」

.net5发布在Windows2008的几个注意事项

C++ OpenCV透视变换综合练习

.Net5 Windows Form App中Linq的分组查询使用

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++ OpenCV中的透视变换是一种图像处理技术,它可以将图像从一个视角转换到另一个视角,使得图像在新的视角下呈现出透视效果。透视变换计算机视觉和图像处理中广泛应用,例如校正图像畸变、图像矫正、场景重建等。 在OpenCV中,透视变换可以通过以下步骤实现: 1. 定义源图像的四个角点和目标图像的四个对应角点。 2. 使用`cv::getPerspectiveTransform()`函数计算透视变换矩阵,该矩阵将源图像的四个角点映射到目标图像的四个对应角点。 3. 使用`cv::warpPerspective()`函数将源图像应用透视变换矩阵,得到目标图像。 下面是一个示例代码,展示了如何使用C++ OpenCV进行透视变换: ```cpp #include <opencv2/opencv.hpp> int main() { // 读取源图像 cv::Mat srcImage = cv::imread("source.jpg"); // 定义源图像的四个角点和目标图像的四个对应角点 std::vector<cv::Point2f> srcPoints, dstPoints; srcPoints.push_back(cv::Point2f(0, 0)); srcPoints.push_back(cv::Point2f(srcImage.cols - 1, 0)); srcPoints.push_back(cv::Point2f(srcImage.cols - 1, srcImage.rows - 1)); srcPoints.push_back(cv::Point2f(0, srcImage.rows - 1)); dstPoints.push_back(cv::Point2f(100, 100)); dstPoints.push_back(cv::Point2f(srcImage.cols - 101, 100)); dstPoints.push_back(cv::Point2f(srcImage.cols - 101, srcImage.rows - 101)); dstPoints.push_back(cv::Point2f(100, srcImage.rows - 101)); // 计算透视变换矩阵 cv::Mat perspectiveMatrix = cv::getPerspectiveTransform(srcPoints, dstPoints); // 应用透视变换 cv::Mat dstImage; cv::warpPerspective(srcImage, dstImage, perspectiveMatrix, srcImage.size()); // 显示结果图像 cv::imshow("Source Image", srcImage); cv::imshow("Destination Image", dstImage); cv::waitKey(0); return 0; } ``` 这段代码中,我们首先读取了源图像,然后定义了源图像的四个角点和目标图像的四个对应角点。接下来,使用`cv::getPerspectiveTransform()`函数计算透视变换矩阵,并使用`cv::warpPerspective()`函数将源图像应用透视变换矩阵,得到目标图像。最后,我们显示了源图像和目标图像。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vaccae

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值