透视变换简析
引言
大学时就感觉OpenCV挺有意思,比如里面的透视变换,通过四个点就可以计算一张二维图和另外一张二维图之间的映射关系,后续通过映射关系就可以将两者之中任意一个图中的元素映射到另外一个图。很遗憾工作后才开始了解其原理。
正文
我是从博客入手学习的,CSDN博主 小魏的修行路 的 两篇博文给了我很大启发,很感谢。两篇博文链接如下:
https://blog.csdn.net/xiaowei_cqu/article/details/26471527
https://blog.csdn.net/xiaowei_cqu/article/details/26478135
1、捋博文的内容
参照 https://blog.csdn.net/xiaowei_cqu/article/details/26471527 这篇文章,我先按照博主的思路,将四边形A变换为四边形B的过程变成: 四边形A先到单位正方形的变换,然后加上单位正方形到四边形B的变换两个阶段。这里就手推(看这字,妥妥手推的(逃. )一下单位正方形到四边形变换的过程:
以上我们已经推导出了方形到任意四边形的透视变换矩阵计算公式,现在我们可以计算四边形A变换为四边形B的变换矩阵H了。
先求四边形A到单位方形的变换矩阵H1,再求单位方形到四边形B的变换矩阵H2,那么四边形A到四边形B的变换矩阵H就等于 H1∗H2。
方形到四边形B的变换矩阵H2可以直接按照推导出的公式得到,H1我们可以这样得到:先求单位方形到四边形A转换的矩阵F,那么H1=F-1,利用伴随矩阵公式F*=F-1 * |F|,推得H1 = F* / |F|。
其实到这一步就可以了,代码实现起来很清晰,整个透视变换过程完成了。
本着能少计算就少计算的原则,我们可以对H做下精简。
我们再来回顾一下,已知四边形A到四边形B的透视变换矩阵H,求四边形A中某点(x,y)在四边形B中对应的点(x′,y′):
x′=(C11∗x+C21∗y+C31) / (C13∗x+C23∗y+C33)
y′=(C12∗x+C22∗y+C32) / (C13∗x+C23∗y+C33)
当我们取H′ = n * H (n != 0)来作为新的变换矩阵的话,四边形A中某点(x,y)在四边形B中对应的点(x′,y′)还是不变的。因为x′、y′计算表达式中分子分母将n抵消,所以H′和H矩阵代入式子计算得到的结果是一样的。
既然这样,我们就取n = |F|,即H′ = |F| * H = |F| * H1∗H2 = |F| * F* / |F|∗H2 = F* ∗H2
这样就得到小魏博客中透视变换代码中计算原理的最终形式。
2、进一步思考
现在我有一个255 * 255 * 24(位深)图片:
我想让它变换到一张600 * 500 *24的图上,位置(默认坐标为(x, y)形式)为:
(117, 31) // top left
(420, 25) // top right
(120, 218) // bottom left
(418, 450) // bottom right
我们创建一个 600 * 500 * 24黑色背景的图片,同时规定求的是图A到图B的透视变换矩阵HHH。这种前提下,我们求得的透视变换矩阵HHH是图A到图B的映射,图A中像素(坐标)都可以映射到图B中,但是图B中不是每个点都与图A中的点有映射关系。所以会出现啥结果呢?接下来根据谁是图A谁是图B进一步讨论。
(1)我们设255 * 255的图为图A,设600 * 500的图为图B
这种情况下,因为600 * 500 * 24的图是我们的目标图同时又是图B,所以在生成的目标图中会有部分像素(坐标)因为与图A中没有映射关系而呈现背景色:
图B中与图A中没有建立映射关系的点显示为背景色(黑色)。
(2)我们设255 * 255的图为图B,设600 * 500的图为图A
这种情况下,因为600 * 500 * 24的图是我们的目标图同时又是图A,所以在生成的目标图中所有像素(坐标)都与图A中有映射关系(映射后的坐标为负值的后期过滤掉即可,不过这也算是有映射)。所以生成的600 * 500 * 24的目标图见下方:
(3)不管设置谁为图A图B,该插值插值
通过后期插值操作,这样怎么也不会出现(1)中情况了。
(4)小总结
我是建议把已知的图像作为图B, 待操作图/目标图作为图A,这样也不用后续的插值操作。同理,OpenCV里面的cv::getPerspectiveTransform() 和 cv::perspectiveTransform()函数使用时也要考虑这种情况,免得透视变换后的图中出现(1)中情况不知所措。
结尾
结尾,附上两个测试样例,一个是调用OpenCV的透视变换函数实现透视变换,一个是调用小魏的PerspectiveTransform类实现透视变换,这两个例子都很好修改来验证博文的内容,大家可以更换图A图B的设定来看看生成的目标图的变化:
1 // 调用OpenCV的透视变换函数的样例 2 #include <opencv2/highgui/highgui.hpp> 3 #include <opencv2/imgproc/imgproc.hpp> 4 5 using namespace cv; 6 7 int main() 8 { 9 // 目标图/待操作图 10 Mat dstImg = Mat::zeros(500, 600, CV_8UC3); 11 Mat srcImg = imread("E:/test.jpg"); 12 13 // 透视变换前的图 对应博文中的图A 14 Mat beforeTransformImg = dstImg; 15 int nBeforeTransHeight = beforeTransformImg.rows; 16 int nBeforeTransWidth = beforeTransformImg.cols; 17 // 透视变换后的图 对应博文中的图B 18 Mat afterTransformImg = srcImg; 19 int nAfterTransHeight = afterTransformImg.rows; 20 int nAfterTransWidth = afterTransformImg.cols; 21 22 vector<Point2f> corners(4); 23 corners[0] = Point2f(117, 31); 24 corners[1] = Point2f(420, 25); 25 corners[2] = Point2f(120, 218); 26 corners[3] = Point2f(418, 450); 27 28 vector<Point2f> corners_trans(4); 29 corners_trans[0] = Point2f(0, 0); 30 corners_trans[1] = Point2f(nAfterTransWidth - 1, 0); 31 corners_trans[2] = Point2f(0, nAfterTransHeight - 1); 32 corners_trans[3] = Point2f(nAfterTransWidth - 1, nAfterTransHeight - 1); 33 34 Mat transform = getPerspectiveTransform(corners, corners_trans); 35 vector<Point2f> ponits, points_trans; 36 for (int cy = 0; cy < nBeforeTransHeight; cy++) 37 { 38 for (int cx = 0; cx < nBeforeTransWidth; cx++) 39 { 40 ponits.push_back(Point2f(cx, cy)); 41 } 42 } 43 perspectiveTransform(ponits, points_trans, transform); 44 45 int count = 0; 46 for (int cy = 0; cy < nBeforeTransHeight; cy++) 47 { 48 uchar* t = beforeTransformImg.ptr<uchar>(cy); 49 for (int cx = 0; cx < nBeforeTransWidth; cx++) 50 { 51 int y = points_trans[count].y; 52 int x = points_trans[count].x; 53 count++; 54 55 if (x<0 || x > (nAfterTransWidth - 1) || y < 0 || y > (nAfterTransHeight - 1)) 56 continue; 57 uchar* p = afterTransformImg.ptr<uchar>(y); 58 t[cx * 3] = p[x * 3]; 59 t[cx * 3 + 1] = p[x * 3 + 1]; 60 t[cx * 3 + 2] = p[x * 3 + 2]; 61 } 62 } 63 imwrite("E:/trans2.jpg", dstImg); 64 65 return 0; 66 }
1 // 调用小魏PerspectiveTransform类进行透视变换的样例 2 #include "PerspectiveTransform.h" 3 #include <opencv2/highgui/highgui.hpp> 4 #include <opencv2/imgproc/imgproc.hpp> 5 6 using namespace cv; 7 8 int main() 9 { 10 // 目标图/待操作图 11 Mat dstImg = Mat::zeros(500, 600, CV_8UC3); 12 Mat srcImg = imread("E:/test.jpg"); 13 14 // 透视变换前的图 对应博文中的图A 15 Mat beforeTransformImg = dstImg; 16 int nBeforeTransHeight = beforeTransformImg.rows; 17 int nBeforeTransWidth = beforeTransformImg.cols; 18 // 透视变换后的图 对应博文中的图B 19 Mat afterTransformImg = srcImg; 20 int nAfterTransHeight = afterTransformImg.rows; 21 int nAfterTransWidth = afterTransformImg.cols; 22 23 PerspectiveTransform tansform = PerspectiveTransform::quadrilateralToQuadrilateral( 24 117, 31, //top left 25 420, 25, //top right 26 120, 218, //bottom left 27 418, 450, 28 0, 0, //top left 29 nAfterTransHeight - 1, 0, //top right 30 0, nAfterTransWidth - 1, //bottom left 31 nAfterTransHeight - 1, nAfterTransWidth - 1 32 ); 33 34 vector<float> ponits; 35 for (int cy = 0; cy < nBeforeTransHeight; cy++) 36 { 37 for (int cx = 0; cx < nBeforeTransWidth; cx++) 38 { 39 ponits.push_back(cx); 40 ponits.push_back(cy); 41 } 42 } 43 tansform.transformPoints(ponits); 44 45 for (int cy = 0; cy < nBeforeTransHeight; cy++) { 46 uchar* t = beforeTransformImg.ptr<uchar>(cy); 47 for (int cx = 0; cx < nBeforeTransWidth; cx++) { 48 int tmp = cy * nBeforeTransWidth + cx; 49 int x = ponits[tmp * 2]; 50 int y = ponits[tmp * 2 + 1]; 51 if (x < 0 || x > (nAfterTransWidth - 1) || y < 0 || y > (nAfterTransHeight - 1)) 52 continue; 53 uchar* p = afterTransformImg.ptr<uchar>(y); 54 t[cx * 3] = p[x * 3]; 55 t[cx * 3 + 1] = p[x * 3 + 1]; 56 t[cx * 3 + 2] = p[x * 3 + 2]; 57 } 58 } 59 imwrite("E:/trans.jpg", dstImg); 60 61 return 0; 62 }