OpenCV图像处理--平面单应性

Posted C君莫笑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenCV图像处理--平面单应性相关的知识,希望对你有一定的参考价值。

图像几何变化

投影变换
投影变换 (Projective Transformation),是仿射变换的一般化,二者区别如下:

仿射变换

  1. 用途
    旋转 (线性变换),平移(向量加).缩放(线性变换),错切,反转

  2. 方法
    仿射变换是一种二维坐标到二维坐标之间的线性变换,它保持了二维图形的“平直性”(直线经过变换之后依然是直线)和“平行性”(二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。任意的仿射变换都能表示为乘以一个矩阵(线性变换),再加上一个向量 (平移) 的形式.

  3. 具体应用
    给出变换前的ABCD和变换后的A’B’C’D’

    透视变换(投影变换)

  4. 用途
    将2D矩阵图像变换成3D的空间显示效果,全景拼接.

  5. 方法
    透视变换是将图片投影到一个新的视平面,也称作投影映射.它是二维(x,y)到三维(X,Y,Z),再到另一个二维(x’,y’)空间的映射.相对于仿射变换,它提供了更大的灵活性,将一个四边形区域映射到另一个四边形区域(不一定是平行四边形).它不止是线性变换.但也是通过矩阵乘法实现的,使用的是一个3x3的矩阵,矩阵的前两行与仿射矩阵相同(m11,m12,m13,m21,m22,m23),也实现了线性变换和平移,第三行用于实现透视变换

    以上公式设变换之前的点是z值为1的点,它三维平面上的值是x,y,1,在二维平面上的投影是x,y,通过矩阵变换成三维中的点X,Y,Z,再通过除以三维中Z轴的值,转换成二维中的点x’,y’.从以上公式可知,仿射变换是透视变换的一种特殊情况.它把二维转到三维,变换后,再转映射回之前的二维空间(而不是另一个二维空间).

  6. 具体应用
    在OpenCV中,透视变换通过函数cvWrapPerspective(src,dst,mat)实现, 与仿射变换不同的是,透视矩阵是一个3x3的矩阵,在计算矩阵时,可利用函数cvGetPerspectiveTransform(srcQuad,dstQuad,mat),由于不再是平行四边形,需要提供四边形的四个顶点

    区别
    仿射变换后平行四边形的各边仍操持平行,透视变换结果允许是梯形等四边形,所以仿射变换是透视变换的子集
    具体案例:
    仿射变换和透视变换的数学原理不需深究,其计算方法为坐标向量和变换矩阵的乘积,换言之就是矩阵运算。在应用层面,仿射变换是图像基于3个固定顶点的变换,如图所示:

    图中红点即为固定顶点,在变换先后固定顶点的像素值不变,图像整体则根据变换规则进行变换
    同理,透视变换是图像基于4个固定顶点的变换,如图所示:

    在OpenCV中,仿射变换和透视变换均有封装好的函数,分别为

void warpAffine(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags=INTER_LINEAR, int borderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())

void warpPerspective(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags=INTER_LINEAR, int borderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())

两种变换函数形式完全相同,因此以仿射变换函数为例:
1、参数InputArray src:输入变换前图像
2、参数OutputArray dst:输出变换后图像,需要初始化一个空矩阵用来保存结果,不用设定矩阵尺寸
3、参数InputArray M:变换矩阵,用另一个函数getAffineTransform()计算
4、参数Size dsize:设置输出图像大小
5、参数int flags=INTER_LINEAR:设置插值方式,默认方式为线性插值
后两个参数不常用,在此不赘述
关于生成变换矩阵InputArray M的函数getAffineTransform():

Mat getAffineTransform(const Point2f* src, const Point2f* dst)
  • 参数const Point2f* src:原图的三个固定顶点

  • 参数const Point2f* dst:目标图像的三个固定顶点

  • 返回值:Mat型变换矩阵,可直接用于warpAffine()函数

注意,顶点数组长度超过3个,则会自动以前3个为变换顶点;数组可用Point2f[]或Point2f*表示
示例代码如下:

//读取原图
	Mat I = imread("img.jpg");
	//设置空矩阵用于保存目标图像
	Mat dst;
	//设置原图变换顶点
	Point2f AffinePoints0[3] = { Point2f(100, 50), Point2f(100, 390), Point2f(600, 50) };
	//设置目标图像变换顶点
	Point2f AffinePoints1[3] = { Point2f(200, 100), Point2f(200, 330), Point2f(500, 50) };
	//计算变换矩阵
	Mat Trans = getAffineTransform(AffinePoints0, AffinePoints1);
	//矩阵仿射变换
	warpAffine(I, dst, Trans, Size(I.cols, I.rows));
	//分别显示变换先后图像进行对比
	imshow("src", I);
	imshow("dst", dst);
	waitKey();

同理,透视变换与仿射变换函数类似:

void warpPerspective(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags=INTER_LINEAR, int borderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())

生成变换矩阵函数为:

Mat getPerspectiveTransform(const Point2f* src, const Point2f* dst)

注意,透视变换顶点为4个

两种变换完整代码及结果比较:

#include <iostream>
#include <opencv.hpp>
using namespace std;
using namespace cv;

Mat AffineTrans(Mat src, Point2f* scrPoints, Point2f* dstPoints)
{
	Mat dst;
	Mat Trans = getAffineTransform(scrPoints, dstPoints);
	warpAffine(src, dst, Trans, Size(src.cols, src.rows), CV_INTER_CUBIC);
	return dst;
}

Mat PerspectiveTrans(Mat src, Point2f* scrPoints, Point2f* dstPoints)
{
	Mat dst;
	Mat Trans = getPerspectiveTransform(scrPoints, dstPoints);
	warpPerspective(src, dst, Trans, Size(src.cols, src.rows), CV_INTER_CUBIC);
	return dst;
}

void main()
{
	Mat I = imread("..//img.jpg");	//700*438

	Point2f AffinePoints0[4] = { Point2f(100, 50), Point2f(100, 390), Point2f(600, 50), Point2f(600, 390) };
	Point2f AffinePoints1[4] = { Point2f(200, 100), Point2f(200, 330), Point2f(500, 50), Point2f(600, 390) };
	Mat dst_affine = AffineTrans(I, AffinePoints0, AffinePoints1);
	Mat dst_perspective = PerspectiveTrans(I, AffinePoints0, AffinePoints1);

	for (int i = 0; i < 4; i++)
	{
		circle(I, AffinePoints0[i], 2, Scalar(0, 0, 255), 2);
		circle(dst_affine, AffinePoints1[i], 2, Scalar(0, 0, 255), 2);
		circle(dst_perspective, AffinePoints1[i], 2, Scalar(0, 0, 255), 2);
	}

	imshow("origin", I);
	imshow("affine", dst_affine);
	imshow("perspective", dst_perspective);
	waitKey();
}

结果如图:


可以看出,仿射变换以3个点为基准点,即使数组长度为4也仅取前3个点作为基准点;透视变换以4个点为基准点,两种变换结果不相同。应根据实际情况判断使用哪种变换方式更佳
案例实战:
将下图

视角改为鸟瞰,从而得到类似下图的鸟瞰图

第一步:首先是读取原图片并显示

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
  Mat dstImage,srcImage = imread("road.png");
  cout<<srcImage.size;  //674 x 1020
  
  imshow("原图", srcImage);
  waitKey();
  return 0;
}

第二步:选取原图上的四个点,并计算出该四对点变换后的位置。
如何选点?我们可以选两边白条的四个定点。那变换后的位置就需要我们自己估算了,如下图:

我们希望将蓝色的透视变换为黄色的。

 Point2f imgPts[4], objPts[4];  //透视前和透视后
  //原坐标
  imgPts[0].x = 20 * 1020 / 230; imgPts[0].y = 95 * 647 / 145;
  imgPts[1].x = 210 * 1020 / 230; imgPts[1].y = 95 * 647 / 145;
  imgPts[2].x = 90 * 1020 / 230; imgPts[2].y = 65 * 647 / 145;
  imgPts[3].x = 140 * 1020 / 230; imgPts[3].y = 65 * 647 / 145;
  //透视后坐标
  int road_w = 540; //将透视变换的图片大小改变一下
  int road_h = 850;
  objPts[0].x = 50;  objPts[0].y = 780;
  objPts[1].x = 490; objPts[1].y = 780;
  objPts[2].x = 50 ; objPts[2].y = 150;
  objPts[3].x = 490; objPts[3].y = 150;


第三步:计算透视变换矩阵

//计算透视变换矩阵
Mat H = getPerspectiveTransform(imgPts, objPts);

第四步:进行透视变换

//进行透视变换
warpPerspective(srcImage, dstImage, H, srcImage.size());
//画出透视变换后的四个点
circle(dstImage, objPts[0], 9, Scalar(0, 0, 255), 3);
circle(dstImage, objPts[1], 9, Scalar(0, 0, 255), 3);
circle(dstImage, objPts[2], 9, Scalar(0, 0, 255), 3);
circle(dstImage, objPts[3], 9, Scalar(0, 0, 255), 3);
imshow("变换后", dstImage);

以上是关于OpenCV图像处理--平面单应性的主要内容,如果未能解决你的问题,请参考以下文章

带有特征检测和单应性的 OpenCV 对象检测

单应性?在某些视图中处理丢失棋盘角的 OpenCV 方法?

OpenCV中的特征匹配+单应性以查找对象

2021-04-11

2021-04-11

相机标定 和 单应性矩阵H