开篇注:博客是为了更好的思考,希望能以此记录自己的学习历程。随着时间流逝,可能有些内容已经失效,望读者望文观义,get到关键点。假如对文中有啥有疑问、有想法、感觉不太对的地方欢迎留言交流~。
引言
大学时就感觉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,先求四边形A到单位方形的变换矩阵H1H_{1}H1,再求单位方形到四边形B的变换矩阵H2H_{2}H2,那么四边形A到四边形B的变换矩阵HHH就等于 H1∗H2H_{1} * H_{2}H1∗H2。
因为已经推导出了方形到四边形的透视变换矩阵计算公式,所以具体在求四边形A到方形的透视变换矩阵时,先求单位方形到四边形A转换的矩阵FFF,那么H1=F−1H_{1} = F^{-1}H1=F−1,利用矩阵公式F∗=F−1∗∣F∣F^* = F^{-1}*|F|F∗=F−1∗∣F∣,推得H1=F∗∣F∣H_{1} = \frac{F^*}{|F|}H1=∣F∣F∗。
我们再来回顾一下,已知四边形A到四边形B的透视变换矩阵HHH,求四边形A中某点(x,yx, yx,y)在四边形B中对应的点(x′,y′x^{'}, y^{'}x′,y′):
x′=C11∗x+C21∗y+C31C13∗x+C23∗y+C33;x^{'} = \frac{C_{11} * x + C_{21} * y + C_{31}}{C_{13} * x + C_{23} * y + C_{33}};x′=C13∗x+C23∗y+C33C11∗x+C21∗y+C31;y′=C12∗x+C22∗y+C32C13∗x+C23∗y+C33 y^{'} = \frac{C_{12} * x + C_{22} * y + C_{32}}{C_{13} * x + C_{23} * y + C_{33}}y′=C13∗x+C23∗y+C33C12∗x+C22∗y+C32
透视变换矩阵HHH乘以非0系数对以上对应点的推导没有影响。
那么,简单起见,我们就令H1=F∗H_{1} = F^*H1=F∗。
综上所述就是对小魏博客中透视变换部分的推导与分析。
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 }