三維重構(6):雙目立體視覺梳理(主要與SFM對比理解)

三維重構學習筆記(6):雙目立體視覺梳理

前話可跳:
學習筆記(6)之前的筆記都是基於單目的,比如SFM相關的。通過單目相機從不同的角度採集圖片,然後通過增量式的SFM對每一張圖片採集圖片等。在做實驗的過程中出現以下問題:

  • 特徵點計算耗時
  • 增量式的SFM隨着圖片數量的增加,誤差增加
  • 匹配點對較少,有時計算本徵矩陣都會遇到麻煩(比如特徵點太少,無法計算出本徵矩陣,那麼後續的圖片就更計算不出來了)
  • 點雲稀疏,應用場景欠缺
  • 想重構特定位置的物體,但是無法確定如何圈定該位置的特徵點等
    ……
    總之,上述問題的出現讓我一度懷疑“不小心路又雙叒叕走歪了”。感覺與我想要的應用場景不是很匹配,單是運算速度這一點就使我獻出膝蓋了。所以我決定試試雙目立體視覺,看看它與我第一接觸的SFM有啥異同。雖然我期望雙目提高速度的願望並沒有實現,但是,對於我理解立體視覺具有很好的促進,所以我決定記錄下來。
    參考博客:
    雙目實現流程
    由視差圖和對應圖片顯示3D點雲

雙目立體視覺不同於SFM的流程

雙目立體視覺與SFM都是基於實際物體通過相機拍攝到圖片時產生的仿射變換這同一個原理工作的。相機標定過程就是求解這個仿射變換的過程,三維重構就是通過這個變換矩陣+一對匹配點求解匹配點在三維座標中的位置。
雙目流程:

  • 標定相機
  • 圖片立體校正
  • 計算視差圖
  • 計算深度圖
  • 獲得3D座標

雙目與SFM的不同點在於:

  • 增量式SFM後續添加的圖片都需要計算出本徵矩陣進而求解該圖片對應的R,TR,T,而一般雙目會通過標定直接確定兩個相機之間的R,TR,T
  • 計算(特徵點,匹配點對)的方式不太一樣:我見到的雙目立體視覺大都是對圖片進行立體校正,然後計算兩張圖片之間的視差,通過視差可以計算像素點的深度;而增量式SFM通過特徵點提取與匹配,計算R,TR,T,然後直接進行三角測量計算匹配點對的三維座標

我認爲雙目的視差計算更像是一種羣體匹配;而SFM通過特徵點的匹配則更加具有針對性,且更加準確
更細節的區別我覺得需要研究清楚雙目的視差算法纔好說,這一點暫時放在後面的學習中。

雙目算法中獲得視差的方法(OpenCV)

只要獲得了比較好的視差,雙目立體重構就算是成功了。計算視差之前需要完成:相機標定、立體校正,然後纔是視差計算。上述過程都可以依賴於OpenCV。

雙目標定

OpenCV標定和MATLAB的工具箱標定都可以,標定結果稍有不同,根據極線約束誤差進行判斷標定好壞。我的相機分辨率480*640(使用的淘寶上最便宜的CHUSEI雙目相機),OpenCV的極線約束誤差大概在0.2個像素。但是用OpenCV獲得的標定結果進行立體校正效果不是很好,所以我用的MATLAB的標定參數。

立體校正

立體校正表示將兩張圖片投影到同一個平面上,目的是爲了降低立體視覺的計算量。校正平面的掃描線與外極線相同,平行於基線,且通過再投影點。(外極線一般是基線與圖像的交點+光線與圖像的交點共同構成的線,校正之後的平面平行於基線,則光線與再投影平面的交點構成外極線,同時這條線是校正平面的掃描線)。如下圖:
在這裏插入圖片描述

stereoRectify(cameraMatrixL, distCoeffL, cameraMatrixR, distCoeffR, 
		imageSize, R, T, Rl, Rr, Pl, Pr, Q, CALIB_ZERO_DISPARITY,
		0, imageSize, &validROIL, &validROIR);

函數原型

CV_EXPORTS_W void stereoRectify( InputArray cameraMatrix1, InputArray distCoeffs1,
                                 InputArray cameraMatrix2, InputArray distCoeffs2,
                                 Size imageSize, InputArray R, InputArray T,
                                 OutputArray R1, OutputArray R2,
                                 OutputArray P1, OutputArray P2,
                                 OutputArray Q, int flags = CALIB_ZERO_DISPARITY,
                                 double alpha = -1, Size newImageSize = Size(),
                                 CV_OUT Rect* validPixROI1 = 0, CV_OUT Rect* validPixROI2 = 0 );

輸入左右相機內參數、畸變參數、圖片尺寸以及右相機相對於左相機的旋轉矩陣RR、平移向量TT,輸出兩個相機的校正變換(rotation matrix)R1,R2R_1,R_2、校正座標系中兩個相機的的仿射投影變換P1,p2P_1,p_2、以及視差轉化爲深度的計算需要用的仿射矩陣QQ。其中QQ主要用於函數reprojectImageTo3D()。最後兩個輸出,用於記錄圖片有效區域,其輸出受限於參數double alpha,若alpha爲0,則只顯示有效部分,若是1,則顯示全部圖片,也可以取值在0-1之間,其效果在0和1之間。

得到上述矯正之後的各個變換矩陣之後,利用initUndistortRectifyMap函數計算畸變矯正和立體校正的映射變換。

initUndistortRectifyMap(cameraMatrixL, distCoeffL, Rl, Pl(Rect(0, 0, 3, 3)), imageSize, CV_32FC1, mapLx, mapLy);
initUndistortRectifyMap(cameraMatrixR, distCoeffR, Rr, Pr(Rect(0, 0, 3, 3)), imageSize, CV_32FC1, mapRx, mapRy);

函數原型

CV_EXPORTS_W void initUndistortRectifyMap( InputArray cameraMatrix, InputArray distCoeffs,
                           InputArray R, InputArray newCameraMatrix,
                           Size size, int m1type, OutputArray map1, OutputArray map2 );

這個函數的目的是:計算沒有畸變和校正的轉換圖,也就是以remap的形式計算“聯合的無畸變校正”的圖。無畸變的圖片就像是從一個內參爲newCameraMatrix的且無畸變的新相機採集的原圖;新相機與原相機的空間座標系稍有不同,因爲在立體校正之後,左右兩個相機採集到的圖片處於同一個平面,所以相機的RR與原相機是不同的(理解是否正確有待接下來的學習)。這個函數實際上建立了用於remap的map1,map2。這兩個map用於將原始圖片向目標圖片進行投影
得到上述的投影map之後,使用remap函數即可將原圖轉化成“無畸變+校正的圖片”:

remap(ImageL, rectifyImageL, mapLx, mapLy, INTER_LINEAR);
remap(ImageR, rectifyImageR, mapRx, mapRy, INTER_LINEAR);

其中ImageL,ImageR表示原圖,輸入可以是RGB圖也可是灰度圖。
查看立體校正的結果可以通過並排畫出rectifyImageL,rectifyImageR進行查看:

		Mat canvas;
		int w = imageSize.width ;
		int h = imageSize.height;
		canvas.create(h, w * 2, CV_8UC1);   //通道
											//左圖
		Mat canvasPart = canvas(Rect(w * 0, 0, w, h));                                //得到畫布的一部分  
		resize(rectifyImageL, canvasPart, canvasPart.size(), 0, 0, INTER_AREA);     //把圖像縮放到跟canvasPart一樣大小 ,此時左圖已經保存在canvas左半邊
		Rect vroiL(cvRound(validROIL.x), cvRound(validROIL.y),   
			cvRound(validROIL.width), cvRound(validROIL.height));
		//右圖像畫到畫布上
		canvasPart = canvas(Rect(w, 0, w, h));                                      //獲得另一半  
		resize(rectifyImageR, canvasPart, canvasPart.size(), 0, 0, INTER_LINEAR);
		//右保存在canvas右半邊
		Rect vroiR(cvRound(validROIR.x * sf), cvRound(validROIR.y*sf),
		cvRound(validROIR.width * sf), cvRound(validROIR.height * sf));
	

		//畫上線條
		for (int i = 0; i < canvas.rows; i += 16)
		line(canvas, Point(0, i), Point(canvas.cols, i), Scalar(0, 255, 0), 1, 8);
		imshow("rectified", canvas);

我的結果如下:
在這裏插入圖片描述

計算視差

通過remap之後,我們得到了“位於同一個平面的無畸變+校正”的兩張圖片rectifyImageL,rectifyImageR。此時可以通過專門計算視差的函數計算兩圖的視差。計算視差的函數有很多種,SAD,BM,SGBM,GC等。參考網上對於上述算法的分析,加上目前我用的OpenCV沒有實現GC算法,所以我主要使用了SGBM算法。SAD算法、BM算法計算較快,但是結果不理想,SGBM據說是比SAD、BM的效果好,但是計算速度較慢。我也嘗試了SAD算法,結果卻是不理想,計算出來的視差雜亂無比。需要說明的是,我就算是使用SGBM算法計算的視差也達不到我的要求。導致這一現象的原因有兩個:相機標定不夠準確+算法參數不合適。計算視差的過程不是一帆風順,結果差強人意。

OpenCV3提供了SGBM算法接口
首先定義一個SGBM算法對象(輸入默認參數)

cv::Ptr<cv::StereoSGBM> sgbm = StereoSGBM::create(0, 16, 3);

然後設置參數:下圖式計算左視差的參數

	int numberOfDisparities = 200 & -16;	///視差搜索範圍是16的倍數
	int numDisparities = 6;
	sgbm->setPreFilterCap(63);				//映射濾波器的大小
	int SADWindowSize = 9;//SAD窗口大小
	int sgbmWinSize = SADWindowSize > 0 ? SADWindowSize : 3;
	sgbm->setBlockSize(sgbmWinSize);
	int cn = rectifyImageL.channels();
	sgbm->setP1(8 * cn*sgbmWinSize*sgbmWinSize);//懲罰係數
	sgbm->setP2(32 * cn*sgbmWinSize*sgbmWinSize);//懲罰係數
	sgbm->setMinDisparity(0);//最小視差,默認是0
	sgbm->setNumDisparities(numberOfDisparities);
	sgbm->setUniquenessRatio(10);
	sgbm->setSpeckleWindowSize(100);
	sgbm->setSpeckleRange(32);
	sgbm->setDisp12MaxDiff(1);
	sgbm->setMode(cv::StereoSGBM::MODE_SGBM);

參數設置好之後,調用compute計算視差

sgbm->compute(rectifyImageL,rectifyImageR , disp);//輸入圖像必須爲灰度圖

此時左圖的視差圖就保存於disp變量。根據OpenCV的官方文檔,disp 中的視差被放大了16倍,所以爲了得到準確的視差,應該把disp中的所有元素都除以16,就能夠得到準確的視差圖。

Mat disp8;
disp.convertTo(disp8, disp.depth(), 1.0 / 16 );

計算深度

得到視差圖之後,有兩種計算方式得到三維座標
可以使用OpenCV的官方函數reprojectImageTo3D

	cv::reprojectImageTo3D(disp, xyz, Q, true);

得到三維座標xyz。注意如果這裏使用的disp是由sgbm->compute出來的,那麼disp被放大16倍,所以計算出來的xyz的座標被縮小了16倍,因此xyz*16之後纔得到實際的空間座標。

在這裏插入圖片描述

可以根據視差圖自行計算
計算思路爲根據視差計算出深度,然後根據三角幾何關係計算出對應的x,y座標,注意下面的參數中doffs表示兩個相機內參數中u0的差

int rowNumber = rectifyImageL.rows;
int colNumber = rectifyImageL.cols;
param_3D.cloud_a.height = rowNumber;
param_3D.cloud_a.width = colNumber;
param_3D.cloud_a.points.resize(colNumber* rowNumber);
for (unsigned int u = 0; u < rowNumber; ++u)
	{
		for (unsigned int v = 0; v < colNumber; ++v)
		{
		double Xw = 0, Yw = 0, Zw = 0;
		Zw = fx * Tx / (((double)disp8.at<Vec3b>(u, v)[0]) + doffs);
		if (Zw > 1000)
		Zw = 0;
		Xw = (v + 1 - u0) * Zw / fx;
		Yw = (u + 1 - v0) * Zw / fy;
		param_3D.cloud_a.points[num].x = Xw;
		param_3D.cloud_a.points[num].y = Yw;
		param_3D.cloud_a.points[num].z = Zw;
		}
}

結果如下:
在這裏插入圖片描述

原圖是:
在這裏插入圖片描述
到這裏一個粗略的雙目立體視覺計算深度的流程就結束了,後面的工作就是對上述過程進行各種優化。

注意點

視差圖填充,視差圖格式,深度範圍限制(防止出現inf出現,導致視圖顯示不完整),視差圖轉色溫圖(爲了顯示好看)
另外,在保存視差圖的時候發現:視差圖的精度會受格式的影響,有的博主說可以將視差圖同意保存xml類型的數據。
還有SGBM算法是怎麼工作的?如何計算得到視差的,函數可以隨意調用,但是參數設置應該和原理有關,因此還是需要細緻學習一下
重構出的點那麼醜,有一些點看起來不屬於重構物體或者誤差看起來很大,考慮刪除等等……
這些注意點內容在下一篇細緻探討,理解了纔會用。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章