浅谈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矩阵里的对应雅克比置零即可。工头叫我去搬砖.....有空再详写)

posted @ 2019-11-20 22:00  钳工一号  阅读(1907)  评论(0编辑  收藏  举报