今天是传说中的5.20,同时也是本人最近特别钟爱的神剧《权力的游戏》大结局的日子,作为一名伪技术宅,这么重大的日子当然要写一篇文来纪念一下。所以就有了这篇手写Bundle Adjustment的记录文。

Bundle Adjustment(后面简写成BA)在视觉SLAM里面的作用不用多说,自然是非常重要的。BA主要求解的是一个关于相机位姿以及路标点的空间坐标的大规模最小二乘优化问题,每一个相机位姿都与多个路标点关联,而每一个路标点也同时和多个相机位姿相关联,由此形成复杂的网状的相互约束,通过调整相机位姿和路标点的位置估计,使得所有路标点在所有相机中的重投影误差最小。下面我们就来亲自动手尝试一下求解Bundle Adjustment。相关的理论可以参考《视觉SLAM十四讲》第六章和第十章,这里仅简单略过。

1  Levenberg-Marquardt优化

考虑简单的最小二乘优化问题:

进行一阶泰勒展开:

同时通过拉格朗日乘子添加对增量的约束,得到以下形式:

求导并令导数等于0,可得以下方程

求解以上增量方程,可得的值,进而令更新对优化变量的估计。

在实际应用中通常取或者,同时还涉及到对大小的调整。

在应用Levenberg-Marquardt优化算法时,最主要的步骤就是求函数值和雅可比,以及求解增量方程得出

下面尝试一下Ceres优化库的一个曲线拟合的实例。拟合用的数据点通过对真实曲线进行采样并附加标准差的高斯噪声生成。

#include<random>
#include<vector>//用来生成高斯噪声
std::default_random_engine e;
std::normal_distribution<double> n(0, 0.2);
//用来存储数据点
std::vector<std::pair<double, double>> data;
//生成数据点,假设x的范围是0-5
for(double x = 0; x < 5; x += 0.05)
{double y = exp(0.3*x + 0.1)+n(e);data.push_back(std::make_pair(x, y));
}

根据以上数据点拟合曲线的最小二乘优化问题为

优化的目标是求得最佳的的值,使得值最小。

,则

#include<eigen3/Eigen/Core>
#include<eigen3/Eigen/Dense>using namespace std;
using namespace Eigen;//为了方便,先定义每一项f(m,c)的误差与雅可比,
//整体的F(m,c)与雅可比J(m,c)可以通过将每个小块组合起来得到
class FitErrorBlock
{
public:FitErrorBlock(double x, double y) :x_(x), y_(y) {}double function(double m, double c){return (exp(m*x_ + c) - y_);}Eigen::Matrix<double,1,2> Jacobian(double m, double c){Eigen::Matrix<double, 1, 2> J;J[0] = x_ * exp(m*x_ + c);J[1] = exp(m*x_ + c);}double x_, y_;
};//接下来开始Levenberg-Marquardt优化
class LMOptimization
{
public://设置初值void setInitValue(double m, double c){m_ = m;c_ = c;}//优化算法主体void Optimize(int niters){double lambda = 1e-4;for (int iter = 0; iter < niters; iter++){//构造雅可比和函数值向量,将每一项作为一行MatrixXd Jacobian(errorTerms.size(), 2);VectorXd err(errorTerms.size());for (int k = 0; k < errorTerms.size(); k++){err[k] = errorTerms[k]->function(m_, c_);Jacobian.row(k) = errorTerms[k]->Jacobian(m_, c_);}//构造增量方程Matrix2d JTJ = Jacobian.transpose()*Jacobian;Matrix2d A = JTJ + lambda * Matrix2d(JTJ.diagonal().asDiagonal());Vector2d b = -Jacobian.transpose()*err;//求解增量方程Ax=bVector2d delta=A.colPivHouseholderQr().solve(b);//如果增量幅度小于阈值,则认为已经收敛,停止优化if (delta.norm() < 1e-10){break;}//计算更新前后的误差double err_before = 0.5*err.squaredNorm();double err_after = 0;for (int k = 0; k < errorTerms.size(); k++){double e= errorTerms[k]->function(m_ + delta[0], c_ + delta[1]);err_after += e * e;}err_after *= 0.5;//判断是否接受更新,如果更新后的误差减小,则接受更新,同时减小lambda;否则不接受更新,并增大lambdaif (err_after < err_before){m_ += delta[0];c_ += delta[1];lambda /= 10;err_before = err_after;}else{lambda *= 10;}cout << "iteration = " << iter << ", error = " << err_before << " , lambda = " << lambda << endl;}}void addErrorTerm(FitErrorBlock* e){errorTerms.push_back(e);}double m_, c_;//保存最小二乘优化的每一项std::vector<FitErrorBlock*> errorTerms;
};

在调用优化算法的时候,可以简单地将每一项加入到优化函数里面

int main()
{std::default_random_engine e;std::normal_distribution<double> n(0, 0.2);std::vector<std::pair<double, double>> data;for (double x = 0; x < 5; x += 0.05){double y = exp(0.3*x + 0.1)+n(e);data.push_back(std::make_pair(x, y));}LMOptimization opt;for (int k = 0; k < data.size(); k++){FitErrorBlock *e = new FitErrorBlock(data[k].first,data[k].second);opt.addErrorTerm(e);}opt.setInitValue(0, 0);opt.Optimize(50);cout << "m = " << opt.m_ << " , c = " << opt.c_ << endl;return 0;
}

结果如下

2  求解Bundle Adjustment

从以上实例可以看出,求解最小二乘优化主要分为四步:计算每一个误差项及其雅可比,将所有误差项组合起来构造增量方程,求解增量方程,更新优化变量。用相同的思想来求解BA,首先要定义每一个误差项以及雅可比,然后利用BA的稀疏结构构建增量方程并求解,最后增新优化变量。

在BA里面比较常见的就是路标点在图像里面的重投影误差,为了方便计算雅可比,这里采用“预测值-测量值”的形式。

  ,   ,  

class ReprojectionError
{
public://位姿采用旋转矩阵与平移向量表示typedef pair<Matrix3d, Vector3d> CameraPose;ReprojectionError() :fx(0), fy(0), cx(0), cy(0), u(0), v(0), camIdx(0), pointIdx(0){;}void setMeasurement(double u, double v){this->u = u;this->v = v;}void setCameraParams(double fx, double fy, double cx, double cy){this->fx = fx;this->fy = fy;this->cx = cx;this->cy = cy;}//计算重投影误差Vector2d function(const CameraPose cam,const Vector3d p){Matrix3d R = cam.first;Vector3d t = cam.second;Vector2d error;Vector3d pc = R * p + t;double xp = pc[0] / pc[2];double yp = pc[1] / pc[2];double up = f * xp + cx;double vp = f * yp + cy;error[0] = up - u;error[1] = vp - v;return error;}Matrix<double, 2, 9> Jacobian(const CameraPose cam,const Vector3d p){Matrix<double, 2, 9> J;Matrix3d R = cam.first;Vector3d t = cam.second;Vector3d pc = R * p + t;double x = pc[0], y = pc[1], z = pc[2];double z2 = z * z;Matrix<double, 2, 3> J_e_pc;J_e_pc << f / z,    0,       -f * x / z2,0,      f / z,    -f * y / z2;Matrix<double, 3, 6> J_pc_ksi;J_pc_ksi << Matrix3d::Identity(), -skew(pc);//对相机位姿的雅可比Matrix<double, 2, 6> J_e_ksi = J_e_pc * J_pc_ksi;Matrix3d J_pc_p = R;//对路标点位置的雅可比Matrix<double, 2, 3> J_e_p = J_e_pc * J_pc_p;J << J_e_ksi, J_e_p;return J;}Matrix3d skew(Vector3d phi){Matrix3d Phi;Phi << 0, -phi(2), phi(1),phi(2), 0, -phi(0),-phi(1), phi(0), 0;return Phi;}double fx, fy, cx, cy;     //保存相机内参double u, v;               //路标点在图像上的测量值int camIdx, pointIdx;      //哪个相机观测哪个路标点
};

在定义好了每一个误差项之后,接下来就是将各个误差项组合成一个大的雅可比和增量方程。

整体的雅可比可以看作是一个以每个误差项对应的雅可比作为一行的分块矩阵

  ,

其中每一项雅可比块仅有两个非零子块

也仅有四个子块非零,分别对应四个位置。最终累加得到的具有特殊的稀疏结构

其中均为分块对角阵,

在进行累加的时候,可以利用以下特点:

  • 中的每个分块均为,分块个数等于相机的个数;每一个对所有的累加得到.
  • 中的每个分块均为,分块个数等于路标点的个数;每一个对所有的累加得到.
  • 中的每个分块均为,行数等于相机的个数,列数等于路标点的个数;每一个得到.

所以在计算时,可以对每一个误差项求出来后,根据下标的值,分别将累加到对应的中去。类似地,误差向量和增量方程右端项也可以通过累加得到。下面的代码计算了增量方程的左端和右端,其中右端根据左端的分块对应地分成了,分别对应于相机和路标点。

for (int k = 0; k < ErrorTerms.size(); k++)
{ReprojectionError* term = ErrorTerms[k];Matrix<double,2,9> J = term->Jacobian(cameras[term->camIdx], points[term->pointIdx]);Matrix<double,2,6> Jc = J.block(0, 0, 2, 6);Matrix<double, 2, 3> Jp = J.block(0, 6, 2, 3);Matrix<double,6,6> JcTJc = Jc.transpose() * Jc;Hcc[term->camIdx] += JcTJc + lambda * JacobianCC(JcTJc.diagonal().asDiagonal());Matrix<double,3,3> JpTJp = Jp.transpose() * Jp;Hpp[term->pointIdx] += JpTJp + lambda * JacobianPP(JpTJp.diagonal().asDiagonal());Hcp[term->camIdx][term->pointIdx] += Jc.transpose()*Jp;Vector2d error = term->function(cameras[term->camIdx], points[term->pointIdx]);bc[term->camIdx] -= Jc.transpose()*error;bp[term->pointIdx] -= Jp.transpose()*error;
}

接下来最能利用BA特殊结构的时候到了,求解增量方程,主要利用了Schur消元。

消去右上角的,可以得到

第一行变成了和无关的方程,可以直接求解

因为通常维数比低很多,而且为分块对角阵,每一个分块均为3阶方阵,很容易求逆,所以求解相对较快。在求得之后,可以将代入求得

在计算和求解上述两个方程时,也可以根据之前的结论将计算过程转化为维数较低的分块矩阵进行累加,避免进行大型矩阵的运算。具体过程不再详细写出来了,将以及分别写成分块的形式,直接进行分块运算就可以了。

void ComputeUpdate()
{int Nc = cameras.size();int Np = points.size();MatrixXd B(6 * Nc, 6 * Nc); //保存Hcc-HcpHpp^(-1)Hcp^(T)vector<Matrix3d> InvHpp(Np); //保存Hpp^(-1)的各个分块vector<Vector6d> ECbp(Nc,Vector6d::Zero());  //保存HcpHpp^(-1)bpVectorXd b(6 * Nc);  //保存bc-HcpHpp^(-1)bp//对Hpp各个分块求逆for (int k = 0; k < Np; k++){InvHpp[k] = Hpp[k].inverse();}//先将B赋值为Hcc,然后再减去HcpHpp^(-1)Hcp^(T)for (int k = 0; k < Nc; k++){B.block(6 * k, 6 * k, 6, 6) = Hcc[k];}//计算Hcc-HcpHpp^(-1)Hcp^(T)和HcpHpp^(-1)bpfor (int k = 0; k < Np; k++){for (int i = 0; i < Nc; i++){Matrix<double, 6, 3> EikCk = Hcp[i][k] * InvHpp[k];ECbp[i] += EikCk * bp[k];for (int j = i; j < Nc; j++){B.block(6 * i, 6 * j, 6, 6) -= EikCk * Hcp[j][k].transpose();}}}//因为B为对称矩阵,所以上面只计算了上三角部分,下三角部分直接赋值for (int i = 0; i < Nc; i++){for (int j = i+1; j < Nc; j++){B.block(6 * j, 6 * i, 6, 6) = B.block(6 * i, 6 * j, 6, 6).transpose();}}//计算bc-HcpHpp^(-1)bpfor (int k = 0; k < Nc; k++){b.segment(6 * k, 6) = bc[k] - ECbp[k];}cout << "Solving Pose updates." << endl;//求解delta xcVectorXd deltac=B.colPivHouseholderQr().solve(b);for (int k = 0; k < Nc; k++){deltacs[k] = deltac.segment(6 * k, 6);}//回代求delta xpfor (int j = 0; j < Np; j++){Vector3d Ekdeltack = Vector3d::Zero();for (int i = 0; i < Nc; i++){Ekdeltack += Hcp[i][j].transpose()*deltacs[i];}deltaps[j] = InvHpp[j] * (bp[j] - Ekdeltack);}cout << "Update Calculated." << endl;
}

最后对相机和路标点的估计进行更新,路标点的坐标直接按向量相加,相机位姿采用李代数运算左乘更新。

for (int k = 0; k < cameras.size(); k++)
{CameraPose X = cameras[k];Vector6d deltaX = deltacs[k];Matrix4d deltaT = SE3Type::exp(deltaX);Matrix3d deltaR = deltaT.block(0, 0, 3, 3);Vector3d deltat = deltaT.block(0, 3, 3, 1);Matrix3d Rnew = deltaR * X.first;Vector3d tnew = deltaR * X.second + deltat;camerasUpdated[k] = make_pair(Rnew, tnew);
}for (int k = 0; k < points.size(); k++)
{pointsUpdated[k] = points[k] + deltaps[k];
}

其余部分均和第1部分Levenberg-Marquardt优化的过程完全相同,包括判断更新前后误差变化、对lambda的增减、判断收敛等,此处不再赘述。完整的代码放在了github上面。

https://github.com/BruceWhiteSJTU/CSDN-Blog/tree/master/Bundle%20Adjustment

参考文献

[1] 高翔,张涛.视觉SLAM十四讲[M].北京:电子工业出版社,2017.

[2]Timothy D.Barfoot.State Estimation for Robotics[M].United Kingdom:Cambridge University Press,2017.

[3]Grisetti G , Ku,Mmerle R , Stachniss C , et al. A Tutorial on Graph-Based SLAM[J]. IEEE Intelligent Transportation Systems Magazine, 2010, 2(4):31-43.

[4]Triggs B . Bundle Adjustment -A Modern Synthesis[J]. 1999.

[5]Frank D , Michael K . Factor Graphs for Robot Perception[C]// 2017:1-139.

爽一把手写Bundle Adjustment相关推荐

  1. ORB_SLAM2代码阅读(5)——Bundle Adjustment

    ORB_SLAM2代码阅读(5)--Bundle Adjustment 1. 说明 2. Bundle Adjustment(BA)的物理意义 3. BA的数学表达 4. BA的求解方法 4.1 最速 ...

  2. Bundle Adjustment原理及应用(附实战代码)

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 虽然现在的轮子很多,但我们在使用过程中会碰到很多问题,而我们经常不知道从哪里下手,说明轮子不是你造的你 ...

  3. 矩阵求逆c语言实现_[V-SLAM] Bundle Adjustment 实现

    SLAM问题的后端有主要有滤波和优化两种方案.目前,普遍认为优化的方法在精度上要超过滤波方法,因为它可以进行多次的线性化.近年来出现的SLAM算法也大都是基于优化的算法(如ORB-SLAM.DSO等) ...

  4. 史上最简SLAM零基础解读(7) - Jacobian matrix(雅可比矩阵) → 理论分析与应用详解(Bundle Adjustment)

    本人讲解关于slam一系列文章汇总链接:史上最全slam从零开始   文末正下方中心提供了本人联系方式,点击本人照片即可显示WX→官方认证{\color{blue}{文末正下方中心}提供了本人 \co ...

  5. OpenCV实现SfM(四):Bundle Adjustment

    http://blog.csdn.net/AIchipmunk/article/details/52433884 OpenCV实现SfM(四):Bundle Adjustment 标签: opencv ...

  6. Bundle Adjustment (BA)简述

    笔者到底想讲些啥? 在SFM(structure from motion)的计算中BA(Bundle Adjustment)作为最后一步优化具有很重要的作用,在近几年兴起的基于图的SLAM(simul ...

  7. 《论文阅读》BA-NET: DENSE BUNDLE ADJUSTMENT NETWORKS

    留个笔记自用 BA-NET: DENSE BUNDLE ADJUSTMENT NETWORKS 做什么 首先是最基础的,Structure-from-Motion(SFM),SFM可以简单翻译成运动估 ...

  8. 《论文阅读》BALM: Bundle Adjustment for Lidar Mapping

    留个笔记自用 BALM: Bundle Adjustment for Lidar Mapping 做什么 首先是最基础的,Structure-from-Motion(SFM),SFM可以简单翻译成运动 ...

  9. 关于SBA(Sparse Bundle Adjustment)编译以及遇到的一些问题

    本人最近接触了SBA,由于没有人指导,只能在网上搜索资料来进行学习,理解地并不是很深入.但考虑到有很多初学者和我一样,会遇到很多问题,故想整理一下,一算是对自己这些天学习的回顾,二算是给接下来学习的人 ...

  10. 【论文笔记】显著性信息辅助的视觉SLAM系统 SBAS: Salient Bundle Adjustment for Visual SLAM

    重庆大学, 汽车工程专业, 重庆大学机械传动重点实验室 本文并不是第一个 利用显著性预测结果改进SLAM系统的工作,但是之前的工作都没有意识到显著性检测模型存在中心偏差的问题,这导致在没有显著性区域的 ...

最新文章

  1. 新浪微博登录密码加密函数 wsse加密算法说明
  2. python做直方图-python OpenCV学习笔记实现二维直方图
  3. 连接端口 配置hive_Zeppelin带有Kerberos认证的Hive解释器的配置
  4. Linux编译安装Python3
  5. 数据结构和算法基础之冒泡排序
  6. 基于VMware Workstation创建虚拟机,以Ubuntu16.04为例
  7. CAD答辩周 -- 与自己相关的几场
  8. mybatis 级联查询兑现_MyBatis之自查询使用递归实现 N级联动效果(两种实现方式)...
  9. python selenium环境安装及配置_selenium环境配置
  10. 王者服务器维修2019年四月份,王者荣耀4月25日更新内容 王者荣耀2019年4月25日全服不停机更新公告...
  11. [转载] java clone方法_Java Calendar clone()方法与示例
  12. Codeforces Round #698 (Div. 2) (思维)
  13. Learning ROS: Using Sensors and Actuators with ROS -在ROS中使用传感器和执行器
  14. uploadify一次上传多个图片:效果展示
  15. C语言中typedef用法
  16. 【HDU 6274】Master of sequence【二分答案+下取整转换】
  17. android studio配置夜神模拟器
  18. 参加口碑最好的广州传智播客Java就业培训班吧
  19. vue图片加载完成前增加loading效果
  20. 智能秤方案设计——蓝牙体脂秤PCBA软硬件端功能说明

热门文章

  1. 廊坊金彩教育:店铺运营技巧思路
  2. 金彩教育:店铺装修色彩怎么搭配
  3. 秦小明 第六讲 投融资,资产运作
  4. window下编译ffmpeg--mys2下安装对应库编译ffmpeg
  5. mysqld: [ERROR] Found option without preceding group in config file D:\MySql\MyS ql\my.ini at line 1
  6. The JAVA_HOME environment variable is not defined correctly(亲测有效)
  7. 机器学习-29-Pointer Network(指针网络)
  8. h5微信f分享链接给对方获取对方手机号_免费微信电子贺卡制作|微请柬
  9. adjacent cache line prefetch
  10. 给SVN泼盆冷水,是时候用GIT了