cartographer_ceres_scan_matcher_2d

21 篇文章 2 订阅
12 篇文章 2 订阅

cartographer_ceres_scan_matcher_2d

0.理论部分

参考曾书格PPT:

请添加图片描述

同样的以 M ( x ) M(x) M(x) 概率越大越好,通过 1 − M ( x ) 1-M(x) 1M(x)则转为求最小值,也就是转为了最优化问题。最终目标要求最优的 T = ( T x , T y , T θ ) T = (T_x,T_y,T_{\theta}) T=(Tx,Ty,Tθ) 通过优化求解。

请添加图片描述
常数 2 不影响结果,直接丢掉。
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

cartographer 在代码实现过程中直接使用的 ceres 自动求导,没有对以上公式推导的细节进行代码实现。

误差项的定义与计算在如下文件中:

#include "cartographer/mapping/internal/2d/scan_matching/occupied_space_cost_function_2d.h"
#include "cartographer/mapping/internal/2d/scan_matching/rotation_delta_cost_functor_2d.h"
#include "cartographer/mapping/internal/2d/scan_matching/translation_delta_cost_functor_2d.h"
#include "cartographer/mapping/internal/2d/scan_matching/tsdf_match_cost_function_2d.h"

对外接口实现在 ceres_scan_matcher_2d.h中,核心函数为 Match 函数。

/**
 * @brief 基于Ceres的扫描匹配
 * 
 * @param[in] target_translation 预测出来的先验位置, 只有xy
 * @param[in] initial_pose_estimate (校正后的)先验位姿, 有xy与theta
 * @param[in] point_cloud 用于匹配的点云 点云的原点位于local坐标系原点
 * @param[in] grid 用于匹配的栅格地图
 * @param[out] pose_estimate 优化之后的位姿
 * @param[out] summary 
 */
void CeresScanMatcher2D::Match(const Eigen::Vector2d& target_translation,
                               const transform::Rigid2d& initial_pose_estimate,
                               const sensor::PointCloud& point_cloud,
                               const Grid2D& grid,
                               transform::Rigid2d* const pose_estimate,
                               ceres::Solver::Summary* const summary) const {
  double ceres_pose_estimate[3] = {initial_pose_estimate.translation().x(),
                                   initial_pose_estimate.translation().y(),
                                   initial_pose_estimate.rotation().angle()};
  ceres::Problem problem;
  // 1.点云匹配残差块
  CHECK_GT(options_.occupied_space_weight, 0.);
  problem.AddResidualBlock(
      OccupiedSpaceCostFunction2D::CreateAutoDiffCostFunction(
          // 总权重/点云数 = 每个point的权重
          options_.occupied_space_weight /
              std::sqrt(static_cast<double>(point_cloud.size())),
          point_cloud, grid),
      nullptr /* loss function */, ceres_pose_estimate);
  // 2.平移残差块
  CHECK_GT(options_.translation_weight, 0.);
  problem.AddResidualBlock(
      TranslationDeltaCostFunctor2D::CreateAutoDiffCostFunction(
          options_.translation_weight, target_translation), // 平移的目标值
      nullptr /* loss function */, ceres_pose_estimate);    // 平移的初值
  // 3.旋转残差块
  CHECK_GT(options_.rotation_weight, 0.);
  problem.AddResidualBlock(
      RotationDeltaCostFunctor2D::CreateAutoDiffCostFunction(
          options_.rotation_weight, ceres_pose_estimate[2]), // 角度的目标值
      nullptr /* loss function */, ceres_pose_estimate);     // 角度的初值
  // 根据配置进行求解
  ceres::Solve(ceres_solver_options_, &problem, summary);

  *pose_estimate = transform::Rigid2d(
      {ceres_pose_estimate[0], ceres_pose_estimate[1]}, ceres_pose_estimate[2]);
}

1.残差项

残差主要有三个方面的来源:

  • (1) 占用栅格与扫描数据的匹配度
  • (2) 优化后的位置相对于target_translation的距离
  • (3) 旋转角度相对于迭代初值的偏差

此外,还应注意到在配置文件中为这三类来源定义了权重。Cartographer为三类不同的残差源分别定义了一个类,并通过其静态函数CreateAutoDiffCostFunction提供使用Ceres原生的自动求导方法的代价函数计算。

2.残差计算

(1)占用栅格代价函数

class OccupiedSpaceCostFunction2D {
 public:
  // 类的构造函数设为私有, 禁止外部构造对象, 只能通过该函数进行构造
  static ceres::CostFunction* CreateAutoDiffCostFunction(
      const double scaling_factor, const sensor::PointCloud& point_cloud,
      const Grid2D& grid) {
    return new ceres::AutoDiffCostFunction<OccupiedSpaceCostFunction2D,
                                           ceres::DYNAMIC /* residuals */,
                                           3 /* pose variables */>(
        new OccupiedSpaceCostFunction2D(scaling_factor, point_cloud, grid),
        point_cloud.size());
  }

  // 重载运算符(), 用来计算残差
  template <typename T>
  bool operator()(const T* const pose, T* residual) const {
    Eigen::Matrix<T, 2, 1> translation(pose[0], pose[1]);
    Eigen::Rotation2D<T> rotation(pose[2]);
    Eigen::Matrix<T, 2, 2> rotation_matrix = rotation.toRotationMatrix();
    Eigen::Matrix<T, 3, 3> transform;
    transform << rotation_matrix, translation, T(0.), T(0.), T(1.);
    
    // 对grid_进行双三次插值
    const GridArrayAdapter adapter(grid_);
    ceres::BiCubicInterpolator<GridArrayAdapter> interpolator(adapter);
    const MapLimits& limits = grid_.limits();

    for (size_t i = 0; i < point_cloud_.size(); ++i) {
      // Note that this is a 2D point. The third component is a scaling factor.
      const Eigen::Matrix<T, 3, 1> point((T(point_cloud_[i].x())),
                                         (T(point_cloud_[i].y())), T(1.));
      const Eigen::Matrix<T, 3, 1> world = transform * point;
      interpolator.Evaluate( // 双三次插值
          (limits.max().x() - world[0]) / limits.resolution() - 0.5 +
              static_cast<double>(kPadding),
          (limits.max().y() - world[1]) / limits.resolution() - 0.5 +
              static_cast<double>(kPadding),
          &residual[i]); // grid_表示的是miss的概率, 本身便是残差
      residual[i] = scaling_factor_ * residual[i];
    }
    return true;
  }

 private:
  static constexpr int kPadding = INT_MAX / 4; // 扩充边界

  // 对grid_进行一层封装, 从而能使用ceres自带的双三次插值器 BiCubicInterpolator
  class GridArrayAdapter {
   public:
    enum { DATA_DIMENSION = 1 };

    explicit GridArrayAdapter(const Grid2D& grid) : grid_(grid) {}

    void GetValue(const int row, const int column, double* const value) const {
      if (row < kPadding || column < kPadding || row >= NumRows() - kPadding ||
          column >= NumCols() - kPadding) {
        *value = kMaxCorrespondenceCost;
      } else {
        *value = static_cast<double>(grid_.GetCorrespondenceCost(
            Eigen::Array2i(column - kPadding, row - kPadding)));
      }
    }

    int NumRows() const {
      return grid_.limits().cell_limits().num_y_cells + 2 * kPadding;
    }

    int NumCols() const {
      return grid_.limits().cell_limits().num_x_cells + 2 * kPadding;
    }

   private:
    const Grid2D& grid_;
  };

  // 类的构造函数设为私有的, 禁止外部构造对象
  OccupiedSpaceCostFunction2D(const double scaling_factor,
                              const sensor::PointCloud& point_cloud,
                              const Grid2D& grid)
      : scaling_factor_(scaling_factor),
        point_cloud_(point_cloud),
        grid_(grid) {}

  OccupiedSpaceCostFunction2D(const OccupiedSpaceCostFunction2D&) = delete;
  OccupiedSpaceCostFunction2D& operator=(const OccupiedSpaceCostFunction2D&) =
      delete;

  const double scaling_factor_; // 每个point的权重
  const sensor::PointCloud& point_cloud_;
  const Grid2D& grid_;
};
  • step 1 将 pose 转为 transform
  • step 2 对grid_进行双三次插值
  • step 3 针对每个 hit 点计算对应的残差代价
    argmin ⁡ T ε ∑ k = 1 K ( 1 − M s m o o t h ( T ε h k ) ) 2 \underset{T_\varepsilon}{\operatorname{argmin}} \sum_{k=1}^{K}\left(1-M_{s m o o t h}\left(T_{\varepsilon} h_{k}\right)\right)^{2} Tεargmink=1K(1Msmooth(Tεhk))2
    • 通过 hit 点坐标与 transform 的相乘得到其在地图坐标系下的坐标 T ε h k T_{\varepsilon} h_k Tεhk
    • 通过刚刚构建的迭代器,和地图坐标,获取在hit点出现的概率。该函数调用有三个参数,前两个参数用来描述x,y轴索引, 第三个参数用于记录插值后的结果。这里的xy索引计算的比较奇怪,它通过GridArrayAdapter类中成员函数GetValue获取栅格数据,这里不再细述。
    • 由于占用栅格中原本存储的就是栅格空闲的概率, 所以这里查询出来的概率就是 ( 1 − M s m o o t h ( T ε h k ) ) \left(1 - M_{smooth}\left(T_{\varepsilon} h_k\right)\right) (1Msmooth(Tεhk)) 也就是残差项。

(2)平移代价函数

class TranslationDeltaCostFunctor2D {
 public:
  static ceres::CostFunction* CreateAutoDiffCostFunction(
      const double scaling_factor, const Eigen::Vector2d& target_translation) {
    return new ceres::AutoDiffCostFunction<TranslationDeltaCostFunctor2D,
                                           2 /* residuals */,
                                           3 /* pose variables */>(
        new TranslationDeltaCostFunctor2D(scaling_factor, target_translation));
  }

  template <typename T>
  bool operator()(const T* const pose, T* residual) const {
    residual[0] = scaling_factor_ * (pose[0] - x_);
    residual[1] = scaling_factor_ * (pose[1] - y_);
    return true;
  }

 private:
  // Constructs a new TranslationDeltaCostFunctor2D from the given
  // 'target_translation' (x, y).
  explicit TranslationDeltaCostFunctor2D(
      const double scaling_factor, const Eigen::Vector2d& target_translation)
      : scaling_factor_(scaling_factor),
        x_(target_translation.x()),
        y_(target_translation.y()) {}

  TranslationDeltaCostFunctor2D(const TranslationDeltaCostFunctor2D&) = delete;
  TranslationDeltaCostFunctor2D& operator=(
      const TranslationDeltaCostFunctor2D&) = delete;

  const double scaling_factor_;
  const double x_;
  const double y_;
};

可以看出:

  • 残差项:二维 --> x, y
  • 待优化的参数:三维 --> x, y, theta
  • scaling_factor_为平移误差的权重

(3)旋转代价函数

class RotationDeltaCostFunctor2D {
 public:
  static ceres::CostFunction* CreateAutoDiffCostFunction(
      const double scaling_factor, const double target_angle) {
    return new ceres::AutoDiffCostFunction<
        RotationDeltaCostFunctor2D, 1 /* residuals */, 3 /* pose variables */>(
        new RotationDeltaCostFunctor2D(scaling_factor, target_angle));
  }

  template <typename T>
  bool operator()(const T* const pose, T* residual) const {
    residual[0] = scaling_factor_ * (pose[2] - angle_);
    return true;
  }

 private:
  explicit RotationDeltaCostFunctor2D(const double scaling_factor,
                                      const double target_angle)
      : scaling_factor_(scaling_factor), angle_(target_angle) {}

  RotationDeltaCostFunctor2D(const RotationDeltaCostFunctor2D&) = delete;
  RotationDeltaCostFunctor2D& operator=(const RotationDeltaCostFunctor2D&) =
      delete;

  const double scaling_factor_;
  const double angle_;
};

可以看出:

  • 残差项:一维 --> 角度
  • 待优化的参数:三维 --> x, y, theta
  • scaling_factor_为旋转误差的权重

4.总结

  • 可能有问题的点
    1.平移和旋转的残差项是逼近于先验位姿的, 当先验位姿不准确时会产生问题
  • 可能的改进建议
    1.先将地图的权重调大, 平移旋转的权重调小, 如 1000, 1, 1, 或者 100, 1, 1
    2.调参没有作用的时候可以将平移和旋转的残差项注释掉
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值