注:這是我在知乎寫的文章,現搬運至此。原文鏈接:https://zhuanlan.zhihu.com/p/51363371
本文實現的是兩幀間pnp問題的BA求解。為了實現GPU上的BA,對BA過程的透徹理解必不可少,而兩幀間BA優化正是大規模后端優化的基礎.為方便期間,本文求解使用高斯牛頓法。需要額外指出的是,本文並未利H矩陣的稀疏性,直接使用Eigen自帶的最小二乘求解。
本代碼編寫於2018年,如有疏漏,歡迎批評指正。本文代碼地址:https://link.zhihu.com/?target=https%3A//github.com/LiuSQ123/pnp_by_BA
0.PNP和BA簡介
PNP(perspective-n-point)是通過一組匹配好的3D點和2D點來求解兩幀圖像之間運動的一種算法。PNP的求解有DLT(直接線性變換)、P3P、EPNP和BA優化等方式。而BA優化是SLAM中的最核心算法,通過BA求解PNP和SLAM系統后端優化中的BA原理相同。其差別只在於,PNP問題僅僅包含兩幀圖像的位姿,而后端中的BA優化則包含多個圖像的位姿。
BA(bundle adjustment)指的是同時調整相機姿態和特征點位置,以便從每個特征點反射出的光線(bundles of light rays),通過調整(adjustment)最后都能通過相機光心。故也有人翻譯為光束平差法。BA通常構建為一個最小二乘問題,通過使重投影誤差最小化來同時調整相機的位姿和特征點的坐標。
首先解釋什么是重投影誤差?分為兩步來解釋:
1.重投影: 重投影顧名思義就是把 3d空間中的點重新投影到圖像平面上。用數學公式表達就是相機的成像過程,即:
其中, 表示相機的成像過程,
為相機在世界坐標系下的位姿(李代數表示),
為3d點在世界坐標系下的坐標,
為該3d點在圖像上像素坐標的理論值。一般情況下,姿態
和點坐標
都為優化變量(優化變量的意思是,在優化開始前我們對
的值只有一個猜測值,而我們要求的正是它們的准確值)。當然,你也可以只優化
或者
,甚至你還可以只優化位姿中的旋轉
而固定位移
,這一點會在下文中說明。
2.誤差:上面說到, 和
都為優化變量,在BA開始之前我們對它無從知曉(正如上文所說,我們對優化變量只有一個猜測值,一般是根據先驗或者傳感器直接測量取得。值得一提的是,針對於BA求解PNP問題,這個初值並不能隨便給定,否則可能會陷入局部極小。而在已知匹配的ICP問題中,初始值可以任意給定,算法總會收斂到全局極小)。那么對應到重投影的公式,再加上噪聲和傳感器誤差的影響,我們根據公式(1)求得的3d點理論像素坐標和我們的觀測到的像素坐標必然不同,而這個理論值和觀測值的差就是重投影誤差,我們定義重投影誤差為 :
其中, 為觀測值(即相機圖像上特征點的像素坐標),
為理論值(或者說是我們的預測值)。針對於
我們可以構建最小二乘問題,通過調整
來使重投影誤差的平方和(因為誤差有正有負,所以取平方) 最小化。我們需要做的就是求解出誤差平方和的梯度,然后按梯度下降方向迭代,直至收斂。BA優化的原理就是這么簡單!
1.求解BA
求解最小二乘問題有很多種方式(我相信大家都知道!不知道的請翻十四講p109),如一階和二階梯度下降法,G-N法,L-M法,狗腿(DogLeg)法等。其中G-N法雖然有很多缺點,例如使用 近似的
矩陣只能保持半正定性,以及由此帶來的算法不穩定,但是其編程實現簡單,且優於一,二階梯度法,故本文使用G-N法求解BA。
1.1 問題簡介
我們把第一幀固定,設定為世界坐標系原點(即該幀相機的旋轉為單位矩陣 ,位移
),同時優化3D點的空間位置和第二幀的姿態。針對於兩幀之間的BA,我們要求解目標函數:
其中, 為我們優化位姿的李代數表示(因為我們要求解的是兩幀之間的PNP問題,並且我們固定了第一幀的姿態,所以只有一個
無下標),
為第一幀和第二幀共同觀測到的特征點坐標,我們假設在兩幀圖片上共匹配了
個特征點。
使用李代數 (3)表示姿態的原因是,只有單位正交陣才能表示旋轉,如果使用 矩陣
,那么就會構建出一個帶有約束的優化問題。而通過李群-李代數的轉換關系,正好可以把BA構建成無約束優化,簡化求解。
我們再設定待估計的狀態向量為:
其中 為李代數表示的相機位姿 ,
為兩幀間匹配的3D點坐標,一共m個。我們記單個誤差項為(注意和上文的
進行區分,
為
的平方,原因可參考<14講>高斯牛頓法):
可以看出來,一個特征點每被觀測一次,會產生一個這樣的2維誤差。我們再記整體誤差為 ,那么
的表達式就為:
因為每個 都是二維的,故
為2*m維的向量,請牢記
的大小,下文中我們還會用到。這樣,我們整體的目標函數可以寫成:
對應到代碼中就是:
1 /*** 2 * @param x 狀態量 3 * @param v_Point2D 觀測到特征點的像素坐標 4 * @return f(x) 5 */ 6 Eigen::Matrix<double ,Eigen::Dynamic,1> 7 findCostFunction(Eigen::MatrixXd x, std::vector<cv::Point2d> v_Point2D) 8 { 9 //e=u-K*T*P; u為圖像上的觀測坐標,K為相機內參,T為相機外參,P為3D點坐標; 10 double fx=camMatrix(0,0); 11 double fy=camMatrix(1,1); 12 double cx=camMatrix(0,2); 13 double cy=camMatrix(1,2); 14 Eigen::Matrix<double ,Eigen::Dynamic,1> ans; 15 16 int size_P=(int)(x.rows()-6)/3; 17 18 if(size_P!=v_Point2D.size()){ 19 std::cout<<"---ERROR---"<<endl; 20 return ans; 21 } 22 //把李代數轉化為矩陣 Pose為變換矩陣 23 Eigen::VectorXd v_temp(6); 24 v_temp=x.block(0,0,6,1); 25 Sophus::SE3 SE3_temp=Sophus::SE3::exp(v_temp); 26 Eigen::Matrix<double,4,4> Pose = SE3_temp.matrix(); 27 28 ans.resize(2*size_P,1); 29 ans.setZero(); 30 for(int i=0;i<size_P;i++){ 31 Eigen::Matrix<double ,4,1> Point; 32 Point(0,0)=x(6+i*3 ,0); 33 Point(1,0)=x(1+6+i*3,0); 34 Point(2,0)=x(2+6+i*3,0); 35 Point(3,0)=1.0; 36 //計算3D點在相機坐標系下的坐標 37 Eigen::Matrix<double ,4,1> cam_Point=Pose*Point; 38 double cam_x=cam_Point(0,0); //相機坐標喜下3D點的坐標 39 double cam_y=cam_Point(1,0); 40 double cam_z=cam_Point(2,0); 41 //計算e 42 ans(2*i, 0) =v_Point2D[i].x-((fx*cam_x)/cam_z)-cx; 43 ans(2*i+1,0) =v_Point2D[i].y-((fy*cam_y)/cam_z)-cy; 44 } 45 return ans; 46 }
1.2 G-N法
本文不再對G-N法做詳細的闡釋,請仔細閱讀 <十四講>第六章。也可參考<狀態估計>4.3.1,同時<狀態估計>p217-p219也闡述了G-N法的另外一種解釋。
無論使用什么方式求解最小二乘問題,歸根結底都要面對增量方程:
在G-N法中,增量方程具體表示為:
其中
要想迭代,首先需要求解出 的值,即每個優化變量的增量。可以看出來,無論
和
如何取值,增量方程的求解都是一個線性方程組問題。大家都知道,在SLAM系統里,增量方程中的
矩陣是一個稀疏矩陣,對其求解有着多種特殊方式,可以加速求解。如果我們想要編寫一個可以實時運行的BA系統,那就不可不考慮H矩陣的稀疏性。但是本文暫不考慮這么多,直接使用Eigen自帶的方程組求解算法求解。
1.3 雅克比矩陣J(x)的獲取
上文提到我們定義優化變量:
那么對應其中一個特征點 求解雅克比矩陣(詳見<十四講 p250頁>):
其中,我們記為
,記
為
(與《14講》p247頁記號相同)。 其中
為該點的重投影誤差,
為某個誤差項關於空間點位置
的導數,
為某個誤差項關於姿態擾動
的導數,至於為什么是關於姿態擾動的導數而不是關於姿態的導數,主要原因是求解
需要計算一個比較復雜的雅克比矩陣
(詳見<14講>p75、<狀態估計>p216)。為了避免計算這個矩陣
(其實也沒必要計算),我們使用擾動模型。如果對此還有疑惑,可以參考這篇文章的第二部分:
我們再來看 的形式,很容易看出來它是一個
維的矩陣。但是可要注意了,這只是一個誤差項
的雅克比,我們一共擁有
個
(因為在兩幀圖像上,我們擁有
個特征點匹配),故整體的
的維度為
維。
那么整體的雅克比矩陣 就為(其實就是把
按行排列,可以參考<14講>p251):
《14講》中P164頁推導了 和
的形式,我把
和
的具體表達形式寫在下面:
我把 分為了兩部分(注意第三列后的分割線),左邊是關於平移t的雅克比,右邊是關於旋轉矩陣R的雅克比,另外注意E表達式中的R編程的時候不要漏掉!
2.具體實施過程
好吧,既然我們取得了 ,那么我們現在只需要求解增量方程:
就可以求解出 了。按理說應該利用
矩陣的稀疏性來求解的,不過本次我們直接使用Eigen求解。
求解出 后,更新
之后再次進行迭代,直至收斂。我直接設置為迭代5次。:
1 for(int i=1;i<=MAX_LOOP;i++){ //循環求解BA 2 std::cout<<"\033[32m"<<"Doing BA Please wait......"<<std::endl; 3 double t = (double)cv::getTickCount(); //計時開始 4 Eigen::MatrixXd Jacobian=findWholeJacobian(x); //求解狀態x的Jacobian 5 Eigen::MatrixXd JacobianT=Jacobian.transpose(); //求解Jacobian 的轉置 6 Eigen::MatrixXd H=JacobianT*Jacobian; //求解H矩陣 7 //std::cout<<"H = "<<endl<<H<<endl; 8 Eigen::MatrixXd fx=findCostFunction(x,v_P2d); //求解f(x)在狀態x下的值 9 Eigen::VectorXd g=-1*JacobianT*fx; //求解g,相見<十四講>p247 10 //求解delt_x 11 //1.Using the SVD decomposition 12 //Eigen::MatrixXd delt_x=H.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(g); 13 //2.Using the QR decomposition 14 std::cout<<"Solving ......"<<"\033[37m"<<std::endl; 15 Eigen::MatrixXd delt_x=H.colPivHouseholderQr().solve(g); 16 ///李代數相加需要添加一些余項,轉化為R再相乘,代替李代數加法,詳見14講 72頁; 17 ///把SE3上的李代數轉化為4x4矩陣 18 Eigen::Matrix4d Pos_Matrix = Sophus::SE3::exp(x.block(0,0,6,1)).matrix(); 19 Eigen::Matrix4d Pos_update_Matrix = Sophus::SE3::exp(delt_x.block(0,0,6,1)).matrix(); 20 ///矩陣更新 21 Pos_Matrix = Pos_Matrix * Pos_update_Matrix; 22 ///轉化為李代數 23 Sophus::SE3 new_Pos_se = 24 Sophus::SE3(Pos_Matrix.block<3,3>(0,0),Pos_Matrix.block<3,1>(0,3)); 25 ///更新姿態 26 x = x + delt_x; 27 x.block(0,0,6,1)=new_Pos_se.log(); 28 printf("BA cost %f ms \n", (1000*(cv::getTickCount() - t) / cv::getTickFrequency())); 29 //--------------------在原圖相上畫出觀測和預測的坐標------------------------------------- 30 Eigen::VectorXd v_temp(6); 31 v_temp=x.block(0,0,6,1); 32 Sophus::SE3 SE3_temp=Sophus::SE3::exp(v_temp); 33 Eigen::Matrix<double,4,4> Pose = SE3_temp.matrix(); 34 cout<<"POSE:"<<endl<<Pose<<endl; 35 cv::Mat temp_Mat=img.clone(); 36 /// 投影到圖像上,展現優化效果 37 for(int j=0;j<v_P3d.size();j++) { 38 double fx=camMatrix(0,0); 39 double fy=camMatrix(1,1); 40 double cx=camMatrix(0,2); 41 double cy=camMatrix(1,2); 42 Eigen::Matrix<double, 4, 1> Point; 43 Point(0,0)=x(6+3*j, 0); 44 Point(1,0)=x(1+6+3*j, 0); 45 Point(2,0)=x(2+6+3*j, 0); 46 Point(3,0)=1.0; 47 Eigen::Matrix<double, 4, 1> cam_Point = Pose * Point; //計算3D點在相機坐標系下的坐標 48 cv::Point2d temp_Point2d; 49 temp_Point2d.x=(fx*cam_Point(0,0)/cam_Point(2,0))+cx; 50 temp_Point2d.y=(fy*cam_Point(1,0)/cam_Point(2,0))+cy; 51 cv::circle(temp_Mat,temp_Point2d,3,cv::Scalar(0,0,255),2); 52 cv::circle(temp_Mat,v_P2d[j], 2,cv::Scalar(255,0,0),2); 53 } 54 imshow("REPROJECTION ERROR DISPLAY",temp_Mat); 55 cout<<"\033[32m"<<"Iteration: "<<i<<" Finish......"<<"\033[37m"<<endl; 56 cout<<"\033[32m"<<"Blue is observation......"<<"\033[37m"<<endl; 57 cout<<"\033[32m"<<"Red is reprojection......"<<"\033[37m"<<endl; 58 cout<<"\033[32m"<<"Press Any Key to continue......"<<"\033[37m"<<endl; 59 cv::waitKey(0); 60 }
我們可以看一下收斂的過程,其中紅色點為重投影到圖像上的特征點,藍色點為觀測到的特征點。可以看出來收斂還是很快的,基本上迭代三次紅色和藍色點就已經重合!!
第一次迭代結果
第二次迭代
第三次迭代
第四次迭代
第五次迭代
特征點提取過程略,參見《14講》p138“實踐:特征提取和匹配”源碼。我從TUM數據集(rgbd_dataset_freiburg2_large_with_loop)中抽取了兩張特征點比較多的圖片(及第一張圖片對應的深度圖)作為例子。關於3D點坐標的計算,參考數據集說明,詳見下圖:
數據集相機內參
3.總結與討論
1.為什么BA優化優於EKF。(工頭叫我去搬磚.....有空再寫)
2.如何只優化位姿中的旋轉 而固定位移
,亦或反之。(很簡單,F矩陣里的對應雅克比置零即可。工頭叫我去搬磚.....有空再詳寫)