淺談PNP的BA求解


注:這是我在知乎寫的文章,現搬運至此。原文鏈接: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)。為了避免計算這個矩陣[公式](其實也沒必要計算),我們使用擾動模型。如果對此還有疑惑,可以參考這篇文章的第二部分:

劉知:SLAM學習過程中的疑惑及其思考(1)​zhuanlan.zhihu.com

我們再來看 [公式] 的形式,很容易看出來它是一個 [公式] 維的矩陣。但是可要注意了,這只是一個誤差項 [公式] 的雅克比,我們一共擁有 [公式] 個 [公式] (因為在兩幀圖像上,我們擁有 [公式] 個特征點匹配),故整體的 [公式] 的維度為[公式]維。

那么整體的雅克比矩陣 [公式] 就為(其實就是把 [公式] 按行排列,可以參考<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矩陣里的對應雅克比置零即可。工頭叫我去搬磚.....有空再詳寫)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM