讲解关于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→官方认证
一、前言
通过前面一系列的博客,PoseExtrapolator 进行了比较细致的分析。到目前为止,对于点云数据的预处理过程可以说时十分了解了,如:点云数据多传感器时间同步、运动畸变校正、重力校正、体素滤波等。做完这一系列的预备工作之后,实际上呢,就可以进行点云的扫描匹配了。
在讲解扫描匹配之前,先来看看 Cartographer 2D 的栅格地图,其不像3D点云地图有很多成熟的库可以调用,具有统一的标准。大多数 2D Slam 的栅格地图都是需要自己编写代码进行构建的。下面就来看看 Cartographer 中时如何构建的。
关于2D栅格地图的构建主要涉及到如下几个类,后续会分别对齐进行详细的讲解:
ActiveSubmaps2D
Submap2D Submap 子父类关系
Grid2D ProbabilityGrid 子父类关系
RangeDataInserterInterface ProbabilityGridRangeDataInserter2D//子父类关系
二、类间关系
先来看看源码中时如何把这些类,或者类对象关联起来的
1、ActiveSubmaps2D
在 LocalTrajectoryBuilder2D::AddAccumulatedRangeData() 函数中,可以找到如下代码:
// 将校正后的雷达数据写入submap
std::unique_ptr<InsertionResult> insertion_result = InsertIntoSubmap(
time, range_data_in_local, filtered_gravity_aligned_point_cloud,
pose_estimate, gravity_alignment.rotation());
该处对 InsertIntoSubmap() 函数的调用,起到了与类 ActiveSubmaps2D 的交互。因为 LocalTrajectoryBuilder2D::InsertIntoSubmap() 函数中,使用到类 LocalTrajectoryBuilder2D 的成员对象:
ActiveSubmaps2D active_submaps_;
该成员对象在 LocalTrajectoryBuilder2D 构造函数的初始化列表中被赋予初值,初始化列表可以看到如下代码:
active_submaps_(options.submaps_options())
其首先根据配置文件中的 submaps 信息,构建 ActiveSubmaps2D 对象,然后赋值给 active_submaps_。配置文件路径为:
src/cartographer/configuration_files/trajectory_builder_2d.lua
src/cartographer/configuration_files/trajectory_builder_2d.lua
2、Submap2D 与 Submap
类 ActiveSubmaps2D 包含成员变量 Submap2D ,如下所示:
std::vector<std::shared_ptr<Submap2D>> submaps_;
另外 Submap2D 为 Submap 的派生类,其都实现于文件 src/cartographer/cartographer/mapping/2d/submap_2d.cc 之中,Submap2D 存在两个重载构造函数,其都会构件 Grid2D 实例对象,然后赋值给成员变量:
std::unique_ptr<Grid2D> grid_; // 地图栅格数据
3、Grid2D
Grid2D 继承自 src/cartographer/cartographer/mapping/grid_interface.h 文件中的 GridInterface,GridInterface 比较简单,仅仅几句代码而已。另外 Grid2D 包含成员变量 MapLimits limits_。另外,Grid2D 同时也是一个基类,如 ProbabilityGrid 与 TSDF2D 都为其派生类。
4、ProbabilityGrid
通过 src/cartographer/cartographer/mapping/2d/submap_2d.cc 文件中的 ActiveSubmaps2D::AddSubmap() 函数,可以得知在构建 Grid2D 对象的时候,其调用的 ActiveSubmaps2D::CreateGrid() 函数,该函数会根据不同的配置信息构件 ProbabilityGrid 或者是 TSDF2D 对象。
5、ProbabilityGridRangeDataInserter2D
调用 Submap2D::InsertRangeData() 函数,其需要传递一个 RangeDataInserterInterface 类型的指针对象,从命名可以看出其为一个接口,源码中实际上传入的实参对象由 ActiveSubmaps2D::CreateRangeDataInserter() 函数决定,其派生类 ProbabilityGridRangeDataInserter2D 与 TSDFRangeDataInserter2D 主要是负责数据插入的功能。ActiveSubmaps2D::CreateRangeDataInserter() 函数也是在 ActiveSubmaps2D 的构造函数中被调用。
三、ActiveSubmaps2D
首先我们来看看 ActiveSubmaps2D 这个类,其主要负责与 LocalTrajectoryBuilder2D 的交互,同时内部再通过 Submap2D、Submap 、Grid2D 、ProbabilityGrid 、ProbabilityGrid 、ProbabilityGridRangeDataInserter2D 这几个类完成地图的保存与插入。
前面已经介绍过,ActiveSubmaps2D 的实例对象在 LocalTrajectoryBuilder2D 构造函数中根据:
src/cartographer/configuration_files/trajectory_builder_2d.lua
src/cartographer/configuration_files/trajectory_builder_2d.lua
配置文件中的如下参数进行构件:
-- 子图相关的一些配置
submaps = {
num_range_data = 90, -- 一个子图里插入雷达数据的个数的一半
grid_options_2d = {
grid_type = "PROBABILITY_GRID", -- 地图的种类, 还可以是tsdf格式
resolution = 0.05, --分辨率
},
range_data_inserter = {
range_data_inserter_type = "PROBABILITY_GRID_INSERTER_2D", --使用2D栅格概率图插入数据
-- 概率占用栅格地图的一些配置
probability_grid_range_data_inserter = {
insert_free_space = true,
hit_probability = 0.55,
miss_probability = 0.49,
},
-- tsdf地图的一些配置
tsdf_range_data_inserter = {
......
......
},
},
这些参数的具体作用,后续再做详细的讲解。ActiveSubmaps2D 主要有如下几个成员变量:
const proto::SubmapsOptions2D options_;
std::vector<std::shared_ptr<Submap2D>> submaps_;
std::unique_ptr<RangeDataInserterInterface> range_data_inserter_;
// 转换表, 第[0-32767]位置, 存的是[0.9, 0.1~0.9]的数据
ValueConversionTables conversion_tables_;
options_ 主要存储匹配信息,submaps_ 用于存储多个子图,range_data_inserter_ 与 ValueConversionTables 后续进行详细分析。下面来分析 ActiveSubmaps2D 的成员函数。
四、InsertRangeData()
对于 ActiveSubmaps2D::InsertRangeData() 函数从命名可以看出,其主要功能为插入雷达数据到子图中,流程如下:
( 01 ) \color{blue}(01) (01) 如果submaps_ 不存在任何一个子图,或者 submaps_ 中最后一个子图数据的数量达到了与 options_.num_range_data()=90(默认配置),则调用 ActiveSubmaps2D::AddSubmap() 函数新建一个子图。
( 02 ) \color{blue}(02) (02) 将一帧雷达数据 range_data 写入到所有子图之中 submaps_ 。不过注意,通常 submaps_ 最多只包含两个子图。具体原由稍后讲解原由。
( 03 ) \color{blue}(03) (03) 如果 submaps_ 中第一个子图中场插入的数据数量达到了两倍 options_.num_range_data(),则把该子图标记为完成。
( 04 ) \color{blue}(04) (04) 调用 ActiveSubmaps2D::submaps() 函数,使用共享指针返回 submaps_ 中的所有子图。
代码注释如下:
// 将点云数据写入到submap中
std::vector<std::shared_ptr<const Submap2D>> ActiveSubmaps2D::InsertRangeData(
const sensor::RangeData& range_data) {
// 如果第二个子图插入节点的数据等于num_range_data时,就新建个子图
// 因为这时第一个子图应该已经处于完成状态了
if (submaps_.empty() ||
submaps_.back()->num_range_data() == options_.num_range_data()) {
AddSubmap(range_data.origin.head<2>());
}
// 将一帧雷达数据同时写入两个子图中
for (auto& submap : submaps_) {
submap->InsertRangeData(range_data, range_data_inserter_.get());
}
// 第一个子图的节点数量等于2倍的num_range_data时,第二个子图节点数量应该等于num_range_data
if (submaps_.front()->num_range_data() == 2 * options_.num_range_data()) {
submaps_.front()->Finish();
}
return submaps();
}
五、AddSubmap()
上面提到,如果submaps_ 不存在任何一个子图,或者 submaps_ 中最后一个子图数据的数量达到了与 options_.num_range_data()=90(默认配置),则调用 ActiveSubmaps2D::AddSubmap() 函数新建一个子图。
新建子图调用的函数就是 ActiveSubmaps2D::AddSubmap(),其目的是构件一个 Submap2D 独占指针对象,然后添加到 submaps_ 之后,不过有几个点是需要注意的:
如果 submaps_ 中包含的子图数量,即 submaps_.size() 大于等于 2,那么会擦除掉 submaps_ 中的第一个地图。所以与前面的内容就呼应起来了,submaps_ 中最多存在两个子图。因为若 submaps_ 已经存在两个及两个以上的子图时,新建一个子图的同时会删除一个子图,所以依旧为两个子图。
代码注释如下:
// 新增一个子图,根据子图个数判断是否删掉第一个子图
void ActiveSubmaps2D::AddSubmap(const Eigen::Vector2f& origin) {
// 调用AddSubmap时第一个子图一定是完成状态,所以子图数为2时就可以删掉第一个子图了
if (submaps_.size() >= 2) {
// This will crop the finished Submap before inserting a new Submap to
// reduce peak memory usage a bit.
CHECK(submaps_.front()->insertion_finished());
// 删掉第一个子图的指针
submaps_.erase(submaps_.begin());
}
// 新建一个子图, 并保存指向新子图的智能指针
submaps_.push_back(absl::make_unique<Submap2D>(
origin,
std::unique_ptr<Grid2D>(
static_cast<Grid2D*>(CreateGrid(origin).release())),
&conversion_tables_));
}
六、CreateRangeDataInserter()
ActiveSubmaps2D 可以支持 概率栅格地图 与 tsdf地图,通过 ActiveSubmaps2D::CreateRangeDataInserter() 函数,根据配置信息可以构建 ProbabilityGridRangeDataInserter2D 与 TSDFRangeDataInserter2D 对象。本人使用的是 ProbabilityGridRangeDataInserter2D,所以后续以其为例进行讲解。他们都派生自 RangeDataInserterInterface(),主要实现如下纯虚函数,用于插入雷达数据(后续会做详细讲解)。
// Inserts 'range_data' into 'grid'.
virtual void Insert(const sensor::RangeData& range_data,GridInterface* grid) const = 0;
七、CreateGrid()
ActiveSubmaps2D::CreateGrid() 函数,主要是根据雷达传感器的原点构建 ProbabilityGrid 或者 TSDF2D 对象,主要作用进行地图保存,其都继承于 Grid2D。另外需要注意,在构建 ProbabilityGrid 或者 TSDF2D 时,会构建一个 MapLimits 对象当作实参传入到构造函数。
八、结语
在 ActiveSubmaps2D 的这些成员函数中,不难看出,对于地图的相关处理都集中在成员函数 InsertRangeData() 函数,其还调用了另一个重要函数AddSubmap(),为了方便理解,根据下图进行讲解一下:
其上的每个方形代表一个子图 Submap2D,根据上面的分析知道 submaps_ 最多同时存在两个子图,这里假设现在 submaps_ 中存储的子图为子图1与子图2。那么子图1中插入的数据定然比子图2多 options_.num_range_data(),因为只有子图一达到 options_.num_range_data() 数据集时,子图二才被创建,同时会删除 submaps_ 最前面的子图(这里假设为子图0,未在上图画出)。后续在插入的数据是,会同时插入到子图1与子图2,也就是说,这两个子图的数据是存在交集的。
当子图2数据达到了 options_.num_range_data(),也就是此时 子图1的数据为2倍的 options_.num_range_data(),会把子图1标记为完成状态,同时从 submaps_ 中删除该子图,这样子图2代替了之前子图1的位置,同时会再创建子图3添加到 submaps_ 之中。也就是说,此时 submaps_ 中包含了子图2与子图3,然后再继续往子图2,3插入数据,所以子图2与子图3也存在交集,依次循环下去。
最后了,会保证两个相邻的子图之间是存在共同数据的,其目的是为了点云匹配时,在两个子图间出现断层的现象。具体的细节后续再做更加详细的分析。文章来源:https://www.toymoban.com/news/detail-659230.html
文章来源地址https://www.toymoban.com/news/detail-659230.html
到了这里,关于(02)Cartographer源码无死角解析-(41) 2D栅格地图→ActiveSubmaps2D的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!