视觉SLAM十四讲学习笔记——第六讲 非线性优化(2)

        这一节主要回顾一下Ceres、g20的使用。

1.Ceres、G2o源码安装方法

        高博士的书中都有各个库的安装方法,但由于版本变化,个别安装方法可能并不适用。这里简单整理一下两个库的源码安装方法,其他的库之后有时间统一整理一下。

(1)Ceres安装

下载源码,下载地址:https://github.com/ceres-solver/ceres-solver

安装依赖项:

sudo apt-get install liblapack-dev libsuitesparse-dev libcxsparse3.1.2 libgflags-dev libgoogle-glog-dev libgtest-dev
sudo apt install cmake libeigen3-dev 

打开源码文件夹安装:

mkdir build
cd build
cmake ..
make -j4
sudo make install

(2)g2o安装

下载源码,下载地址:https://github.com/RainerKuemmerle/g2o

安装依赖项:

sudo apt-get install libeigen3-dev libsuitesparse-dev qtdeclarative5-dev qt5-qmake libqglviewer-dev qt5-default libqglviewer-dev-qt5 libcholmod3

打开源码文件夹安装:

mkdir build
cd build
cmake ..
make -j4
sudo make install

2.使用方法

        Ceres库的使用方法较为简单,整体感觉比较简单,一些使用时的注意事项都记录在代码注释里了,相较而言g2o的代码更难看懂,使用起来比较困难。这里推荐"计算机视觉life"的三篇博客:

从零开始一起学习SLAM | 理解图优化,一步步带你看懂g2o代码_计算机视觉life-CSDN博客

从零开始一起学习SLAM | 掌握g2o顶点编程套路_计算机视觉life-CSDN博客

从零开始一起学习SLAM | 掌握g2o边的代码套路_计算机视觉life-CSDN博客

3.源码注释

        高博士的例程中有两个库用来求解曲线拟合的示例代码,我对两个代码做了更详细的注释,也标记出了一些没有看懂的问题:

(1)/slambook2-master/ch6/ceresCurveFitting.cpp

//
// Created by xiang on 18-11-19.
//

#include 
#include 
#include 
#include 

using namespace std;

// 代价函数的计算模型:输入x、y,计算当前参数下的损失函数 y-exp(ax^2+bx+c)
struct CURVE_FITTING_COST {
  // 构造函数:传递用于计算误差的测量值
  CURVE_FITTING_COST(double x, double y) : _x(x), _y(y) {}

  // 重载()操作符,用于计算误差
  template
  bool operator()(const T *const abc, // 模型参数,有3维
    T *residual)
    const {
    residual[0] = T(_y) - ceres::exp(abc[0] * T(_x) * T(_x) + abc[1] * T(_x) + abc[2]); // y-exp(ax^2+bx+c)
    return true;
  }
  // 用于存储测量值的成员变量
  const double _x, _y;    // x,y数据
};

int main(int argc, char **argv) {
  //相同的数据创建方式
  double ar = 1.0, br = 2.0, cr = 1.0;         // 真实参数值
  double ae = 2.0, be = -1.0, ce = 5.0;        // 估计参数值
  int N = 100;                                 // 数据点
  double w_sigma = 1.0;                        // 噪声Sigma值
  double inv_sigma = 1.0 / w_sigma;
  cv::RNG rng;                                 // OpenCV随机数产生器

  vector x_data, y_data;      // 数据
  for (int i = 0; i < N; i++) {
    double x = i / 100.0;
    x_data.push_back(x);
    y_data.push_back(exp(ar * x * x + br * x + cr) + rng.gaussian(w_sigma * w_sigma));
  }
  //待估计的参数:a,b,c
  double abc[3] = {ae, be, ce};

  // 构建最小二乘问题
  ceres::Problem problem;
  for (int i = 0; i < N; i++)
  {
      // 向问题中添加误差项
    problem.AddResidualBlock(
      // AutoDiffCostFunction使用自动求导,模板参数:误差类型,输出维度,输入维度,维数要与前面struct中一致
      new ceres::AutoDiffCostFunction(
        new CURVE_FITTING_COST(x_data[i], y_data[i])
      ),
      nullptr,            // 核函数,这里不使用,为空
      abc                 // 待估计参数
    );
  }
  /*
   * ceres的使用:
   * Problem::AddResidualBlock():要用于向Problem类传递残差模块的信息
   *                              传递参数包括:CostFunction *cost_function:代价函数模块 包含了参数模块的维度信息,内部使用仿函数定义误差函数的计算方式
   *                                          LossFunction *loss_function:损失函数模块 该参数可以取NULL或nullptr,此时损失函数为单位函数
   *                                          const vector parameter_blocks:参数模块 待优化的参数,可一次性传入所有参数的指针容器
   *        参数模块较为简单;损失函数默认不使用;重点关注“代价函数模块”
   *        代价函数CostFunction:常用的包括以下三种
   *                                            自动导数(AutoDiffCostFunction):由ceres自行决定导数的计算方式,最常用的求导方式
   *                                            数值导数(NumericDiffCostFunction):由用户手动编写导数的数值求解形式
   *                                            解析导数(Analytic Derivatives):当导数存在闭合解析形式时使用,用于可基于CostFunciton基类自行编写
   *                            这里使用的是自行求导AutoDiffCostFunction
   *                            ceres::AutoDiffCostFunction(CostFunctor* functor);
   *                            传递参数包括:CostFunctor         仿函数
   *                                        int residualDim     残差维数(1)
   *                                        paramDim            参数维数(3)
   *                            接受参数类型:为仿函数指针CostFunctor*
   *                            后两个变量不难理解,重点关注“仿函数CostFunctor”
   *                            仿函数的本质为结构体struct或者类class,由于重载了()运算符,使得其能够具有和函数一样的调用行为,因此被称为仿函数
   *                            主要包括 构造函数、重载操作符() 和 工厂函数三部分
   *                            详细信息见 示例代码中的定义部分
   */
  // 配置求解器
  ceres::Solver::Options options;     // 这里有很多配置项可以填
  options.linear_solver_type = ceres::DENSE_NORMAL_CHOLESKY;  // 增量方程如何求解:CHOLESKY:分解
  options.minimizer_progress_to_stdout = true;   // 输出到cout

  ceres::Solver::Summary summary;                // 优化信息
  chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
  ceres::Solve(options, &problem, &summary);  // 开始优化
  chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
  chrono::duration time_used = chrono::duration_cast>(t2 - t1);
  cout << "solve time cost = " << time_used.count() << " seconds. " << endl;

  // 输出结果
  cout << summary.BriefReport() << endl;
  cout << "estimated a,b,c = ";
  for (auto a:abc) cout << a << " ";
  cout << endl;

  return 0;
}

(2)/slambook2-master/ch6/g2oCurveFitting.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

// 自定义顶点,模板参数:
// int D: 优化变量维度,3
//    T :数据类型,Vector3d
class CurveFittingVertex : public g2o::BaseVertex<3, Eigen::Vector3d> {
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW

  // setToOriginImpl():顶点重置函数,设定被优化变量的原始值为<0,0,0>
  virtual void setToOriginImpl() override {
    _estimate << 0, 0, 0;
  }

  // oplusImpl():顶点更新函数。用于优化过程中增量△x如何更新到节点的计算。
  // 我们根据增量方程计算出增量之后,就是通过这个函数对估计值进行调整的,这里选择直接相加“+=”
  virtual void oplusImpl(const double *update) override {
    _estimate += Eigen::Vector3d(update);
  }

  // 存盘和读盘:一般情况下不需要进行读/写操作的话,仅仅声明一下就可以
  virtual bool read(istream &in) {}

  virtual bool write(ostream &out) const {}
};

// 自定义边: 模板参数:观测值维度,类型,连接顶点类型
// 针对曲线拟合问题,使用一元边:BaseUnaryEdge
// 观测值维度:1,估计值和实际值之差
// 测量值数据类型:double
// 节点类型:由于是一元边,因此只有一个参数CurveFittingVertex
class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex> {
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
  CurveFittingEdge(double x) : BaseUnaryEdge(), _x(x) {}

  // 使用当前顶点的值计算的测量值与真实的测量值之间的误差
  virtual void computeError() override {
    // 连接的节点
    const CurveFittingVertex *v = static_cast (_vertices[0]);
    // 从节点中读取待优化的变量
    const Eigen::Vector3d abc = v->estimate();
    // 计算误差:error=y-exp(a*x*x+b*x+c)
    _error(0, 0) = _measurement - std::exp(abc(0, 0) * _x * _x + abc(1, 0) * _x + abc(2, 0));
  }

  // linearizeOplus():是在当前顶点的值下,该误差对优化变量的偏导数,即计算雅可比矩阵
  virtual void linearizeOplus() override {
    // 连接的节点
    const CurveFittingVertex *v = static_cast (_vertices[0]);
    // 从节点中读取待优化的变量
    const Eigen::Vector3d abc = v->estimate();
    // 求导公式
    double y = exp(abc[0] * _x * _x + abc[1] * _x + abc[2]);
    _jacobianOplusXi[0] = -_x * _x * y;
    _jacobianOplusXi[1] = -_x * y;
    _jacobianOplusXi[2] = -y;
  }
  // 读盘、存盘函数,一般情况下不需要进行读/写操作的话,仅仅声明一下就可
  virtual bool read(istream &in) {}

  virtual bool write(ostream &out) const {}
// x 值, y 值为 _measurement
public:
  double _x;
};

int main(int argc, char **argv) {
  //同样的方式生成含有噪声的数据
  double ar = 1.0, br = 2.0, cr = 1.0;         // 真实参数值
  double ae = 2.0, be = -1.0, ce = 5.0;        // 估计参数值
  int N = 100;                                 // 数据点
  double w_sigma = 1.0;                        // 噪声Sigma值
  double inv_sigma = 1.0 / w_sigma;
  cv::RNG rng;                                 // OpenCV随机数产生器

  vector x_data, y_data;      // 数据
  for (int i = 0; i < N; i++) {
    double x = i / 100.0;
    x_data.push_back(x);
    y_data.push_back(exp(ar * x * x + br * x + cr) + rng.gaussian(w_sigma * w_sigma));
  }

  // 第一步:构建图优化,先设定g2o:每个误差项优化变量维度为3,误差值维度为1
  typedef g2o::BlockSolver> BlockSolverType;
  // 第二步:创建一个线性求解器LinearSolver:使用dense cholesky分解法,用于求解HΔx=−b
  // 这里存在一个问题:BlockSolverType::PoseMatrixType是根据什么确定的,查找了一些资料都没有提及,大家似乎认为这是理所应当的
  typedef g2o::LinearSolverDense LinearSolverType;
  // 第三步:创建一个块求解器BlockSolver:用LinearSolver初始化
  // 第四步:创建一个总求解器solver:用BlockSolver初始化,选择高斯-牛顿法优化策略
  // 梯度下降方法,可以从GN, LM, DogLeg 中选
  auto solver = new g2o::OptimizationAlgorithmGaussNewton(
    g2o::make_unique(g2o::make_unique()));

  // 创建稀疏优化器SparseOptimizer:核心优化算法
  g2o::SparseOptimizer optimizer;
  // 用solver设置为求解器
  optimizer.setAlgorithm(solver);
  // 打开优化过程中的调试输出
  optimizer.setVerbose(true);

  // 向图中添加节点:本示例只有一个节点,包含三个需要优化的参数a,b,c
  CurveFittingVertex *v = new CurveFittingVertex();
  // 添加节点带优化变量
  v->setEstimate(Eigen::Vector3d(ae, be, ce));
  // 设置节点编号
  v->setId(0);
  // 向优化器optimizer中添加节点
  optimizer.addVertex(v);

  // 向图中添加边:每组数据都是一条边,拥有N组100条
  for (int i = 0; i < N; i++) {
    // x是边的输入
    CurveFittingEdge *edge = new CurveFittingEdge(x_data[i]);
    // 设置编号
    edge->setId(i);
    // 都连接到同一个顶点
    edge->setVertex(0, v);
    // y为观测数值
    edge->setMeasurement(y_data[i]);
    // 信息矩阵:协方差矩阵之逆
    edge->setInformation(Eigen::Matrix::Identity() * 1 / (w_sigma * w_sigma));
    // 在优化器optimizer中添加边
    optimizer.addEdge(edge);
  }

  // 执行优化
  cout << "start optimization" << endl;
  chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
  // 初始化
  optimizer.initializeOptimization();
  // 设置迭代次数:10,开始优化
  optimizer.optimize(10);
  chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
  chrono::duration time_used = chrono::duration_cast>(t2 - t1);
  cout << "solve time cost = " << time_used.count() << " seconds. " << endl;

  // 输出最终结果
  Eigen::Vector3d abc_estimate = v->estimate();
  cout << "estimated model: " << abc_estimate.transpose() << endl;

  return 0;
}

你可能感兴趣的:(视觉SLAM十四讲学习笔记,机器学习,slam)