最近在做雙目測距,覺得有必要記錄點東西,所以我的第一篇博客就這么誕生啦~
雙目測距屬於立體視覺這一塊,我覺得應該有很多人踩過這個坑了,但網上的資料依舊是雲里霧里的,要么是理論講一大堆,最后發現還不知道怎么做,要么就是直接代碼一貼,讓你懵逼。 所以今天我想做的,是盡量給大家一個明確的闡述,並且能夠上手做出來。
一、 標定
首先我們要對攝像頭做標定,具體的公式推導在learning opencv中有詳細的解釋,這里順帶提一句,這本書雖然確實老,但有些理論、算法類的東西里面還是講的很不錯的,必要的時候可以去看看。
Q1:為什么要做攝像頭標定?
A: 標定的目的是為了消除畸變以及得到內外參數矩陣,內參數矩陣可以理解為焦距相關,它是一個從平面到像素的轉換,焦距不變它就不變,所以確定以后就可以重復使用,而外參數矩陣反映的是攝像機坐標系與世界坐標系的轉換,至於畸變參數,一般也包含在內參數矩陣中。從作用上來看,內參數矩陣是為了得到鏡頭的信息,並消除畸變,使得到的圖像更為准確,外參數矩陣是為了得到相機相對於世界坐標的聯系,是為了最終的測距。
ps1:關於畸變,大家可以看到自己攝像頭的拍攝的畫面,在看矩形物體的時候,邊角處會有明顯的畸變現象,而矯正的目的就是修復這個。
ps2:我們知道雙目測距的時候兩個相機需要平行放置,但事實上這個是很難做到的,所以就需要立體校正得到兩個相機之間的旋轉平移矩陣,也就是外參數矩陣。
Q2:如何做攝像頭的標定?
A:這里可以直接用opencv里面的sample,在opencv/sources/sample/cpp里面,有個calibration.cpp的文件,這是單目的標定,是可以直接編譯使用的,這里要注意幾點:
1.棋盤
棋盤也就是標定板是要預先打印好的,你打印的棋盤的樣式決定了后面參數的填寫,具體要求也不是很嚴謹,清晰能用就行。之所用棋盤是因為他檢測角點很方便,and..你沒得選。。
2. 參數
一般設置為這個樣子:-w 6 -h 8 -s 2 -n 10 -o camera.yml -op -oe [<list_of_views.txt>] ,這是幾個重要參數的含義:
-w <board_width> # 圖片某一維方向上的交點個數 -h <board_height> # 圖片另一維上的交點個數 [-n <number_of_frames>] # 標定用的圖片幀數
[-s <square_size>] # square size in some user-defined units (1 by default) [-o <out_camera_params>] # the output filename for intrinsic [and extrinsic] parameters [-op] # write detected feature points [-oe] # write extrinsic parameters
可以發現 -w -h是棋盤的長和高,也就是有幾個黑白交點,-s是每個格子的長度,單位是cm
長和高一定要數對,不然程序在識別角點的時候會識別不出來的。
最終得到的yml文件,就是單目標定的參數矩陣,之后使用它就可以得到校正后的圖像啦。
3. 需要對程序做一些修改,這是我遇到的問題,就是他的讀取攝像頭的代碼在我這邊沒有用,所以我自己重新修改了,不知道大家會 不會碰到這個問題。
然后就是雙目標定了,同樣的地方,找到stereo_calib.cpp,這個參數比較簡單,只要確定長、寬和輸入的一個xml文件(在之前 的文件夾里面),這個文件是為了讀取圖片用的,你需要自己用固定好的雙目攝像頭拍14對棋盤圖片,命名為 left01,right01......這樣 一系列的名字,另外,最簡單的方法就是把自己拍的照片放到相應的工程下,以及stereo開頭的那個xml文件也復制過去這個程序代碼 並不復雜,可以稍微研究一下,工程向的代碼確實嚴謹,各種情況都考慮到了,比起自己之前做的那個小項目不知道高到哪里去了
這里也有幾個注意點(坑):
1.老生常談的問題,長寬一定要寫對!!! 這個不多說了,都是淚。
2.代碼的核心函數 static void StereoCalib(const vector<string>& imagelist, Size boardSize, bool useCalibrated=true, bool showRectified=true),注意搞清楚參數的意義,因為我是用的單目標定好的攝像頭拍攝的圖片,不需要再校正了,所以第三個參數要用false,這樣最后的結果才能看,不說了,都是淚...
3.另外注意到計算rms誤差的時候,結束條件的幾個參數是可以調整的,
double rms = stereoCalibrate(objectPoints, imagePoints[0], imagePoints[1],
cameraMatrix[0], distCoeffs[0],
cameraMatrix[1], distCoeffs[1],
imageSize, R, T, E, F,
TermCriteria(CV_TERMCRIT_ITER+CV_TERMCRIT_EPS, 100, 1e-5),
CV_CALIB_FIX_ASPECT_RATIO +CV_CALIB_ZERO_TANGENT_DIST +CV_CALIB_SAME_FOCAL_LENGTH +CV_CALIB_RATIONAL_MODEL +CV_CALIB_FIX_K3 + CV_CALIB_FIX_K4 + CV_CALIB_FIX_K5)
下面這段話是某度百科上的:
void loadCameraParams(Mat &cameraMatrix, Mat &distCoeffs)
{
FileStorage fs("camera.yml", FileStorage::READ);//這個名字就是你之前校正得到的yml文件
fs["camera_matrix"] >> cameraMatrix;
fs["distortion_coefficients"] >> distCoeffs;
}
Mat calibrator(Mat &view)//需要校正處理的圖片
{
vector<string> imageList;
static bool bLoadCameraParams = false;
static Mat cameraMatrix, distCoeffs, map1, map2;
Mat rview;
Size imageSize, newImageSize;
if (!view.data)
return Mat();
imageSize.width = view.cols;
imageSize.height = view.rows;
newImageSize.width = imageSize.width;
newImageSize.height = imageSize.height;
if (bLoadCameraParams == false)
{
loadCameraParams(cameraMatrix, distCoeffs);
bLoadCameraParams = true;
initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(),
getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, newImageSize, 0), newImageSize, CV_16SC2, map1, map2);
}
//undistort( view, rview, cameraMatrix, distCoeffs, cameraMatrix );
remap(view, rview, map1, map2, INTER_LINEAR);
imshow("左圖", rview);
//int c = 0xff & waitKey();
rview.copyTo(view);
return view;
}
這樣最后就可以得到校正后消除畸變的圖片。
OK,接下來顯然就是雙目啦,雙目校正之后的工作就比較多了,我准備另開一節來說...
二、立體匹配
這是一個很大的題目,網上的資料也很多,所以我想說的是我的一些理解。
這里最好的方法是從后往前說,我們首先需要理解測距的原理。這個很多人看了一大堆還不明白(其實只有我自己吧..),相似三角形測距,這種東西小學生都能搞清楚,但兩攝像頭到底怎么做到的,就是我們需要搞清楚的。
首先需要搞清楚一個非常重要的概念,視差,搞清楚視差,后面的就簡單了 ,老生常談的問題我不想多說,網上那些一大堆,我希望給大家的是一些明了的東西
這三幅圖看明白了就行,其實視差確實很簡單,但很多人都沒去理清楚,第一幅圖是三維世界的一個點在兩個相機的成像,我們可以相信的是,這兩個在各自相機的相對位置基本不可能是一樣的,而這種位置的差別,也正是我們眼睛區別3D和2D的關鍵,將右邊的攝像機投影到左邊,怎么做呢?因為他的坐標和左邊相機的左邊相距Tx(標定測出來的外參數),所以它相當於在左邊的相機對三維世界內的(x-tx,y,z)進行投影,所以這時候,一個完美的形似三角形就出來,這里視差就是d=x-x‘,
得到視差以后,再用相似三角形......也就得到了深度也就是距離啦。
結束了么??並沒有....這樣做確實很完美,但是問題來了:1.當我在左邊相機確定一個點的時候,我怎么在右邊找到這個點? 2.我左邊點所在的行一點跟右邊點所在的行上的像素一定完全一樣么?
解決第一個問題的方法就是立體匹配了。
Q1:立體匹配是什么,怎么進行立體匹配?
A:簡單的回答就是:立體匹配就是解決上面問題的東西啦....其實我覺得這樣就是也夠了,有些成熟的算法,未必需要鑽研太深,畢竟我這種實在的菜雞,還是工程導向的..學術的事,日后再說!
opencv中提供了很多的立體匹配算法,類似於局部的BM,全局的SGBM等等,這些算法的比較大概是,速度越快的效果越差,如果不是很追究時效性,並且你的校正做的不是很好的話..推薦使用SGBM,算法的具體原理大家可以去百度,不難。這里我想提一下的是為什么做立體匹配有用,原因就是極線約束,這也是個很重要的概念,理解起來並不難,左攝像機上的一個點,對應三維空間上的一個點,當我們要找這個點在右邊的投影點時,有必要把這個圖像都遍歷一邊么,當然不用...
如上圖,顯然,PL對應的P這個點一定在一條極線上,只要在這條線上找就行了,更明顯的是下面這個圖:
最后,怎么在opencv里面實現呢..機智的我又找到了sample..找到stereo_match.cpp這個文件,命令行設置為:left01.jpg right02.jpg --algorithm=hh --blocksize=5 --max-disparity=256 --scale=1.0 --no-display -i intrinsics.yml -e extrinsics.yml -o disparity.jpg同意給幾個建議:
1.參數的意義:
-max-disparity 是最大視差,可以理解為對比度,越大整個視差的range也就越大,這個要求是16的倍數
--blocksize 一般設置為5-29之間的奇數,應該是局部算法的窗口大小。
另,注意帶上參數-i intrinsics.yml -e extrinsics.yml,畢竟咱有校正參數...
2.后面有兩行代碼:
reprojectImageTo3D(disp, xyz, Q, true);
saveXYZ(point_cloud_filename, xyz);
這個就是得到圖片的三維坐標,Z也就是我們最終要求的深度啦。
第二個問題,行和行是對應的么? 之前我們說過,雙目校正的目的就是為了得到兩個平行的攝像頭,所以當程序運行完畢以后,它會把兩幅圖像顯示出來,並作出一系列的平行線,這樣你會看到線上的點大致是呈對應關系,左邊的角點對應右邊的交點,所以,經過匹配和校正后,是對應的。
三、總結
雙目拖了很久,一直沒做,最重要的原因就是...我沒有兩個一樣的攝像頭,所以最后也沒有貼出效果圖,因為兩個不一樣的攝像頭,做出來的東西畫面太美我不敢看,不過最終搞清楚了整個流程和原理,還是比較開心的。這里面像校正和匹配的算法,我只是有所理解,因為以后不一定走3D這一塊,所以也沒有過去深入,如果用到在去研究,其實也不晚..總之,第一篇博客,完工啦~