開篇注:博客是為了更好的思考,希望能以此記錄自己的學習歷程。隨着時間流逝,可能有些內容已經失效,望讀者望文觀義,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 }