(02)Cartographer源码无死角解析-(52) 2D点云扫描匹配→ceres扫描匹配:CeresScanMatcher2D→栅格地图残差

news2024/9/22 15:25:56

讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下:
(02)Cartographer源码无死角解析- (00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/127350885
 
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX官方认证
 

一、前言

在上一篇博客中,对ceres扫描匹配的平移与旋转优化部分进行了详细的讲解,知道关于ceres的使用核心在于禅茶的构建。不过,留下了两个疑问暂时比较迷糊:

疑问 1 \color{red}疑问1 疑问1 疑问 2 \color{red}疑问2 疑问2 整合:CeresScanMatcher2D::Match()函数中,为什么平移残差优化的目标是 target_translation(由推断器或者暴力匹配获得),为什么旋转的残差目标值设置为 ceres_pose_estimate[2]。暂且记住这两个疑问,看下接下来的分析中是否能够找到答案。

下面要分析的是 src/cartographer/cartographer/mapping/internal/2d/scan_matching/ceres_scan_matcher_2d.cc 文件中的 CeresScanMatcher2D::Match() 函数的如下部分:

  // 地图部分的残差
  CHECK_GT(options_.occupied_space_weight(), 0.);
  switch (grid.GetGridType()) {
    case GridType::PROBABILITY_GRID:
      problem.AddResidualBlock(
          CreateOccupiedSpaceCostFunction2D(
              options_.occupied_space_weight() /
                  std::sqrt(static_cast<double>(point_cloud.size())),
              point_cloud, grid),
          nullptr /* loss function */, ceres_pose_estimate);
      break;
    case GridType::TSDF:
      problem.AddResidualBlock(
          CreateTSDFMatchCostFunction2D(
              options_.occupied_space_weight() /
                  std::sqrt(static_cast<double>(point_cloud.size())),
              point_cloud, static_cast<const TSDF2D&>(grid)),
          nullptr /* loss function */, ceres_pose_estimate);
      break;
  }

由于目前使用的是概率栅格地图,所以只对上述的 case GridType::PROBABILITY_GRID 情况进行讲解,即其核心函数为 CreateOccupiedSpaceCostFunction2D(),该函数主要作用是构建一个 ceres::CostFunction 对象指针返回作为 problem.AddResidualBlock() 函数的第一个实参。这里与平移旋转的优化一样 loss function 设置为 nullptr 表示不使用损失函数,CreateOccupiedSpaceCostFunction2D() 函数最后接收的参数 ceres_pose_estimate 表示待优化位姿的初始值。

那么下面就来详细分析一下吧。
 

二、CreateOccupiedSpaceCostFunction2D()

首先这里粘贴一下该函数的代码:

// 工厂函数, 返回地图的CostFunction
ceres::CostFunction* CreateOccupiedSpaceCostFunction2D(
    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()); // 比固定残差维度的 多了一个参数
}

首先其是一个工厂函数,第一个形参表示 scaling_factor 表示残差结果的所缩放因子;第二个形参 point_cloud 表示需要进行扫描匹配的点云数据,也是待优化的点云数据;第三个形参 grid 存储了栅格地图的相关数据与信息,后续会使用到。

ceres::AutoDiffCostFunction 类模板根据根据上述的三个形参构建一个:

ceres::AutoDiffCostFunction<OccupiedSpaceCostFunction2D,
                                         ceres::DYNAMIC /* residuals */,
                                         3 /* pose variables */>

类对象,第一个模板参数OccupiedSpaceCostFunction2D是一个可调用类型(可以是仿函数,或者可调用对象);第二个非类型模板参数 ceres::DYNAMIC=-1 与上一篇博客讲解的有些不一样,这里是一个动态值,也就是说残差结果项是动态的,需要在程序执行过程中才能确定(主要与点云数量相关);第三个非类型模板参数3表示位姿,也就是前面参数 ceres_pose_estimate(位姿初始值) 的维度,简单的说,就是待优化变量的维度。

创建上述类模板的实例,需要传递两个参数,第一个参数为可调用类型 OccupiedSpaceCostFunction2D 的实例对象,第二个参数为 point_cloud.size(),只有在 ceres::DYNAMIC=-1,即残差项为动态的情况下,才需要传入该参数,其目的就是指定残差项的维度。
 

三、GridArrayAdapter

从上面的分析中可以明显知道,class OccupiedSpaceCostFunction2D 这个类是栅格地图优化的核心,不过在对齐进行讲解之前,先来看 class GridArrayAdapter 这个类,该类同样实现于 occupied_space_cost_function_2d.cc 文件中,且被包含在类 OccupiedSpaceCostFunction2D 之中。

GridArrayAdapter 类型实例主要用于创建类模板 ceres::BiCubicInterpolator 的实例对象,ceres::BiCubicInterpolator 的作用是进行双线性插值。其对需要进行插值的实例对象有一定要求,在本人人在 /usr/include/ceres/cubic_interpolation.h 中关于 ceres::BiCubicInterpolator 的介绍,可以找到如下内容:

// Given as input an infinite two dimensional grid like object, which
// provides the following interface:
//
//   struct Grid {
//     enum { DATA_DIMENSION = 1 };
//     void GetValue(int row, int col, double* f) const;
//   };
//
// Where, GetValue gives us the value of a function f (possibly vector
// valued) for any pairs of integers (row, col), and the enum
// DATA_DIMENSION indicates the dimensionality of the function being
// interpolated. For example if you are interpolating a color image
// with three channels (Red, Green & Blue), then DATA_DIMENSION = 3.

大概的意思:如果使用 ceres::BiCubicInterpolator 进行双线性插值,需要实现一个结构体(或者类) Grid,用该类创建的实例对象作为 ceres::BiCubicInterpolator 构造函数的实参。关于 Grid 这个结构体需要实现两个接口:①enum { DATA_DIMENSION = 1 }用于指定数据的维度;②GetValue函数,该函数返回指定行列(int row, int col)位置的value值。

源码中的 Grid 结构体,就是 class GridArrayAdapte,是严格按照上述要求进行实现的。并且还额外实现了 NumRows() 与 NumCols() 函数,在GetValue() 函数重被调用,需要注意的是,其对 map上下左右各增加 kPadding,也就是对 map 进行了填充。相关代码注释如下:

private:
  static constexpr int kPadding = INT_MAX / 4;
  
  // 自定义网格
  class GridArrayAdapter {
   public:
    // 枚举 DATA_DIMENSION 表示被插值的向量或者函数的维度
    enum { DATA_DIMENSION = 1 };

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

    // 获取栅格free值
    void GetValue(const int row, const int column, double* const value) const {
      // 处于地图外部时, 赋予最大free值
      if (row < kPadding || column < kPadding || row >= NumRows() - kPadding ||
          column >= NumCols() - kPadding) {
        *value = kMaxCorrespondenceCost;
      } 
      // 根据索引获取free值
      else {
        *value = static_cast<double>(grid_.GetCorrespondenceCost(
            Eigen::Array2i(column - kPadding, row - kPadding)));
      }
    }

    // map上下左右各增加 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_;
  };

实现了 GridArrayAdapter 之后,后续就能够通过如下代码进行双线性插值了:

    const GridArrayAdapter adapter(grid_);
    ceres::BiCubicInterpolator<GridArrayAdapter> interpolator(adapter);

关于双线性插值的知识,这里就不进行讲解了,不是很明白的朋友可以查阅一些其他的资料。总的来说,没有插值之前,我们只能通过整数索引 (int row, const int column) 获得栅格值,但是通过插值之后,就可以通过小数索引获得栅格值了。
 

四、OccupiedSpaceCostFunction2D

首先这里讲解一下优化的原理:①待优化的参数为机器人位姿,即 CeresScanMatcher2D::Match() 函数中ceres_pose_estimate变量,其使用二维的方式表示机器人位姿,即位置(x,y),角度(angle)。②优化之后的最终结果是希望所有点云数据都能够打在障碍物上,即点云数据对应的栅格值(没有被占用概率)越小越好,也就说,点云对应的栅格值越小,则其被占用的机率越大。

1.源码注释

先简单过一下源码的注释,然后再对其重难点进行分袖

class OccupiedSpaceCostFunction2D {
 public:
  OccupiedSpaceCostFunction2D(const double scaling_factor,
                              const sensor::PointCloud& point_cloud,
                              const Grid2D& grid)
      : scaling_factor_(scaling_factor),//残差项的缩放因子
        point_cloud_(point_cloud),//当前扫描匹配的点云数据
        grid_(grid) {}//存储有地图栅格信息

  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]);//二维角度(姿态)
    //把角度使用2x2矩阵形式表示
    Eigen::Matrix<T, 2, 2> rotation_matrix = rotation.toRotationMatrix();

    /*姿态与位姿都添加到3x3的矩阵中,构成类似: r1 r2  tx 的矩阵形式
                                           r3 r4  ty    
                                           0. 0.  1.          */
    Eigen::Matrix<T, 3, 3> transform;
    transform << rotation_matrix, translation, T(0.), T(0.), T(1.);

    //对地图进行双线性插值,ceres的基本使用形式
    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.
      // 由于点云数据经过重力校正,所以只取x,y坐标
      const Eigen::Matrix<T, 3, 1> point((T(point_cloud_[i].position.x())),
                                         (T(point_cloud_[i].position.y())),
                                         T(1.));
      // 根据预测位姿对单个点进行坐标变换
      // 这里把point看成一个向量, 该向量的原点未local系, 向量的长度
      // 该向量的长度点云数据的模,所以还需要对齐进行变换,等价于把
      // 该向量的原点移动至Robot系原点,但是点云数据的数字是相对于loacl系的
      const Eigen::Matrix<T, 3, 1> world = transform * point;
      
      // 获取三次插值之后的栅格free值, Evaluate函数内部调用了GetValue函数
      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]);
      // free值越小, 表示占用的概率越大
      // 希望residual[i]为0,即希望每个点云都打在障碍物上,也就是等价
      // 点云对应栅格值越小越好
      residual[i] = scaling_factor_ * residual[i];
    }
    return true;
  }

 private:
  static constexpr int kPadding = INT_MAX / 4;
  
  OccupiedSpaceCostFunction2D(const OccupiedSpaceCostFunction2D&) = delete;
  OccupiedSpaceCostFunction2D& operator=(const OccupiedSpaceCostFunction2D&) =
      delete;

  const double scaling_factor_;
  const sensor::PointCloud& point_cloud_;
  const Grid2D& grid_;
};

2.重难点分析

其上的大部分代码都是比较容易理解的,但是对于如下部分代码或许存在一定疑问:

 	const Eigen::Matrix<T, 3, 1> world = transform * point;

下面是本人绘画的图示,①黄色圆点表示点云数据;②紫色五边形表示机器人所在位置及朝向;③黑色矩形表示障碍物;
在这里插入图片描述
从上图可以看到,点云数据虽然位于local坐标系下,但是很明显是不能计算残差的。需要把点云数据变换到如下位置才能进行残差计算:
在这里插入图片描述
上述源码就是为了实现上述功能,需要注意的是,源码中的 point 与 world 都是基于local 系的。从上图很容易看出,point 基于local系进行平移与旋转即可变换成 world,其平移与旋转的量就是机器人在local系下的位姿。使用数学公式表示则如下所示: p o i n t t r a c k i n g l o c a l = R o b o t t r a c k i n g l o c a l ∗ p o i n t l o c a l (01) \color{Green} \tag{01} point^{local}_{tracking}=\mathbf {Robot}^{local}_{tracking}*point^{local} pointtrackinglocal=Robottrackinglocalpointlocal(01)
p o i n t t r a c k i n g l o c a l point^{local}_{tracking} pointtrackinglocal 表是的含义就是对 local 系下的向量,起点为 tracking(机器人),终点为点云数值。
 

五、结语

通过上一篇博客与该篇博客的讲解,可以知道扫描匹配核心函数 CeresScanMatcher2D::Match() 主要是对初始位姿 ceres_pose_estimate 进行优化,优化主要包含如下三个步骤:

①首先根据点云数据,结合栅格地图进行位姿优化,尽量让所有的点云数据都打在障碍物上。
②以推断器推断出来位置target_translation作为优化目标,计算x,y的残差。
③以ceres_pose_estimate[2]作为角度优化目标,该残差也只计算了角度这一项。

这里再重述一下上一篇博客遗留的疑问→ 疑问 1 \color{red}疑问1 疑问1 疑问 2 \color{red}疑问2 疑问2 整合:CeresScanMatcher2D::Match()函数中,为什么平移残差优化的目标是 target_translation(由推断器或者暴力匹配获得),为什么旋转的残差目标值设置为 ceres_pose_estimate[2]。

CeresScanMatcher2D::Match() 的三个残差块都可以通过配置文件中的 occupied_space_weight、translation_weight、rotation_weight 对残差结果进行缩放。

总的来说,Cartographer源码是比较信任推断器的的,所以②使用target_translation作为优化目标,起到一个约束作用,避免优化过程中跑偏了。①的优化可能会导致角度发生得到优化,即ceres_pose_estimate[2]发生变化,但是源码中似乎认为推断器推断出来的姿态是十分准确的,并不想对齐进行优化,所以用初值作为优化目标,尽量保证其值不变。

 
 
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/188418.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

上海洲邦携手图扑建设数字孪生工厂,获 2022 智能制造优秀场景

前言 12 月 2 日&#xff0c;工信部公示了 2022 年度智能制造示范工厂揭榜单位和优秀场景名单。图扑软件和上海洲邦合作建设的宁波甬友数字孪生工厂被评为优秀场景&#xff0c;全国共有 369 个智能制造典型场景入选。 《智能制造试点示范行动实施方案》包括智能制造优秀场景和…

ESP-IDF:快速排序测试

ESP-IDF:快速排序测试 /快速排序测试/ void printArray(int arr[], int len) { for (int i 0; i < len; i) { cout << arr[i] << " "; } cout << endl; } void QuickSort(int arr[], int start, int end) { int i start; int j end; // 找…

【IoT】蔽障智能车设计:带有无线调试系统的蔽障小车

说明 近年来随着计算机在社会领域的渗透和大规模集成电路的发展&#xff0c;单片机的应用正在不断地走向深入&#xff0c;由于它具有功能强&#xff0c;体积小&#xff0c;功耗低&#xff0c;价格便宜&#xff0c;工作可靠&#xff0c;使用方便等特点&#xff0c;因此越来越广…

Activiti7工作流-使用idea插件actiBPM设计请假流程---工作流工作笔记005

首先我们看一下我们之前写的测试类, ProcessEngine 这个流程引擎,打开 看源码可以看到有几个常用的, HistoryService就是用来管理之前_hi表的,关于历史的 TaskService用来管理任务的 RuntimeService用来管理_run那些运行时的表的 然后我们还可以通过,上面定义的processEngi…

程序员的成长离不开哪些软技能?

道破一个残忍的真相&#xff1a;一个程序员的成长往往是软技能&#xff1e;硬技能&#xff0c;想纯靠技术能力一路挺过35岁基本是不可能的任务&#xff0c;作为一个优秀的程序员&#xff0c;离不开以下这些软技能。 较真 所有在技术上的较真都是一次专业技能的提升&#xff0…

Ruoyi-Cloud框架学习-【07 新增功能模块】

根据该博客新建模块后&#xff0c;在添加具体的数据表实现增删改查 在ruoyi-cloud中建立数据表 新建business_device表中【与gen_table、gen_table_column在一个库】 CREATE TABLE business_device (id bigint NOT NULL AUTO_INCREMENT COMMENT 主键,name varchar(255) COLLA…

USB转双串口芯片 CH342 兼容替代CP2105

概述CH342是一个USB总线的转接芯片&#xff0c;实现USB转两个异步串口。每个串口都支持高速全双工&#xff0c;支持奇偶校验&#xff0c;提供了常用的MODEM联络信号&#xff0c;用于为计算机扩展异步串口&#xff0c;或者将普通的串口设备或者MCU直接升级到USB总线。特点全速US…

PySpark数据分析基础:核心数据集RDD原理以及操作一文详解(一)

目录 前言 一、RDD概念 二、RDD与DataFrame之间的区别 特性区别 本质区别 三、PySpark中RDD的操作 1.aggregate 2.aggregateByKey 3.map 4.mapPartitions 5.getNumPartitions 6. glom() 点关注&#xff0c;防走丢&#xff0c;如有纰漏之处&#xff0c;请留言指教&am…

< 每日算法 - JavaScript解析:用两个栈实现队列 >

每日算法 - JavaScript解析&#xff1a;用两个栈实现队列一、任务描述&#xff1a;拓展知识二、问题解析三、解决方案&#xff1a;参考文献往期内容 &#x1f4a8;一、任务描述&#xff1a; 用两个栈实现一个队列。队列的声明如下&#xff0c;请实现它的两个函数 appendTail 和…

JS扩展运算符(...)的用法

参考链接&#xff1a; 本文可以说是完全抄写的参考链接的内容&#xff0c;本文的目的只用于自己进行记录&#xff0c;加深理解。原文参考链接 一、定义&#xff1a; 扩展运算符(...)是ES6的新增语法&#xff0c;用于取出参数对象中的所有可遍历属性&#xff0c;然后拷贝到当前…

vue移动端适配方案

一、基于rem的适配方案 rem是什么&#xff1f; rem是指相对于根元素的字体大小的单位&#xff0c;在日常开发过程中我们通常把根元素&#xff08;html/body&#xff09;的字体设置为10px,方便于我们计算&#xff08;此时子元素的1rem就相当于10px&#xff09;。 适用场景 不…

开学季,给上小学的孩子送什么比较好?小学生比较实用的礼物

很快来临开学的日子&#xff0c;对于上小学的孩子们&#xff0c;送写学习用具之类&#xff0c;或者是孩子自身喜欢的玩具也可以的&#xff0c;但是因为大部分小朋友在学习时都会眨眼睛&#xff0c;也有些近视的现象了&#xff0c;所以我会选择送护眼的用具比较合适的&#xff0…

DBCO-SS-NHS;CAS NO:1435934-53-4 二苯并环辛炔-二硫键-活性酯,科研实验用试剂

DBCO-SS-活性酯|DBCO-SS-NHS酯 名称;二苯并环辛炔-二硫键-活性酯&#xff1b;DBCO-SS-NHS CAS NO&#xff1a;1435934-53-4 分子量&#xff1a;565.66 分子式&#xff1a;C28H27N3O6S2 含 量&#xff1a;>95% 用 途&#xff1a;仅供科研实验使用&#xff0c;不用于诊治…

Kettle(三)数据写入错误如何处理?

Kettle作为数据治理工具&#xff0c;总是会涉及到数据的搬迁&#xff0c;在原始数据过大、种类过多时写入肯定会出现一些错误&#xff08;缺少必填项、数据格式错误、主键冲突等等&#xff09;&#xff0c;那么Kettle如何解决&#xff1f;其实Kettle与我们自己写程序的思维不会…

从零开发一个非常有意思的 Python 项目:手势识别

最近开发了一个手势处理的项目&#xff08;零基础也可以学&#xff0c;就是针对零基础的&#xff09;&#xff0c;我在这儿简单的复述一下原理&#xff0c;总体来说还是比较简单的&#xff0c;主要运用的知识就是opencv&#xff0c;python基本语法&#xff0c;图像处理基础知识…

OCR文字识别技术

OCR全称是optical character recognition&#xff0c;中文光学字符识别。主要技术是&#xff1a;把图像形状转变为文本字符。简单来说&#xff0c;OCR技术就是通过图像处理和模式识别技术对光学的字符进行识别&#xff0c;即&#xff0c;对文本资料进行扫描&#xff0c;然后对图…

呕心沥血保姆级Fiddler移动端抓包

本篇文章&#xff0c;博主想使用通俗易懂的话语&#xff0c;让大家明白以下内容&#xff1a;什么是抓包哪些场景需要用到抓包Fiddler抓包的原理怎样使用Fiddler进行移动端抓包抓包包 (Packet) 是TCP/IP协议通信传输中的数据单位&#xff0c;一般也称“数据包”。我们平常测试过…

PCI设备的访问方法_非桥设备(type0)

PCI设备的访问方法_非桥设备(type0) 文章目录PCI设备的访问方法_非桥设备(type0)一、硬件结构二、 PCI本地总线的信号三、 访问PCI设备3.1 怎么访问PCI设备3.1 PCI设备的地址空间3.2 访问PCI设备的步骤3.3 示例&#xff1a;配置PCI Agent设备致谢一、硬件结构 PCI系统框图&…

2023年第七届航空航天、机械与机电工程国际会议(CAMME 2023)

2023年第七届航空航天、机械与机电工程国际会议(CAMME 2023) 重要信息 会议网址&#xff1a;www.camme.org 会议时间&#xff1a;2023年5月26-28日 召开地点&#xff1a;中国西安 截稿时间&#xff1a;2023年4月26日 录用通知&#xff1a;投稿后2周内 收录检索&#xff1a;EI,S…

ORA-01691: unable to extend lob segment

报错信息&#xff1a; ORA-01691: unable to extend lob segment YUNYAO.SYS_LOB0000152921C00013$$ by 1024 in tablespace USERS 针对此问题查了一些文章&#xff0c;排查到原因是表空间“USERS”的数据文件满了&#xff0c;到达最大值。 可能会出现的原因如下&#xff1a…