1. 理论公式
透视变换(Pespective Transform)是将一个视平面上的物体转换到一个新的视平面。变换公式如下:
其中等式右边的u,v是源图片的坐标,在变换后图像中的对应坐标x, y,可以用下式计算得到:
据此,原图像和透视变换后的目标图像中的点,对应转换关系如下:
变换矩阵的子矩阵
表示线性变换,比如scaling(缩放),shearing和rotation(旋转)。
表示平移。
产生透视变换。所以可以认为仿射变换是透视变换的特殊形式。到此,我们解释了透视变换的理论公式,那透视变换矩阵中的9个参数该如何求解呢?强大的OpenCV库粉墨登场。
2. OpenCV的getPerspectiveTransform函数,warpPerspective函数和findHomography函数
在第一部分介绍的透视变换矩阵可以使用OpenCV库的getPerspectiveTransform函数求解,它在OpenCV 2.4.13中的函数原型如下:
Mat getPerspectiveTransform(InputArray src, InputArray dst)
参数: src为原图像四边形定点的坐标集合。dst为目标图像对应四边形定点的坐标集合。在这里建议使用std::vector<point2f> 数据结构存储四个点的坐标。Note: 坐标值必须是32f,也就是float类型,使用std::vector<point>是不行的。
warPerspective函数是对一个图像做透视变换,它的C++格式的函数原型声明如下:
void warpPerspective(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags=INTER_LINEAR, int borderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())
参数: src是源图像,也就是我们想操作的图像,dst是变换后的目标图像,M就是我们之前得到的透视变换矩阵。dsize是得到的目标图像的尺寸,这是个很有意思的参数,我一般是设为,之前计算透视矩阵时,选取的目标四边形的大小。如若不然,会得到很丑的黑色填充区域。至于剩下的参数,他们都有缺省值,一般用不到,读者如果感兴趣,可以翻阅一下reference manual,目前最新版本是2.4.13.
Mat findHomography(InputArray srcPoints, InputArray dstPoints, int method=0, double ransacReprojThreshold=3, OutputArray mask=noArray() )
该函数是计算两个面之间对应点之间的单应性矩阵,点对之间如需要十分精准的映射关系,我们就使用该函数,而不是getPerspectiveTransform。
参数:srcPoints是原图像中的点坐标,dstPoints是目标图像中的点坐标。在选取点时,越均匀,映射关系越好。method有:0-使用所有点的常规方法,CV_RANSAC-基于RANSAC的鲁棒方法,CV_LMEDS-最小中位数鲁棒方法。
void perspectiveTransform(InputArray src, OutputArray dst, InputArray m)
该函数实现视角转换,实现点点之间的映射。src是源图像中的点,dst是目标图像中的点,m就是之前的getPerspectiveTransform函数和findHomography函数中计算出来的映射矩阵。
3. 撸代码
来来来,代码撸起来!!!程序思路如下:使用OpenCV的鼠标点击响应函数,手动选取出四边形四个点坐标(坐标点自动识别比较难,对于一些特殊的图形,可以试试直线霍夫变换,角点检测实现自动识别)。然后设定目标图像的尺寸,计算透视转换矩阵,完成透视转换。最后,我希望我在原图像里点击一个位置,转换后图像的对应位置,也能有一致的响应。
#include <opencv2/core/core.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void mouse(int event, int x, int y, int flags, void*);
Mat src, gray, dst_img, h;
vector<Point2f> selected;
vector<Point2f> dst;
int width =500;
int height =400;
int flag=0;
int main()
{
src = imread("book.jpg", 1);
imshow("book", src);
setMouseCallback("book", mouse, 0); //void setMouseCallback(const string& winname, MouseCallback onMouse, void* userdata=0)
dst.push_back(Point2f(0, 0));
dst.push_back(Point2f(width-1, 0));
dst.push_back(Point2f(width-1, height-1));
dst.push_back(Point2f(0, height-1));
waitKey();
}
void mouse(int event, int x, int y, int flags, void*)
{
if(event == EVENT_LBUTTONDOWN) //如果鼠标按下了。
{
circle(src, Point(x,y), 3, Scalar(0,0,255), -1);
imshow("book", src);
selected.push_back(Point2f(x,y));
++flag;
if(flag==4)
{
h= getPerspectiveTransform(selected, dst);
warpPerspective(src, dst_img, h, Size(width, height));
imshow("dst", dst_img);
waitKey(1);
}
if(flag>4) //我在透视转换前的图像里点击一个位置,我们希望在透视转换后的图像里,也可以有相应的响应.
{
double h11=h.at<double>(0, 0);
double h12=h.at<double>(0, 1);
double h13=h.at<double>(0, 2);
double h21=h.at<double>(1, 0);
double h22=h.at<double>(1, 1);
double h23=h.at<double>(1, 2);
double h31=h.at<double>(2, 0);
double h32=h.at<double>(2, 1);
double h33=h.at<double>(2, 2);
int tr_x=(int)(h11*x+h12*y+h13)/(h31*x+h32*y+h33);
int tr_y=(int)(h21*x+h22*y+h23)/(h31*x+h32*y+h33);
//也可以使用perspectiveTransform函数,直接转换
//vector<Point2f> tmp(1);
//tmp.push_back(Point2f(tr_x, tr_y));
//vector<Point2f> p(1);
//perspectiveTransform(tmp, p, h);
//circle(dst_img, p[0], 3, Scalar(0,0,255), -1);
circle(dst_img, Point(tr_x,tr_y), 3, Scalar(0,0,255), -1);
imshow("dst", dst_img);
waitKey();
}
}
}
4. 成果展示
左图是源图像,我选取了它的四个角,进行透视变换,得到了右边方方正正的书!至于点击响应,羽毛的中间部分已经被我点满啦!