本文目的大概的讲一下整个代码框架,本来是想尽量这一篇写详细一点,后来时间不够了,而且一篇太长了阅读太难受,后面拆成了好多章去讲,这篇主要是拆官方文档,之后的篇章东西我也没有很透彻,有错误的地方欢迎指正。
从这章开始,借鉴了很多大佬的精华:
1.无处不在的小土
2.南北粉面馆
3.火种源码的机器人
4._Leveon
下面开始解读代码
官方文档(这里要,基本下面也就是照着官方文档说的,可以把下面当成官方文档的翻译)
Cartographer 可以被视为两个独立但相关的子系统。
第一个是局部SLAM(有时也称为前端或局部轨迹构建器)。它的工作是建立一系列的子图。每个子图都应该是局部一致的,但我们接受局部 SLAM 会随着时间的推移而漂移。大多数局部 SLAM 选项可以在install_isolated/share/cartographer/configuration_files/trajectory_builder_2d.lua(对于 2D)和install_isolated/share/cartographer/configuration_files/trajectory_builder_3d.lua(对于 3D)中找到。(对于本页的其余部分,我们将参考TRAJECTORY_BUILDER_nD的常用选项)。
另一个子系统是全局 SLAM(有时称为后端)。它在后台线程中运行,它的主要工作是找到闭环约束。它通过针对子图的扫描匹配扫描(主要是通过节点Node)来做到这一点。将位姿推移类传感器和前端结果按时间顺序进行串联起来形成一条轨迹,寻找闭环关系,最终采用优化求解获取位姿总体误差最小时的位姿轨迹即submap位姿。在 3D 中,它还试图找到重力的方向。它的大部分选项都可以在install_isolated/share/cartographer/configuration_files/pose_graph.lua 中找到。
更抽象的说,局部SLAM的工作是生成更好的子图,全局SLAM的工作是更好的将子图约束到一起。
测距传感器(例如:激光雷达)提供多个方向的深度信息。然而,一些测量与 SLAM 无关。如果传感器部分被灰尘覆盖,或者如果它指向机器人的一部分,则某些测量距离可以被视为 SLAM 的噪声。另一方面,一些最远的测量也可能来自不希望的来源(反射、传感器噪声),并且与 SLAM 无关。为了解决这些问题,Cartographer 首先应用带通滤波器,并且仅将范围值保持在某个最小和最大范围之间。应根据机器人和传感器的规格选择这些最小值和最大值。
TRAJECTORY_BUILDER_nD.min_range
TRAJECTORY_BUILDER_nD.max_range
在 2D 中,Cartographer 将比 max_range 更远的范围替换为TRAJECTORY_BUILDER_2D.missing_data_ray_length. 它还提供 amax_z和min_z值以将 3D 点云过滤成 2D 切割。
在 Cartographer 配置文件中,每个距离都以米为单位定义。
距离是在机器人实际移动的一段时间内测量的。然而,距离是由传感器在大量 ROS 消息中“批量”传递的。Cartographer 可以独立考虑每条消息的时间戳,以考虑机器人运动引起的畸变。Cartographer 获取测量值越频繁,就越能更好地解卷,即可以立即捕获的单个相干扫描。因此,强烈建议通过扫描提供尽可能多的距离数据(ROS 消息)(一组可以与另一次扫描匹配的距离数据)。
TRAJECTORY_BUILDER_nD.num_accumulated_range_data 这个值写略大就可以将一帧激光分几帧发出来用来处理激光的畸变问题。
距离数据通常是从机器人上的一个点测量的,但是是从多个角度测量的。这意味着靠近的表面(例如道路)反馈的点越多。相反,较远的物体被击中的频率较低并且提供的分数较少。为了减少点处理的计算量,我们通常需要对点云进行二次采样。然而,一个简单的随机抽样会从我们已经有低密度测量的区域中删除点,而高密度区域仍然会有比需要更多的点。为了解决这个密度问题,我们可以使用体素滤波器,将原始点下采样成一个恒定大小的立方体,并且只保留每个立方体的质心。
小的立方体尺寸将导致更密集的数据表示,从而导致更多的计算。大立方体会导致数据丢失,但速度会快得多。
TRAJECTORY_BUILDER_nD.voxel_filter_size 体素滤波器的参数是这个
在应用了固定大小的体素滤波器后,Cartographer 还应用了自适应体素滤波器。此过滤器尝试确定最佳体素大小(在最大长度下)以实现目标点数。在 3D 中,两个自适应体素滤波器用于生成高分辨率和低分辨率点云,它们的用法将在Local SLAM中阐明。
TRAJECTORY_BUILDER_nD.*adaptive_voxel_filter.max_length
TRAJECTORY_BUILDER_nD.*adaptive_voxel_filter.min_num_points
惯性测量单元可以成为 SLAM 的有用信息来源,因为它提供了准确的重力方向(因此是地面),以及全面但有噪声的旋转信息。为了过滤 IMU 噪声,在一定时间内观察重力。如果您使用 2D SLAM,则可以实时处理距离数据,而无需额外的信息源,因此您可以选择是否希望 Cartographer 使用 IMU。使用 3D SLAM,您需要提供 IMU,因为它用作扫描方向的初始猜测,大大降低了扫描匹配的复杂性。
TRAJECTORY_BUILDER_2D.use_imu_data
TRAJECTORY_BUILDER_nD.imu_gravity_time_constant
Cartographer可以将二维码、反光板等作为landmark,也就是当作特征点,这里先不讲,放在后续文档里讲解。
Cartographer可以将GPS当作fixed pose输入,目前没有条件,还没有测试过。
一旦从多个距离数据中组合并过滤了扫描,就可以使用局部SLAM算法了。局部 SLAM 通过使用来自姿态推测器的初始猜测进行扫描匹配,将新的扫描插入到其当前的子图构造中。姿势推测器背后的想法是使用除测距仪之外的其他传感器的传感器数据来预测下一次扫描应该插入到子图中的位置。(以前版本的位姿推测器是通过UKF去做的,后来应该是觉得UKF的输入太慢了,简化了这部分传感器融合的工作,位姿推测器部分后续会单独开一篇讲解)
有两种扫描匹配策略可用:
无论哪种方式,CeresScanMatcher都可以配置为为其每个输入赋予一定的权重。权重是对数据信任度的衡量标准,这可以看作是静态协方差。权重参数的单位是无量纲量,不能相互比较。一个数据源的权重越大,Cartographer 在进行扫描匹配时就会越重视这个数据源。数据源包括占用空间(来自扫描的点)、来自姿态推测器(或RealTimeCorrelativeScanMatcher)的平移和旋转。
下面三个参数是激光反馈点的权重
TRAJECTORY_BUILDER_nD.ceres_scan_matcher.occupied_space_weight
TRAJECTORY_BUILDER_3D.ceres_scan_matcher.occupied_space_weight_0
TRAJECTORY_BUILDER_3D.ceres_scan_matcher.occupied_space_weight_1
下面两个参数是位姿推测器的权重
TRAJECTORY_BUILDER_nD.ceres_scan_matcher.translation_weight
TRAJECTORY_BUILDER_nD.ceres_scan_matcher.rotation_weight
在 3D 中,occupied_space_weight_0和occupied_space_weight_1参数分别与高分辨率和低分辨率滤波点云相关。
当然也会有参数去配置 后端里odom 和前端 imu 的权重
扫描匹配问题被建模为此类问题的最小化,其中两次扫描之间的运动(变换矩阵)是要确定的参数。Ceres 使用下降算法针对给定的迭代次数优化运动。Ceres 可以配置为根据您自己的需要调整收敛速度。
TRAJECTORY_BUILDER_nD.ceres_scan_matcher.ceres_solver_options.use_nonmonotonic_steps
TRAJECTORY_BUILDER_nD.ceres_scan_matcher.ceres_solver_options.max_num_iterations
TRAJECTORY_BUILDER_nD.ceres_scan_matcher.ceres_solver_options.num_threads
可以根据您对传感器的信任进行切换为RealTimeCorrelativeScanMatcher,它通过在由最大距离半径和最大角度半径定义的搜索窗口中搜索类似扫描来工作。当与在此窗口中找到的扫描执行扫描匹配时,可以为平移和旋转分量选择不同的权重。例如,如果您知道您的机器人不经常旋转,您可以使用这些权重。
TRAJECTORY_BUILDER_nD.use_online_correlative_scan_matching
TRAJECTORY_BUILDER_nD.real_time_correlative_scan_matcher.linear_search_window
TRAJECTORY_BUILDER_nD.real_time_correlative_scan_matcher.angular_search_window
TRAJECTORY_BUILDER_nD.real_time_correlative_scan_matcher.translation_delta_cost_weight
TRAJECTORY_BUILDER_nD.real_time_correlative_scan_matcher.rotation_delta_cost_weight
为了避免每个子图插入过多的扫描,一旦扫描匹配器发现两次扫描之间的运动,它就会通过运动滤波器。如果导致扫描的运动被认为不够重要,则放弃扫描。只有当扫描的运动超过某个距离、角度或时间阈值时,才会将扫描插入到当前子图中。
TRAJECTORY_BUILDER_nD.motion_filter.max_time_seconds
TRAJECTORY_BUILDER_nD.motion_filter.max_distance_meters
TRAJECTORY_BUILDER_nD.motion_filter.max_angle_radians
这里可见代码中的IsSimilar
当局部 SLAM 接收到给定数量的范围数据时,子图被认为是完整的。局部 SLAM 随时间漂移,全局 SLAM 用于修复这种漂移。子图必须足够小,以便它们内部的漂移低于分辨率,以便它们在局部正确。另一方面,它们应该足够大以区分闭环以正常工作。
TRAJECTORY_BUILDER_nD.submaps.num_range_data
这里需要根据你使用的激光长度去修改每个submap的大小。
子图可以将其范围数据存储在几种不同的数据结构中:最广泛使用的表示称为概率网格。但是,在 2D 中,也可以选择使用截断有符号距离场 (TSDF)。
TRAJECTORY_BUILDER_2D.submaps.grid_options_2d.grid_type
概率网格将空间切割成 2D 或 3D 表格,其中每个单元格具有固定大小并包含被阻挡的几率。比率根据“命中”(测量范围数据的位置)和“未命中”(传感器和测量点之间的自由空间)更新。命中和未命中都可以在占用概率计算中具有不同的权重,从而或多或少地信任占用或空闲空间测量。
TRAJECTORY_BUILDER_2D.submaps.range_data_inserter.probability_grid_range_data_inserter.hit_probability
TRAJECTORY_BUILDER_2D.submaps.range_data_inserter.probability_grid_range_data_inserter.miss_probability
TRAJECTORY_BUILDER_3D.submaps.range_data_inserter.hit_probability
TRAJECTORY_BUILDER_3D.submaps.range_data_inserter.miss_probability
在 2D 中,每个子图只存储一个概率网格。在 3D 中,出于扫描匹配性能的原因,使用了两个混合概率网格。(术语“混合”仅指内部树状数据表示,并抽象给用户)
扫描匹配首先将低分辨率点云的远点与低分辨率混合网格对齐,然后通过将靠近的高分辨率点与高分辨率混合网格对齐来细化位姿。
TRAJECTORY_BUILDER_2D.submaps.grid_options_2d.resolution
TRAJECTORY_BUILDER_3D.submaps.high_resolution
TRAJECTORY_BUILDER_3D.submaps.low_resolution
TRAJECTORY_BUILDER_3D.high_resolution_adaptive_voxel_filter.max_range
TRAJECTORY_BUILDER_3D.low_resolution_adaptive_voxel_filter.max_range
当局部 SLAM 生成其连续的子图时,全局优化(通常称为“优化问题”或“稀疏姿态调整”)任务在后台运行。它的作用是重新排列彼此之间的子图,使它们形成一个连贯的全局图。例如,此优化负责更改当前构建的轨迹以正确对齐子图与闭环有关。
一旦插入一定数量的轨迹节点,优化就会分批运行。根据您需要运行它的频率,您可以调整这些批次的大小。
POSE_GRAPH.optimize_every_n_nodes
将 POSE_GRAPH.optimize_every_n_nodes 设置为 0 是禁用全局 SLAM 并专注于局部 SLAM 行为的便捷方法。这通常是调整 Cartographer 时要做的第一件事。
全局 SLAM 是一种“ GraphSLAM ”,它本质上是一种位姿图优化,通过在节点和子图之间建立约束,然后优化得到的约束图来工作。约束可以直观地被认为是把所有节点绑在一起的小绳索。稀疏的姿势调整将这些绳索完全固定。生成的网络称为“姿势图”。
POSE_GRAPH.constraint_builder.max_constraint_distance
POSE_GRAPH.fast_correlative_scan_matcher.linear_search_window
POSE_GRAPH.fast_correlative_scan_matcher_3d.linear_xy_search_window
POSE_GRAPH.fast_correlative_scan_matcher_3d.linear_z_search_window
POSE_GRAPH.fast_correlative_scan_matcher*.angular_search_window
为了限制约束(和计算)的数量,Cartographer 仅考虑所有冻结节点的子采样集来构建约束。这由采样率常数控制。采样过少的节点可能会导致遗漏约束和无效的闭环。采样太多节点会减慢全局 SLAM 并阻止实时闭环。
POSE_GRAPH.constraint_builder.sampling_ratio
POSE_GRAPH.global_sampling_ratio (重点,这俩是算力不够处理的主要对象)
当一个节点和一个子图被考虑用于约束构建时,它们会通过第一个扫描匹配器,称为FastCorrelativeScanMatcher. 该扫描匹配器专为 Cartographer 设计,可实现实时闭环扫描匹配。它FastCorrelativeScanMatcher依靠“分支定界”机制在不同的网格分辨率下工作并有效地消除不正确的匹配。此机制在本文档前面介绍的 Cartographer 论文中得到了广泛介绍。它适用于深度可以控制的探索树。
POSE_GRAPH.constraint_builder.fast_correlative_scan_matcher.branch_and_bound_depth
POSE_GRAPH.constraint_builder.fast_correlative_scan_matcher_3d.branch_and_bound_depth
POSE_GRAPH.constraint_builder.fast_correlative_scan_matcher_3d.full_resolution_depth
一旦FastCorrelativeScanMatcher有足够好的得分(高于匹配的最低分数),它就会被送入 Ceres Scan Matcher 以优化姿势。
POSE_GRAPH.constraint_builder.min_score
POSE_GRAPH.constraint_builder.ceres_scan_matcher_3d
POSE_GRAPH.constraint_builder.ceres_scan_matcher
当 Cartographer 运行优化问题时,Ceres 用于根据多个残差重新排列子图。残差是使用加权成本函数计算的。全局优化具有考虑大量数据源的成本函数:全局(闭环)约束、非全局(匹配器)约束、IMU 加速度和旋转测量、局部 SLAM 粗略姿态估计、里程计源或固定框架(如 GPS 系统)。权重和 Ceres 选项可以按照局部 SLAM部分中的说明进行配置。
POSE_GRAPH.constraint_builder.loop_closure_translation_weight
POSE_GRAPH.constraint_builder.loop_closure_rotation_weight
POSE_GRAPH.matcher_translation_weightPOSE_GRAPH.matcher_rotation_weight
POSE_GRAPH.optimization_problem.*_weight
POSE_GRAPH.optimization_problem.ceres_solver_options
这里是用来降低错误约束的
通过切换POSE_GRAPH.max_num_final_iterations
,可以找到有关优化问题中使用的残差的有用信息
作为 IMU 残差的一部分,优化问题为 IMU 姿势提供了一些灵活性,默认情况下,Ceres 可以自由优化 IMU 和跟踪框架之间的外部校准。如果您不信任您的 IMU 姿势,可以记录 Ceres 全局优化的结果并用于改进您的外部校准。如果 Ceres 没有正确优化你的 IMU 姿势并且你足够信任你的外部校准,你可以使这个姿势保持不变。
POSE_GRAPH.optimization_problem.log_solver_summary
POSE_GRAPH.optimization_problem.use_online_imu_extrinsics_in_3d
在残差中,异常值的影响由配置有特定 Huber 尺度的Huber 损失函数处理。
Huber 值越大,异常值(离群值)的影响就越大。
POSE_GRAPH.optimization_problem.huber_scale
这里可以用来应对对称环境
轨迹完成后,Cartographer 会运行新的全局优化,其迭代次数通常比以前的全局优化多得多。这样做是为了完善 Cartographer 的最终结果,通常不需要实时,因此大量迭代通常是正确的选择。
POSE_GRAPH.max_num_final_iterations
slam前端主要采用scan-match的相关技术,计算相邻激光点相对位姿,若已知初始位姿,则可获取所有scan节点全局绝对位姿。因其原理原因,会引入累计误差,但由于相对位姿较为精确,短时间内累加误差较小,cartographer采用一段时间内的scan构建submap而非整个map;每个submap相互独立,并且采用scan-to-map思想进行scan-match进一步降低累计误差,其结构如下所示。
从图中可看出N个连续scan构建submap0,然后接下来的N个连续scan构建新的submap1,以此类推。前端可获取每个scan的相对于对应submap的位姿,和submap在全局的位姿。
前端的处理流程如下:
1)传感器预处理:主要将测距传感器即激光传感器信息转换为cartographer内部定义的统一类型:点云类型,而非ros中的range类型;
2)激光点云数据经过两次体素滤波处理,即降采样;
3)odometry、imu及其scan-match精确位姿作为历史轨迹,构建位置估计器实时估计下刻位置;其中odometry和imu在2d slam非必要输入,可完全采用纯激光激光匹配估计。
4)根据预估位置作为初始位姿,经相关匹配(可配置使能或禁能)和ceres优化匹配获取精确的位置;
5)经过运动滤波器(即降采样)的scan节点作为slam轨迹节点;
6)将轨迹节点scan即及其对应的点云进行维护和更新submap;
7)当submap满足一定数量的激光帧时,完成此submap的创建,输出MatchingResult类型结果;
重新累计新的scan node构建新的submap,以此循环;
slam后端主要解决slam前端带来的累计误差,主要包括闭环检测和图优化求解两大部分。其中图优化求解目前一般都采用开源优化库,如google的ceres库和国内常用的g2o库;故目前slam质量的好坏,闭环检测成为了重中之重。如下给出cartographer进行后端优化位姿图结构拓扑,后端给出了相邻scan node的位姿约束,同时也给出了submap与组成其的scan node的约束关系,如下图在前端基础上增加了黑色线即为闭环检测提供的闭环约束。
在进行图优化求解前,构建位姿图。其中红色和绿色约束由前端匹配结果获取;而黑色线约束,即scannode 与历史的submap(非自身构建的submap)之间的相对位姿;或submap与历史的scan node(除构建自身的其他scannode)之间的相对位姿。
闭环检测
闭环检测本质仍为scan-match,只是采用当前scannode匹配历史的submap而非当前时刻的submap;或者采用当前完成的submap去匹配历史的scannode。当匹配的置信度(即可认为是吻合度)达到一定值时,可认为闭环存在,增加其约束关系。
由于闭环检测是检测当前位姿是否经过曾经经历的地方,和前端匹配本质区别,无法获取较为准确的初始位姿。因其搜索范围较大,则不适合直接使用real_time_correlative_scan_match。cartographer优化了相关匹配算法,提出了FastCorrelativeScanMatcher(即分支限界加速匹配法),实现快速搜索闭环可能性。
如果存在闭环,即匹配可信度超过设置阈值,将匹配出的位姿作为初始值送入ceres库获取更加精细的匹配结果,其结果即为闭环约束。
后端处理的流程如下:
1)将scannode和submap作为优化节点,加入位姿图;
2)从前端匹配结果中获取相邻scannode节点间约束和submap内部的submp与scannode约束;
3)创建闭环匹配器进行闭环检测,增加闭环约束;
4)构建位姿图优化器进行优化;
5)将优化结果将旧值进行更新;
6)由于后端处理不需要实时处理,为保证前端的实时,后端采用线程池在后台处理;因此优化的结果替换时无法替换所有scan node值,需将优化后最新的位姿转移矩阵,去估计前端中未被优化的节点。否则可能会导致在地图最新时间附近错位。
main
Run
XXX::RunPureLocalization,
Node::StartTrajectoryWithDefaultTopics
Node::AddTrajectory
MapBuilderBridge::AddTrajectory
MapBuilder::AddTrajectoryBuilder
CollatedTrajectoryBuilder::CollatedTrajectoryBuilder
TrajectoryCollator::AddTrajectory
其中,
XXX继承于Node,XXX的MapBuilder采用了CreateMapBuilder函数生成为MapBuilder类型,而XXX的MapBuilderBridge则引用了该MapBuilder,也就是说XXX中的MapBuilderBridge包含了MapBuilder的实例。而最终由MapBuilder的AddTrajectoryBuilder完成计算。
Node::HandlePointCloud2Message:
map_builder_bridge_->sensor_bridge(trajectory_id)->HandlePoint
Cloud2Message(sensor_id, msg);
SensorBridge::HandlePointCloud2Message:
this->HandleRangefinder(sensor_id, subdivision_time, frame_id, subdivision);
SensorBridge::HandleRangefinder
trajectory_builder_->AddSensorData(
sensor_id, carto::sensor::TimedPointCloudData{
time, sensor_to_tracking->translation().cast
carto::sensor::TransformTimedPointCloud(
ranges, sensor_to_tracking->cast
CollatedTrajectoryBuilder::::AddSensorData
CollatedTrajectoryBuilder::HandleCollatedSensorData
CollatedTrajectoryBuilder::AddData
TrajectoryCollator::AddSensorData
LocalSlamResult2D::AddToPoseGraph
PoseGraph2D::AddNode
SensorBridge::HandleLaserScanMessage()
SensorBridge::HandleRangefinder() trajectory_builder_->AddSensorData
CollatedTrajectoryBuilder::AddSensorData()
CollatedTrajectoryBuilder::AddData()
Collator::AddSensorData()
Collator::AddTrajectory()
CollatedTrajectoryBuilder::CollatedTrajectoryBuilder()
CollatedTrajectoryBuilder::HandleCollatedSensorData()
Dispatchable::AddToTrajectoryBuilder()
GlobalTrajectoryBuilder::AddSensorData()
LocalTrajectoryBuilder2D::AddRangeData()
LocalTrajectoryBuilder2D::AddAccumulatedRangeData()
LocalTrajectoryBuilder2D::ScanMatch()
如果用real_time_correlative_scan_matcher_,就进real_time_correlative_scan_matcher_.Match 否则不进
ceres_scan_matcher_.Match
AddNode()
PoseGraph2D::ComputeConstraintsForNode()
PoseGraph2D::ComputeConstraint()
ConstraintBuilder2D::MaybeAddGlobalConstraint()
ConstraintBuilder2D::ComputeConstraint()
接下来做各个scanmatch和分支定界
下面摘部分简要说明,没来得及写完的,先贴上来了
前端代码解析
后端代码解析
一、ceres_scan_matcher.cc这里前端和后端都会用到,我们统一说明,具体见: