cartographer 代码分析

相关注释代码链接为:cartographer代码注释
在这里插入图片描述

代码主要分为两个部分,其一为cartographer的核心实现,另一个为cartographer的ros封装壳。首先介绍其ros封装,可以看到大概的调用流程,然后再深入源码去剖析其实现过程。但是其代码可以说十分的繁琐且复杂,在只能大致理清楚其逻辑。

Cartographer-ROS

根据运行的命令 roslaunch cartographer_ros offline_backpack_2d.launch bag_filenames:=${HOME}/Downloads/b2-2016-04-05-14-44-52.bag可以知道,运行文件offline_backpack_2d.launch启动算法。

  • 加载配置文件 backpack_2d.lua
  • 调用launch文件 offline_node.launch

因此,深入文件 offline_node.launch

  • 运行节点 rviz
  • 运行节点 cartographer_occupancy_grid_node
  • 运行节点 cartographer_offline_node

因此对于cartographer-ros来说,主要的节点就是这两个,其在如下文件中分别实现

  • offine_node_main.cc
  • occupancy_grid_node.cc

occupancy_grid_node

在这里插入图片描述

主要实现的功能有

  • 新建一个定时器,定时发布全局地图信息
  • 构造回调函数,在回调函数内处理子地图列表信息

其子列表信息回调函数流程如下:

  • 设置所有之前的子地图为待删除子地图
  • 在待删除子地图中去掉当前子地图列表中还存在的
  • 获取新子地图信息 FetchSubmapTextures
    • 发布一个srv,获取压缩后的子地图栅格信息
    • 对栅格地图信息进行解压
  • 转换子地图信息格式

而发布流程则为

  • 将所有子地图构造成一个图片(调用cairo实现)
  • 转换为ROS格式并发布

offine_node_main

在main函数中

  • 调用函数CreateMapBuilder构造了一个MapBuilder
  • 调用RunOfflineNode函数,传入MapBuilder类指针

其中,RunOfflineNode函数则为离线运行节点的主要逻辑

在这里插入图片描述

可以从图中看到,函数主要工作为

  • 调用AddOffineTrajectory()函数,实际上该函数主要调用的是map_builder类的AddTrajectoryBUilder函数。
  • 创建ROS相关的发布以及订阅消息的处理,并定时发布可视化信息
  • 构造SensorBridge类,并处理传感器数据,实际上则是调用TrajectoryBuilderInterface类的传感器数据处理
  • 读取参数配置文件,保存地图信息等其他工作

从输入输出的角度来看整体的代码,我们需要弄清楚本节点发布的ROS数据具体有哪些,是如何获得的,并且是如何处理这些传感器数据。

输入

  • 传递传感器数据到核心cartographer代码
    直接由SensorBridge类使用TrajectoryBuilderInterface类指针调用其传感器数据处理函数。

输出

  • 发布消息到ROS
    • 定时发送轨迹、子地图列表、约束项等信息
    • 在请求时返回子地图的栅格地图数据,调用函数HandleSubmapQuery,实际上调用的是map_builder_->SubmapToProto函数获取压缩后的栅格信息

因此,整理如下

  • 调用map_builder类的AddTrajectoryBuilder函数,进行初始化
  • 调用TrajectoryBuilderInterface类相关的传感器处理函数处理传感器信息
  • 调用map_builder_->SubmapToProto函数等获取处理结果

最终,我们可以看到实际上交互的内容并不多,在获取栅格地图信息上由于数据量较大进行了数据压缩,其他的都是直接通过指针获取得到数据。因此我们接下来看主体代码时只要集中在前面两点上即可。

Cartographer 主体

根据上文结论,接下来分为两个部分进行介绍。

调用map_builder类的AddTrajectoryBuilder函数

这里根据配置参数的选择,才有了2D激光数据和3D激光数据的区别,根据不同的传感器数据配置2D或者3D对应的类,由于在代码上没有太大的区别逻辑都是几乎一模一样的(在处理IMU数据上有一些区别,3D激光必须要IMU,2D可以不要)。因此后面都是以2D类举例解释。

  1. 构造局部轨迹生成类 LocalTrajectoryBuilder2D
  2. 使用局部轨迹生成类,构造全局轨迹生成类 GlobalTrajectoryBuilder
  3. 使用全局轨迹生成类,构造轨迹生成管理类指针 CollatedTrajectoryBuilder
  4. 将轨迹生成管理类指针保存在一个vector中

在这里插入图片描述

这里就非常的绕,需要仔细思考,所有的轨迹生成类均为TrajectoryBuilderInterface的子类,因此需要特别注意,使用TrajectoryBuilderInterface类指针是具体指向的是哪一个子类的实现。最后返回的是CollatedTrajectoryBuilder类的指针,因此下面调用的是该之类的传感器数据处理函数。

  • 其他工作
    • 纯定位模式的配置
    • 初始位置的设置

调用TrajectoryBuilderInterface类函数处理传感器数据

由上面分析可知这里的TrajectoryBuilderInterface类是父类,而实际调用的是子类CollatedTrajectoryBuilder

CollatedTrajectoryBuilder

主要功能:

  • 构造Collator类,管理所有的传感器数据,设置回调函数为HandleCollatedSensorData(),在Collator类内所有的传感器数据都被表达成通用的传感器数据结构。
  • 调用Collator类处理传感器数据,在将传感器数据转换成通用数据后,调用回调函数HandleCollatedSensorData()
  • 在回调函数中调用全局轨迹生成类GlobalTrajectoryBuilder对传感器数据进行处理
GlobalTrajectoryBuilder

在全局轨迹生成类中将传感器数据进行了分类,其中激光雷达数据、IMU数据和里程计数据用于生成局部轨迹,其他的传感器数据如GPS信息、路标点信息等则直接被添加到了位姿图优化类PoseGraph2D类中。

  • 针对里程计、IMU和激光信息,调用局部路径生成LocalTrajectoryBuilder2D类进行处理
  • 处理结束后将其添加到PoseGraph2D类中进行优化
  • 将其他传感器数据同样添加到PoseGraph2D类中进行优化

因此,具体的实现部分在LocalTrajectoryBuilder2D类中添加传感器数据部分,以及PoseGraph2D类中添加传感器数据作为节点部分。

LocalTrajectoryBuilder2D
  • 构造PoseExtrapolator位姿外推类,类似于卡尔曼滤波器的功能,利用之前的传感器信息和当前激光采集时间,预测当前位置和速度。
  • 根据预测速度,对激光数据进行运动畸变矫正
  • 调用CeresScanMatcher2D类,进行激光数据前端匹配的计算当前帧与当前子地图的位置关系。
  • 激光达到一定距离则调用submap_2d类插入到子地图中
  • 利用匹配结果更新PoseExtrapolator的估计,为下一次做准备

其中,CSM前端匹配算法是,子地图-激光帧的匹配。具体原理部分可参考原论文,这里的重点不是理论。另外submap_2d类插入激光数据到子地图中,对子地图进行了管理,具体的子地图管理策略为。
在这里插入图片描述

其中,激光数据插入地图调用的是probability_grid_range_data_inserter_2d类中的函数,其原理为占用栅格地图更新原理,可参考注释和相关资料理解,这里不再赘述。

PoseGraph2D

在这里插入图片描述

  • 在收到局部轨迹生成类得到的子地图后,首先添加到构造的优化问题OptimizationProblem2D类中。
  • 然后调用fast_correlative_scan_matcher_2d.cc文件中的算法进行回环,主要就是利用分支定界算法在一定大小的窗口内进行搜索匹配。
  • 回环检测结束后,无论是否成功都将优化求解问题添加到线程池中
  • 其他传感器数据采集到也会将优化问题添加到线程池中进行求解

分支定界方法用于寻找回环约束的具体实现与原理较为复杂,可以参考论文和代码注释进行学习,这同样不是本文的重点。最后,将所有传感器的数据添加到OptimizationProblem2D类中构造了后端优化问题,接下来我们将求解这样一个最终的优化问题。

OptimizationProblem2D

优化问题的求解调用函数OptimizationProblem2D::Solve,其主要流程和普通的Ceres优化流程没有什么区别,就是按照Ceres的套路来。其中最为关键问题在于图优化中的节点和边如何构造以及误差函数的计算,这里我们采用因子图的方式表达这样一个图模型。
在这里插入图片描述

最后我们看一下误差函数,定义在SpaCostFunction2D类中,非常非常的简单,就是优化结果不能和之前匹配得到的相对位置相差太多。

  bool operator()(const T* const start_pose, const T* const end_pose,
                  T* e) const {
    // 误差计算函数  
    // ScaleError 基于旋转和平移项不同的权重,保证收敛
    // ComputeUnscaledError 真正计算误差的函数
    const std::array<T, 3> error =
        ScaleError(ComputeUnscaledError(
                       transform::Project2D(observed_relative_pose_.zbar_ij),
                       start_pose, end_pose),
                   observed_relative_pose_.translation_weight,
                   observed_relative_pose_.rotation_weight);
	// 保存误差
    std::copy(std::begin(error), std::end(error), e);
    return true;
  }

这里里程计的误差函数和匹配的误差函数是相同的,可以认为结果是两个里程计的可变加权和,其他的误差函数如路标点等这里不再过多展开,都是一样的。

至此,我们将cartographer代码的整体流程过了一遍,其原理不难,但是源码过于复杂,其中还有许许多多的细节,需要花费大量时间仔细阅读才能真正熟悉它。

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Cartographer 是一款 Google 开源的用于实时构建 2D 和 3D 室内地图的工具,主要用于在机器人等移动设备上进行定位和建图任务。本文将对 Cartographer 的源代码进行分析解读,以帮助读者更好地理解该工具的实现原理。 1. 源码结构 Cartographer 的源码分为多个部分,包括: - cartographer:主要代码库,包含地图构建、局部地图匹配、位姿估计等核心功能。 - cartographer_ros:ROS wrapper,将 cartographer 代码库与 ROS 框架进行了整合,提供了 ROS 接口。 - cartographer_rviz:RViz 插件,用于可视化展示地图、轨迹等信息。 - cartographer_android:Android 版本,用于在 Android 系统上运行 Cartographer。 2. 核心算法 Cartographer 采用了多种算法来实现室内地图的构建和定位,其中比较重要的算法包括: - 位姿图优化(Pose Graph Optimization):通过对传感器数据进行多次优化,得到机器人在运动过程中的位置和方向信息。 - 实时建图(Real-Time Mapping):使用激光雷达等传感器,实时获取机器人周围环境的信息,构建室内地图。 - 局部地图匹配(Local Submap Matching):将当前传感器数据与已构建的局部地图进行匹配,从而得到更准确的位置信息。 - 点云滤波(Point Cloud Filtering):将激光雷达获取的点云数据进行滤波处理,去除噪声和无效数据。 3. 关键函数解析 以下是 Cartographer 中比较重要的几个函数的解析: - cartographer/mapping/internal/pose_graph_2d.cc: PoseGraph2D::AddNode():向位姿图中添加新的节点,包括节点 ID、位姿信息等。 - cartographer/mapping/internal/pose_graph_2d.cc: PoseGraph2D::AddConstraint():向位姿图中添加新的约束,包括约束类型、起始和终止节点 ID、约束值等。 - cartographer/mapping/internal/scan_matching/ceres_scan_matcher.cc: CeresScanMatcher::Match():使用 Ceres 库实现激光雷达数据与局部地图的匹配过程。 - cartographer/mapping/internal/3d/optimization/optimization_problem_3d.cc: OptimizationProblem3D::Solve():使用 Ceres 库实现位姿图优化过程,得到机器人在运动过程中的位置和方向信息。 - cartographer/mapping/internal/3d/scan_matching/real_time_correlative_scan_matcher_3d.cc: RealTimeCorrelativeScanMatcher3D::Match():实时建图过程中使用的一种激光雷达数据与局部地图的匹配算法。 4. 开发环境 Cartographer 的开发环境需要使用 Google 推荐的 Bazel 构建系统,以及 C++11 编译器和 ROS 框架。具体的开发环境搭建和编译过程可以参考 Cartographer 的官方文档。 5. 总结 Cartographer 是一款非常优秀的室内地图构建和定位工具,在机器人、自动驾驶等领域有着广泛的应用。本文对 Cartographer 的源代码进行了分析解读,希望能够帮助读者更好地理解该工具的实现原理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值