一、仿射变换与透视变换

一直无法理解两种仿射变换与透视变换的区别,因此详细学习了两种变换的具体细节,重新书写了公式,并给出自己的一些看法。

1.仿射变换

可以认为,仿射变换透视变换的一种特例

        仿射变换是一种二维坐标二维坐标之间的线性变换,也就是只涉及一个平面内二维图形的线性变换。

图形的平移旋转错切放缩都可以用仿射变换的变换矩阵表示。

它保持了二维图形的两种性质:

       ① “平直性”:直线经过变换之后依然是直线。一条直线经过平移旋转错切放缩都还是一条直线。

        ②“平行性”:变换后平行线依然是平行线,且直线上点的位置顺序不变。

直观的感受就是,我们在电脑上对一张图片进行拖动、翻转、拉伸等等操作,看这一张图片的视角是不会变的。

任意的仿射变换都能表示为一个坐标向量乘以一个矩阵的形式,下面是几种仿射变换的矩阵形式。

放缩

旋转

错切

上面几种变换都可以直接只用2x2矩阵变换,但是平移无法做到,因为在2x2矩阵中无论怎么相乘都无法变换出一个常数量。因此需要将原本的2维坐标向量变成齐次坐标,也就是用3维向量来表示2维向量。

平移:

        

变成齐次坐标后,为了实现原本2x2矩阵的放缩、旋转、错切的变换,只需要令即可。

上面的变换都是线性变换,因此仿射变换可以用以下通式来表示,也就是网上常见到的形式:

此时仿射变换的变换矩阵2x3矩阵。

因此坐标变换的方程组如下:

可以看到有6个未知的系数,需要3对映射点(前提是相互独立)才能求解。这不难理解,6个变量自然需要至少列6个等式才可计算,而1对映射点可以提供2个等式

同时3个点唯一确定一个平面,另外的3个映射点由于是线性变换也必然在同一个平面内,所以可以说仿射变换是平面内的图形变换。

2.透视变换

        透视变换是将图片投影到一个新的视平面,也称作投影映射。

它是二维三维,再到另一个二维空间的映射。

相对于仿射变换,它不仅仅是线性变换。它提供了更大的灵活性,可以将一个四边形区域映射到另一个四边形区域。

        透视变换也是通过矩阵乘法实现的,使用的是一个3x3的矩阵,矩阵的前两行与仿射矩阵相同,这意味着仿射变换的所有变换透视变换也可以实现。而第三行则用于实现透视变换。

        透视变换也使用了齐次坐标来表示二维向量:

此时透视变换的变换矩阵3x3矩阵

透视变换得到的不是最后的坐标,需要进一步转化:

才是最后转化后的坐标,即:

事实上这里就可以明白为什么仿射变换是透视变换的一种特例。因为如果仿射变换后的坐标向量也用齐次坐标表示,2x3矩阵转为3x3矩阵可以看作是:

此时仿射变换与透视变换的形式得到统一,仿射变换过程视为如下:

所以仿射变换只是透视变换矩阵第三行的一种特殊情况。此时已经扩展到3维向量,其实我们也可以理解为仿射变换是在空间坐标系的平面上进行的平面内的图像变换,即z方向的值无论怎么变都是1,始终都限制在平面内。

再回到透视变换,整个透视变换的过程如下:

因此坐标变换的方程组如下:

共有9个未知参数,可以进一步简化为8个未知参数,也就是网上常见的形式。

这里我的思路是:证明变换矩阵与变换矩阵(k是常数),所表示的变换是等价的。

将变换矩阵设为,代入运算:

得到方程组如下:

可以发现最后计算出来的与使用变换矩阵计算出来的结果是一样的。

因此对于,我们总可以等价为所带来的转换。

对上述变量重新命名即可得到变换矩阵T:

此时只有8个未知参数,变换方程组也变成如下:

求解8个未知数需要8个等式,1组映射点提供2个等式,所以需要找到4组映射点,这也是为什么我们需要提供变换前后4个点来表示透视变换。

至此我们也可以理解为什么透视变换是二维到三维,三维又到二维的过程。变换之前的点为,这是三维空间上的点,但我们认知上它在二维平面上的投影是。通过矩阵变换成三维空间中的点,再通过除以三维中Z轴的值得到三维空间的点,最后投影到二维平面得到点。整个过程就是把二维转到三维,再转映射回之前的二维空间。

        透视变换效果相当于观察者的视角发生改变时所观察到画面产生的变化。

为什么通过一个矩阵可以实现这种视角变化?具体的数学原理在网上没找到,大部分文章也只是止步于变换矩阵,对于具体原因没有深入说明,这困扰了我很长时间。

经过推证,我认为透视变换的过程应该是如下我画的图:

首先观察点位于原点,然后往z轴的正方向看去,投影面上就是我们在显示屏看到物体的位置。

经过透视变换矩阵变化,原本的变成(可以证明,变换后的点仍然处于空间中的同一个平面)。此时的坐标已经不仅仅在平面上,而是在整个三维空间中,也就是矩阵会将原本在平面内的图形转化为空间某一个平面的图形。这就是转换矩阵使得看图片的视角变化的原因。

然后该图形各个点与视点(即原点)连线,在投影面上投影形成图形。数值上的表现是三个坐标值都除以,这么做的原因其实是几何上的等比例的缩放:

这也解决了我一直以来的困惑,即透视变换引发我们图片视角的变化并不是我们观察者视角的变化,而是观察者视角不变而对物体空间位置发生了转换,导致了我们看到的图片视角变化。这可以理解为:不是我们人走动看到了物体角度发生变化,而是人始终保持不动,其他人对物体进行了挪动导致我们看到这个物体角度发生了变化。

二、透视变换实现

在应用中,需要用到OpenCV的两个函数getPerspectiveTransform()warpPerspective()两个函数。

1.getPerspectiveTransform()

Mat getPerspectiveTransform(InputArray src, InputArray dst, int solveMethod = DECOMP_LU)

①功能描述

4对映射点计算透视变换的变换矩阵,返回的矩阵数据类型是Mat。这里需要注意的是,1对映射点指的是,但是变换的矩阵作用在。即:

②参数描述

参数 src源图像四边形的4个顶点坐标。

参数 dst目标图像对应四边形的4个顶点坐标。

参数 solveMethod:传递给cv::solve(#DecompTypes)的计算方法,默认是DECOMP_LU,一般不用输入此参数。

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

2.warpPerspective()

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

①功能描述

将变换矩阵应用于原图像,使其透视变换为目标图像。

②参数描述

        参数src:输入图像。

        参数dst:输出图像,需要初始化一个空矩阵用来保存结果,不用设定矩阵尺寸。

        参数M3x3的转换矩阵。

        参数dsize:输出图像的大小。

        参数flags:设置插值方法。默认为INTER_LINEAR表示双线性插值,INTER_NEAREST表示最近邻插值,WARP_INVERSE_MAP表示M作为反转转换 (dst->src) 。

        参数borderMode:像素外推方法,默认为BORDER_CONSTANT,指定常数填充。翻阅官方文档发现还有一个选项是BORDER_REPLICATE

        参数borderValue:常数填充时边界的颜色设置,默认是(0,0,0),表示黑色。这就是为什么透视变换后图片周围是黑色的原因。这里需要注意的是类型为Scalar (B, G, R)

3.函数的使用

代码如下:

#include <iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;void main()
{Mat img = imread("test.png");  Point2f AffinePoints0[4] = { Point2f(0, 0), Point2f(img.cols, 0), Point2f(0, img.rows), Point2f(img.cols, img.rows) };//变化前的4个节点Point2f AffinePoints1[4] = { Point2f(100, 0), Point2f(img.cols - 100, 0),Point2f(0, img.rows), Point2f(img.cols, img.rows) };//变化后的4个节点Mat Trans = getPerspectiveTransform(AffinePoints0, AffinePoints1);//由4组映射点得到变换矩阵Mat dst_perspective;//存储目标透视图像warpPerspective(img, dst_perspective, Trans, Size(img.cols, img.rows));//执行透视变换imshow("原图像", img);imshow("透视变换后", dst_perspective);waitKey();
}

执行结果如下:

可以看到,原本的效果是从正面平视电脑屏幕,现在透视变换后是仰视电脑屏幕(或者说电脑屏幕向后倾斜)。

4.映射点标记

为了更清晰地知道变换前后的映射点,我们可以在图上标注出来,这里使用到OpenCV的circle函数。

①函数原型

        void circle()

circle (InputOutputArray img, Point center, int radius, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)

②函数功能

        在图像上画一个具有给定中心和半径的空心或实心圆。

③函数参数

        参数img:画圆绘制的图像。

        参数center:画圆的圆心坐标,类型为Scalar(x, y)

        参数radius:圆的半径。

        参数color:圆的颜色,规则为(B,G,R),类型为Scalar(B,G,R)

        参数thickness:如果正数表示组成圆的线条的粗细程度。如果是负数表示圆是否被填充,如FILLED表示要绘制实心的圆。

        参数line_type:线条的类型,默认是LINE_8

        参数shift:圆心坐标点和半径值的小数点位数,默认为0位

④代码过程

    for (int i = 0; i < 4; i++)//显示4组映射点的位置{//画一个圆心在映射点(转换前),半径为10,线条粗细为3,红色的圆circle(img, AffinePoints0[i], 10, Scalar(59, 23, 232), 3);//画一个圆心在映射点(转换后),半径为10,线条粗细为3,蓝色的圆circle(dst_perspective, AffinePoints1[i], 10, Scalar(139, 0, 0), 3);}

执行效果:

可以看到转换前后的4组点都被画了出来,因为我映射点设置在边角,所以只能看得到圆的一部分。当设置在图片中间时可以看到整个圆形。

5.库函数的实现

一般直接调用上述函数即可实现透视变换,这里为了更好的理解变换过程,我对上面两个函数中的透视变换函数进行了实现(求变换矩阵的函数就是纯数学问题,这里不再重复),可能与官方的库函数实现有所不同。

        warpPerspective的实现思路是:

已知变换矩阵源图像,需要使用变换矩阵源图像透视变换为目标图像。这个过程并不是我们上面理解的正向过程,即将变换矩阵T乘上原图像的所有坐标得到目标图像的坐标。因为这样会导致目标图像上的个别坐标没有映射。我们使用反向过程,将映射回原图像寻找目标像素值。反向映射的推导过程如下:

已知:

同除以

同左乘

即:

设求得的变换矩阵T的逆为:

展开后可以得到:

该式表明,当我们求得变换矩阵T的逆之后,对于目标图像上的任意坐标都可以用上面公式求得在原图像上对应坐标的位置,进而得到这个位置的像素值。当然,位置不一定是整数,可能需要插值。

代码实现过程如下:

//自己实现的wrapPerspective函数
void _wrapPerspective(const Mat& src, const Mat& T, Mat& dst)//src为源图像,T为变换矩阵,dst为目标图像
{dst.create(src.size(), src.type());//创建一个和原图像一样大小的MatMat T_inverse;//变换矩阵的逆invert(T, T_inverse);//求矩阵T的逆,结果存到T_inverse//取出矩阵中的值double c11 = T_inverse.ptr<double>(0)[0];double c12 = T_inverse.ptr<double>(0)[1];double c13 = T_inverse.ptr<double>(0)[2];double c21 = T_inverse.ptr<double>(1)[0];double c22 = T_inverse.ptr<double>(1)[1];double c23 = T_inverse.ptr<double>(1)[2];double c31 = T_inverse.ptr<double>(2)[0];double c32 = T_inverse.ptr<double>(2)[1];double c33 = T_inverse.ptr<double>(2)[2];//遍历目标图像的每个位置,求取原图像对应位置的像素值for (int y = 0; y < dst.rows; y++){for (int x = 0; x < dst.cols; x++){double xp = c11 * x + c12 * y + c13;double yp = c21 * x + c22 * y + c23;double z = c31 * x + c32 * y + c33;//z'z = z ? 1.0 / z : 0;//z'不为0时求导数,否则设为0xp *= z;yp *= z;//将双精度坐标限制在整型能表示的最大最小值之间double fx = max((double)INT_MIN, min((double)INT_MAX, xp));double fy = max((double)INT_MIN, min((double)INT_MAX, yp));//转化为int,这里简单地使用了最近邻插值int X = saturate_cast<int>(fx);int Y = saturate_cast<int>(fy);//是否在原图像大小范围内if (X >= 0 && X < src.cols && Y >= 0 && Y < src.cols){dst.at<Vec3b>(y, x)[0] = src.at<Vec3b>(Y, X)[0];dst.at<Vec3b>(y, x)[1] = src.at<Vec3b>(Y, X)[1];dst.at<Vec3b>(y, x)[2] = src.at<Vec3b>(Y, X)[2];}else//以黑色填充{dst.at<Vec3b>(y, x)[0] = 0;dst.at<Vec3b>(y, x)[1] = 0;dst.at<Vec3b>(y, x)[2] = 0;}}}
}

运行效果:

和官方的函数实现的结果比较可发现基本一致,说明实现思路正确。

三、透视变换的应用

1、透视变换的交互程序

①实验要求:

设计一个交互程序,可以编辑四边形顶点,并且顶点位置改变时图像形变的结果可以实时更新

②实验思路:

根据上面提到的知识,实现已知4对映射点的的透视变换是很容易的。因此问题的难点在于如何设计交互程序,这里需要使用到OpenCV的鼠标点击事件。

void setMousecallback(const string& winname, MouseCallback onMouse, void* userdata=0)

参数描述:

        winname:窗口的名字。

        onMouse:鼠标响应函数或者说回调函数。指定窗口里每次鼠标事件发生的时候,被调用的函数指针。 这个函数的原型为void on_Mouse(int event, int x, int y, int flags, void* param)

        userdata:传给回调函数的参数,默认为0。这个参数我个人还没用到过。

void MouseCallback(int event,int x,int y,int flags,void *useradata);

参数描述:

        event:鼠标事件。

        x:鼠标事件的x坐标。

        y: 鼠标事件的y坐标。

        flags: 代表鼠标的拖拽事件和键盘鼠标联合的事件。

        userdata : 可选的参数,目前没用到过。

        鼠标事件event主要有下面几种:

EVENT_MOUSEMOVE :鼠标移动

EVENT_LBUTTONDOWN : 鼠标左键按下

EVENT_RBUTTONDOWN : 鼠标右键按下

EVENT_MBUTTONDOWN : 鼠标中键按下

EVENT_LBUTTONUP : 鼠标左键放开

EVENT_RBUTTONUP : 鼠标右键放开

EVENT_MBUTTONUP : 中键放开

EVENT_LBUTTONDBLCLK : 左键双击

EVENT_RBUTTONDBLCLK : 右键双击

EVENT_MBUTTONDBLCLK : 中键双击

        Flags主要有一下几种:

EVENT_FLAG_LBUTTON :左键拖拽

EVENT_FLAG_RBUTTON : 右键拖拽

EVENT_FLAG_MBUTTON : 中键拖拽

EVENT_FLAG_CTRLKEY : Ctrl按下不放

EVENT_FLAG_SHIFTKEY : shift按下不放

EVENT_FLAG_ALTKEY : alt按下不放

总体思路:

为了更好的观感,我们需要一张比原图像稍大一点的画布来放目标图像。初始4个映射点在原图像的4个角上。在这张画布上,当鼠标移动到4个映射点的圆形范围时,如果此时按下左键不放开就可以实现映射点的拖拽。当鼠标左键放开时,此时鼠标的位置就是映射点移动后的位置。然后重新计算变换矩阵,对原图像实现透视变换并显示。

当然实时更新也可以理解为当左键拖拽时映射点位置一发生变化就计算变换矩阵并实现透视变换,但实验中发现这样延迟很高(debug模式下),并且实际使用中我们有时候已经明确要将映射点移动到某个目标位置了,但在这移动过程中都会进行不必要的透视变换。

③实现过程

重点在于鼠标事件的编写,其他的过程都与前面透视变换方法相似。

当左键按下时,需要判断是否点击在了映射点的区域,如果点在了有效区域,那么还需要记录点击的是哪一个映射点。

当左键按下并拖拽时,如果需要在拖拽过程中实时更新透视变换,那么实时记录映射点位置并进行透视变换。我这里实现方式上选择了左键松开后再进行透视变换,因此左键拖拽时不需要记录映射点位置。但为了更好的交互效果,我还是记录位置并绘制了一些直线和圆形进行交互提示。

当左键松开后,执行透视变换。

具体代码如下:

void mouseHander(int event, int x, int y, int flags, void* p)
{if (event == EVENT_LBUTTONDOWN)//左键按下{for (int i = 0; i < 4; i++){//判断是否选择了某个映射点if (abs(x - dstPoint[i].x) <= radius && abs(y - dstPoint[i].y) <= radius){pickPoint = i;beforePlace = dstPoint[pickPoint];//记录原本的位置break;}}}else if (event == EVENT_MOUSEMOVE && pickPoint >= 0 )//左键按下后选取了某个点且拖拽{//更改映射后坐标dstPoint[pickPoint].x = x, dstPoint[pickPoint].y = y;//在临时图像上实时显示鼠标拖动时形成的图像//注意不能直接在dstImg上画,否则会画很多次Mat tmp = dstImg.clone();//原本的圆circle(tmp, beforePlace, radius, Scalar(228, 164, 140), -1);//绘制直线line(tmp, dstPoint[0], dstPoint[1], Scalar(246, 230, 171), 5, 8);line(tmp, dstPoint[1], dstPoint[2], Scalar(246, 230, 171), 5, 8);line(tmp, dstPoint[2], dstPoint[3], Scalar(246, 230, 171), 5, 8);line(tmp, dstPoint[3], dstPoint[0], Scalar(246, 230, 171), 5, 8);//重新绘制4个圆for (int i = 0; i < 4; i++){if (i != pickPoint)circle(tmp, dstPoint[i], radius, Scalar(228, 164, 140), -1);elsecircle(tmp, dstPoint[i], radius, Scalar(96, 96, 240), -1);}imshow("透视变换后", tmp);}else if (event == EVENT_LBUTTONUP && pickPoint >= 0)//左键松开{//执行透视变换Mat Trans = getPerspectiveTransform(srcPoint, dstPoint);//由4组映射点得到变换矩阵warpPerspective(srcImg, dstImg, Trans, Size(dstImg.cols, dstImg.rows));//执行透视变换for (int i = 0; i < 4; i++)//显示4组映射点的位置{//画一个圆心在映射点(转换后),半径为10,线条粗细为3,黄色的圆circle(dstImg, dstPoint[i], radius, Scalar(0, 215, 255), 3);}imshow("透视变换后", dstImg);pickPoint = -1;//重置选取状态}
}

④运行结果

接着对不同图片进行测试:

可以看到对路的海报透视变换后,我们看的视角从平视变成了俯视路面。

今天随手拍了一张图书馆的照片,是从图书馆左侧的位置拍摄的,对其透视变换:        

可以看到调整完后,视角更接近于正面。

⑤源代码

#include <iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;Mat srcImg, dstImg;//原图像、目标图像
Point2f srcPoint[4], dstPoint[4];//原图像和目标图像的4个映射点
Point2f beforePlace;//记录移动映射点之前的位置
int radius;//映射点的判定半径
int pickPoint;//记录点击了哪个点void mouseHander(int event, int x, int y, int flags, void* p)
{if (event == EVENT_LBUTTONDOWN)//左键按下{for (int i = 0; i < 4; i++){//判断是否选择了某个映射点if (abs(x - dstPoint[i].x) <= radius && abs(y - dstPoint[i].y) <= radius){pickPoint = i;beforePlace = dstPoint[pickPoint];//记录原本的位置break;}}}else if (event == EVENT_MOUSEMOVE && pickPoint >= 0 )//左键按下后选取了某个点且拖拽{//更改映射后坐标dstPoint[pickPoint].x = x, dstPoint[pickPoint].y = y;//在临时图像上实时显示鼠标拖动时形成的图像//注意不能直接在dstImg上画,否则会画很多次Mat tmp = dstImg.clone();//原本的圆circle(tmp, beforePlace, radius, Scalar(228, 164, 140), -1);//绘制直线line(tmp, dstPoint[0], dstPoint[1], Scalar(246, 230, 171), 5, 8);line(tmp, dstPoint[1], dstPoint[2], Scalar(246, 230, 171), 5, 8);line(tmp, dstPoint[2], dstPoint[3], Scalar(246, 230, 171), 5, 8);line(tmp, dstPoint[3], dstPoint[0], Scalar(246, 230, 171), 5, 8);//重新绘制4个圆for (int i = 0; i < 4; i++){if (i != pickPoint)circle(tmp, dstPoint[i], radius, Scalar(228, 164, 140), -1);elsecircle(tmp, dstPoint[i], radius, Scalar(96, 96, 240), -1);}imshow("透视变换后", tmp);}else if (event == EVENT_LBUTTONUP && pickPoint >= 0)//左键松开{//执行透视变换Mat Trans = getPerspectiveTransform(srcPoint, dstPoint);//由4组映射点得到变换矩阵warpPerspective(srcImg, dstImg, Trans, Size(dstImg.cols, dstImg.rows));//执行透视变换for (int i = 0; i < 4; i++)//显示4组映射点的位置{//画一个圆心在映射点(转换后),半径为10,线条粗细为3,黄色的圆circle(dstImg, dstPoint[i], radius, Scalar(0, 215, 255), 3);}imshow("透视变换后", dstImg);pickPoint = -1;//重置选取状态}
}void main()
{srcImg = imread("library.jpg");  radius = 10;//设置四个点的圆的半径pickPoint = -1;//映射前的4个点srcPoint[0] = Point2f(0, 0);srcPoint[1] = Point2f(srcImg.cols, 0);srcPoint[2] = Point2f(srcImg.cols, srcImg.rows);srcPoint[3] = Point2f(0, srcImg.rows);//创建一张略大于原图像的画布dstImg = Mat::zeros(Size(2 * radius + 100 + srcImg.cols, 2 * radius + 100 + srcImg.rows), srcImg.type());//初始映射后的4个点dstPoint[0] = Point2f(radius + 50, radius + 50);dstPoint[1] = Point2f(radius + 50 + srcImg.cols, radius + 50);dstPoint[2] = Point2f(radius + 50 + srcImg.cols, radius + 50 + srcImg.rows);dstPoint[3] = Point2f(radius + 50, radius + 50 + srcImg.rows);Mat Trans = getPerspectiveTransform(srcPoint, dstPoint);//由4组映射点得到变换矩阵Mat dst_perspective;//存储目标透视图像warpPerspective(srcImg, dstImg, Trans, Size(dstImg.cols, dstImg.rows));//执行透视变换for (int i = 0; i < 4; i++)//显示初始4组映射点的位置{//画一个圆心在映射点(转换后),半径为10,线条粗细为3,黄色的圆circle(dstImg, dstPoint[i], radius, Scalar(95, 180, 243), 3);}imshow("原图像", srcImg);imshow("透视变换后", dstImg);//鼠标事件setMouseCallback("透视变换后", mouseHander);waitKey();
}

2.虚拟广告牌的实现

根据上面的透视变换的过程,我们可以想到一种应用:将图片A作为广告牌透视变换到图片B的特定位置。

实现过程也很容易,我们将图片A的初始映射点设置在四个角上,然后在背景图B上选择透视变换后的4个映射点,然后将图片A透视变换到指定位置(将图片B的原位置覆盖)。这里需要注意的是映射点的选取需要规定好固定的顺序,否则无法与原本的映射点一一对应,我默认选取点的顺序是从左上角开始顺时针选取。

#include <iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;Mat srcImg, dstImg;//原图像、目标图像
Mat resultImg;//结果图像
vector<Point2f> srcPoints, dstPoints;//原图像和目标图像的映射点
int pickNums;//目前已选取的节点void mouseHander(int event, int x, int y, int flags, void* p)
{if (event == EVENT_LBUTTONDOWN)//左键按下{Mat tmp = dstImg.clone();if (pickNums == 4)//选取的点超过4个后,下一次点击会实现透视变换{//执行透视变换Mat Trans = getPerspectiveTransform(srcPoints, dstPoints);//由4组映射点得到变换矩阵warpPerspective(srcImg, tmp, Trans, Size(tmp.cols, tmp.rows));//执行透视变换resultImg = dstImg.clone();for (int y = 0; y < dstImg.rows; y++){for (int x = 0; x < dstImg.cols; x++){if ((int)tmp.at<Vec3b>(y, x)[0] == 0 && (int)tmp.at<Vec3b>(y, x)[1] == 0 && (int)tmp.at<Vec3b>(y, x)[2] == 0)//像素点全0continue;else//非全0{resultImg.at<Vec3b>(y, x)[0] = tmp.at<Vec3b>(y, x)[0];resultImg.at<Vec3b>(y, x)[1] = tmp.at<Vec3b>(y, x)[1];resultImg.at<Vec3b>(y, x)[2] = tmp.at<Vec3b>(y, x)[2];}}}imshow("虚拟广告牌", resultImg);dstPoints.clear();pickNums = 0;}else//选取的节点还没4个{dstPoints.push_back(Point2f(x, y));pickNums++;for (int i = 0; i < dstPoints.size(); i++){circle(tmp, dstPoints[i], 5, Scalar(0, 215, 255), 3);}imshow("虚拟广告牌", tmp);}}}int main()
{srcImg = imread("test.png");//透视变换的图像,也就是广告图//设置原图像的4个映射点srcPoints.push_back(Point2f(0, 0));srcPoints.push_back(Point2f(srcImg.cols, 0));srcPoints.push_back(Point2f(srcImg.cols, srcImg.rows));srcPoints.push_back(Point2f(0, srcImg.rows));dstImg = imread("library.jpg");//背景图imshow("虚拟广告牌", dstImg);//鼠标事件setMouseCallback("虚拟广告牌", mouseHander);waitKey(0);return 0;
}

实现效果:

我们还可以用来把同学的电脑屏幕变成你想要的照片:

基于OpenCV的图像透视变换详解(从理论到实现再到实践)相关推荐

  1. opencv 仿射变换与透视变换详解

    常见的2D图像变换从原理上讲主要包括基于2×3矩阵的仿射变换和基于3×3矩阵透视变换. 仿射变换 原理 基本的图像变换就是二维坐标的变换:从一种二维坐标(x,y)到另一种二维坐标(u,v)的线性变换: ...

  2. 基于opencv的模板匹配详解

    1.什么是模板匹配 在OpenCV教程中这样解释模板匹配: 模板匹配是一项在一幅图像中寻找与另一幅模板图像最匹配(相似)部分的技术.这里说的模板是我们已知的小图像,模板匹配就是在一副大图像中搜寻目标. ...

  3. 【OpenCV】图像旋转详解,边缘用黑色填充

    项目要用到图像旋转,OpenCV里面居然没有专门封装好的函数,只好自己写了.根据<learnning OpenCV>发现效果不是很理想,旋转后图像大小不变,可是图像却被裁减了. 例子如下: ...

  4. 【OpenCV】图像金字塔详解及编程实现

    图像金字塔被广泛用于各种视觉应用中.图像金字塔是一个图像集合,集合中所有的图像都源于同一个原始图像,而且是通过对原始图像连续降采样获得,直到达到某个中止条件才停止降采样.有两种类型的图像金字塔常常出现 ...

  5. OpenCV实战(12)——图像滤波详解

    OpenCV实战(12)--图像滤波详解 0. 前言 1. 频域分析 2. 低通滤波器 3. 图像下采样 3.1 使用低通滤波器下采样图像 3.2 内插像素值 4. 中值滤波器 5. 完整代码 小结 ...

  6. 基于opencv的图像阴影消除车辆变道检测

    基于opencv的图像阴影消除 详细代码在这!!! 最大滤波 def max_filtering(N, I_temp):wall = np.full((I_temp.shape[0]+(N//2)*2 ...

  7. OpenCV SIFT源码详解——总体概览

    OpenCV SIFT源码详解--总体概览 一.版本 二.章节系列 此系列文章源自本人硕士毕业论文,主要讲源码,对于SIFT理论知识默认大家有过了解.若文章中有不对之处还请读者指出. 一.版本 本系列 ...

  8. 逆透视变换详解 及 代码实现(二)

    根据 逆透视变换详解 及 代码实现(一)的原理 下面我用车上拍摄的车道图像,采用逆透视变换得到的图像,给出代码前我们先看下处理结果. 首先是原始图像: 下图为逆透视变换图像: 下面说具体的实现吧!! ...

  9. 逆透视变换详解 及 代码实现(一)

    逆透视变换详解 及 代码实现(一) 中主要是原理的说明: 一.世界坐标轴和摄像机坐标轴 从下图中可以看到,世界坐标为(X,Y,Z)  相机坐标为(Xc,Yc,Zc) 而世界坐标变换到相机坐标存在一个旋 ...

最新文章

  1. mysql 多线程 一致性_常见缓存数据库一致性方案(建议收藏)
  2. 字符串相加/大数相加(代码极短)
  3. IDEA启动项目:找不到或无法加载主类
  4. 学习opencv(1)
  5. 新版本springboot-整合多数据源拆分思路
  6. 湖南大学计算机系统原理实验,湖南大学-计算机组成原理实验-实验3-bomblab_图文.pdf...
  7. Python 集合(set) 介绍
  8. 接口测试系列:工作中所用(十:配置文件的读写操作 configparser模块)
  9. java.lang.Runtime 运行时类 执行 dos 、cmd 命令、VBS 脚本
  10. javascript怎么判断对象为空
  11. windows技巧——notepad2 取代自带 notepad ,功能强大!
  12. 【科研论文】找到中文论文的英文引用格式
  13. 08CMS之后台二次开发
  14. Rayman的绝顶之路——Leetcode每日一题打卡11
  15. 这绝对是目前最好用的电脑桌面便签,免费的,墙裂推荐
  16. Zookeeper--客户端操作
  17. Exception】Chrome浏览器提示:此网页正试图从未经验证的来源加载脚本
  18. KS值是衡量分类模型预测准确度的重要指标之一,它反映了模型预测结果与实际数据分布差异的程度。本文将介绍什么是KS值,如何计算以及在Python中如何实现。
  19. codeforces 549F Yura and Developers(分治、启发式合并)
  20. 阿里董事局主席张勇:数字化建设将成为新发展方向

热门文章

  1. 【测试记录】基于pdf论文提取论文doi—pdf2doi包的安装与使用
  2. Tapestry原则
  3. GitHub 之 上传文件(一)
  4. 估计中的概率公式总结
  5. Matlab画天球坐标图,知道方位角和高度角
  6. DC Administration Services 宣布ISDA裁决委员会2020年申请流程
  7. 2016级移动应用开发在线测试13-Location、Sensor Network
  8. Linux常用的设置文件安全上下文命令使用方法
  9. apache beam java api_Apache Beam的基本概念
  10. 百度广告屏蔽及游戏推广屏蔽