點擊上方"藍色小字"關注我哦~
上面這副圖就是我們今天要處理的了,我們想把它從拍照視角變成鳥瞰圖,這是機器人導航中的常用手段,以便在該平面上進行規劃和導航。
這種變換常常用到透視變換,但我們今天在講解透視變換時,需要普及一下其他的變換,包括平移,旋轉,錯切,放縮,以及仿射變換。
綜述
所有複雜的東西,都是由基本的組成的。所以我們需要先了解一下基礎的變換有哪些:
平移
我們對矩形(圖像)平移,需要怎麼做?
對每一個像素點坐標平移。可以讓每一個像素點的x,y坐標都加一個變量。
矩陣形式表示:
等式左邊[X,Y,1]是像素坐標的齊次形式。等式右邊是平移之後的坐標。
放縮
進行放縮,就是將矩形(圖像)放縮n倍,也就是長寬各乘一個變量。
旋轉
對矩形(圖片)進行旋轉,關於旋轉的數學推導在後面仿射會介紹:
錯切
前面的都比較直觀,那錯切是什麼呢?
我們可以看下矩形關於y方向的錯切:
看圖就很直觀了,那數學表達呢?
x軸上的錯切就是同理了,公式如下:
然後兩者和起來,就如下了:
好了,到此我們就了解了這四種變換了,那仿射變換是什麼呢?可以看下圖公式:
等式右邊就是仿射變換矩陣,是由原圖像平移,旋轉,放縮,錯切之後得來的。
在書上往往將仿射變換和透視變換放一起講,這兩者各是什麼呢?
在剛學仿射變換和透視變換時,我是有些分不清的。印象最深刻的就是下圖:
可以看到,仿射變換(下)是將矩形變換成平行四邊形(即變換後各邊依舊平行),而透視變換(上)可以變換成任意不規則四邊形。這樣看來,好像仿射變換是透視變換的子集。
那到底是不是呢?其實是的。仿射變換屬於線性變換,而透視變換則不僅僅是線性變換。仿射變換可以看做是透視變換的一種特例。
直觀上感受,我們可以認為:
仿射變換是單純對圖片進行縮放,傾斜和旋轉,因此圖片不論如何變化,線之間的平行性是不變的。如下圖。
可以感受到,右圖是可以通過左圖平移,旋轉,錯切,縮放之後得來。
而透視變換,則是當觀察者的視角發生變化時物體發生的透視變換,此轉換允許造成透視形變。
我們看下圖的公路,近處寬遠處窄,就是因為視角的原因,
而我們本文要做的,就是將視角改為鳥瞰,從而得到類似下圖的鳥瞰圖:
前文已經說了,仿射變換是單純對圖片進行平移,縮放,傾斜和旋轉,而這幾個操作都不會改變圖片線之間的平行關係。
opencv中給出了仿射變換的函數接口:
warpAffine( InputArray src, 輸入圖像 OutputArray dst, 輸出圖像 InputArray M, 仿射計算矩陣 Size dsize, 輸出圖像大小 int flags = INIET_LINEAR, 插值方法 int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar() );這個函數很好理解,輸入一個圖像,輸出這個圖像的仿射變換。
但第三個參數需要我們輸入2*3的仿射計算矩陣,這是什麼鬼?
我們先看一下仿射計算矩陣長什麼樣子(可以去掉最後一行):
我們的輸出圖像G(x,y) = F(x,y)乘仿射矩陣。
我們可以看下圖推導出仿射計算矩陣。
一個點P在原始坐標系下的坐標是(Xsp,Ysp)。然後要完成旋轉操作,旋轉操作是基於原點的。如何得到旋轉之後的點的坐標,這裡用到一個技巧:
坐標系中某個點的旋轉可以等價地去旋轉坐標軸。
所以有了上圖中以(Xs0,Ys0)為中心的虛線與屏幕水平垂直的坐標系。在這個坐標系中確定P的坐標,和在藍色坐標系中確定旋轉之後P的坐標是等價的。
基於這個結論,我們可以通過簡單的立體幾何知識確定P在新坐標系中的坐標。P在新坐標系中的X坐標和Y坐標分別是
進而我們可以得到:
到此,我們完成了旋轉操作,如何平移呢?僅是加一個平移常數的事:
到此,我們的2*3大小的仿射變換便推導出來了。
推導知道了,但如何實現呢?
opencv同樣給我們提供了計算仿射矩陣的函數接口:
getAffineTransform( const Point2f* src, 輸入圖像的點集 const Point2f* dst 輸出圖像的點集 );這個函數可以計算出我們想要圖像變換的矩陣,但需要我們輸入至少三對點集,點集是什麼鬼?為什麼是至少三對?
我們可以看到上面公式裡有六個變量,因此自然需要至少列六個等式才可計算出該矩陣。
因此我們需要找輸入圖像和輸出圖像上一一對應的三對點(3個x,y對應計算式)來作為輸入。
這樣,我們就可以進行仿射變換啦。
透視變換原理
我們說仿射變換是在二維空間中的旋轉,平移和縮放。而透視變換則是在三維空間中視角的變化。
opencv中同樣給出了透視變換的函數接口:
void warpPerspective(InputArray src, 輸入圖像OutputArray dst, 輸出圖像InputArray M, 輸入透視變換矩陣MSize dsize, int flags=INTER_LINEAR, int borderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar());和仿射變換基本相同,不同的是輸入透視變換矩陣M大小為3*3:
上面矩陣的未知量比仿射變換的矩陣多了一個透視變換矩陣T3(兩個未知量),因此我們需要給下面計算透視變換矩陣的函數提供四對以上的點來求解:
Mat cv::getPerspectiveTransform ( const Point2f src[], 輸入圖像點集 const Point2f dst[], 輸出圖像點集 );T1為線性變換完成旋轉,錯切和放縮,T2完成平移操作。T3就是設了兩個變量來表示映射關係。
編程實現
理解了透視變換的原理後,我們就著手來實現了(代碼可以順次複製即可運行):
首先是讀取原圖片並顯示啦:
#include <opencv2/opencv.hpp>#include <iostream>using namespace std;using namespace cv;int main(){ Mat dstImage,srcImage = imread("road.png"); cout<<srcImage.size; //674 x 1020 imshow("原圖", srcImage); waitKey(); return 0;}
然後我們需要選取原圖上的四個點,並計算出該四對點變換後的位置。
如何選點?我們可以選兩邊白條的四個定點。那變換後的位置就需要我們自己估算了,如下圖:
我們希望將藍色的透視變換為黃色的。
Point2f imgPts[4], objPts[4]; //透視前和透視後 //原坐標 imgPts[0].x = 20 * 1020 / 230; imgPts[0].y = 95 * 647 / 145; imgPts[1].x = 210 * 1020 / 230; imgPts[1].y = 95 * 647 / 145; imgPts[2].x = 90 * 1020 / 230; imgPts[2].y = 65 * 647 / 145; imgPts[3].x = 140 * 1020 / 230; imgPts[3].y = 65 * 647 / 145; //透視後坐標 int road_w = 540; //將透視變換的圖片大小改變一下 int road_h = 850; objPts[0].x = 50; objPts[0].y = 780; objPts[1].x = 490; objPts[1].y = 780; objPts[2].x = 50 ; objPts[2].y = 150; objPts[3].x = 490; objPts[3].y = 150;
我們選取了如圖四個點,首先計算透視變換矩陣:
//計算透視變換矩陣Mat H = getPerspectiveTransform(imgPts, objPts);然後進行透視變換:
//進行透視變換warpPerspective(srcImage, dstImage, H, srcImage.size());//畫出透視變換後的四個點circle(dstImage, objPts[0], 9, Scalar(0, 0, 255), 3);circle(dstImage, objPts[1], 9, Scalar(0, 0, 255), 3);circle(dstImage, objPts[2], 9, Scalar(0, 0, 255), 3);circle(dstImage, objPts[3], 9, Scalar(0, 0, 255), 3);imshow("變換後", dstImage);
這樣,我們就得到鳥瞰圖啦。
本文中的部分公式截圖來自下面視頻的PPT:
這個視頻也是介紹仿射變換和透視變換的,大家可以和本文對比著看。
本文如有表述錯誤的地方,還望批評指正!
【opencv實踐】圖像增強基本操作
【opencv】帶你再學一遍直方圖
【opencv小項目】深入理解回調函數
點個在看,就可以評論啦~