TrajectoryBuilder是用来创建一个trajectory的。
该类首先要保存trajectory上的传感器数据,从一个连续的多帧传感器数据中会抽取若干关键帧。一帧关键帧数据被称为trajectory上的一个节点,一条trajectory由一串节点组成,所以TrajectoryBuilder要维护一个节点列表
每一帧时的传感器数据相对于该Submap的局部坐标变换要已知
该类要创建并维护一个栅格化的Submap列表,以便在MapBuilder中对所有的submap进行优化,形成一个总的Map。每个Submap相对于世界坐标系的位姿要估计出来,这样PoseGraph才能依据不同的submap各自的位姿变换矩阵,把他们tie在一起并做全局优化。
GlobalTrajectoryBuilder
分为前端和后端两部分:local_trajectory_builder和pose_graph。GlobalTrajectoryBuilder
实际上是一个模板类,模板列表中的LocalTrajectoryBuilder2D
和PoseGraph2D
分别是前端和后端的两个核心类型。
成员变量很少:
完成指定ID的trajectory中前端和后端的联系,需要获取trajectory的ID。
类体内的成员函数AddLocalSlamResultData
是把前端结果直接给后端,但是不能是当前的前端。这个仅用于后端约束优化。这个函数没有被调用,LocalSlamResult2D::AddToTrajectoryBuilder
中被调用的同名函数应当是CollatedTrajectoryBuilder
的
时间点云的 AddSensorData
类有多个AddSensorData
函数,其中最重要的是时间点云对应的,也是cartographer最核心的函数 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51// 参数sensor_id记录了产生数据的传感器, timed_point_cloud_data则是传感器产生的数据
void AddSensorData(
const std::string& sensor_id,
const sensor::TimedPointCloudData& timed_point_cloud_data) override
{
// 检查一下前端核心对象是否存在
CHECK(local_trajectory_builder_)
<< "Cannot add TimedPointCloudData without a LocalTrajectoryBuilder.";
// Local SLAM的业务主线, 在该结果中同时记录了子图的更新信息
std::unique_ptr<typename LocalTrajectoryBuilder::MatchingResult>
matching_result = local_trajectory_builder_->AddRangeData(
sensor_id, timed_point_cloud_data);
// 结果中包含了轨迹中节点的pose、点云和submap的相关信息
if (matching_result == nullptr)
// The range data 还没有fully accumulated yet.
return;
/* 将前端的输出结果喂给后端进行闭环检测和全局优化 */
kLocalSlamMatchingResults->Increment();
// 前端匹配的结果,即submap位置和submap
std::unique_ptr<InsertionResult> insertion_result;
// 判定前端成功的是否将传感器的数据插入到子图中
if (matching_result->insertion_result != nullptr)
{
kLocalSlamInsertionResults->Increment();
// 将匹配后的结果加入图优化节点中
// 把前端的输出结果喂给后端,即后端增加一个新的节点
// 后端优化位姿图节点ID,记录在临时对象 node_id
auto node_id = pose_graph_->AddNode(
matching_result->insertion_result->constant_data, trajectory_id_,
matching_result->insertion_result->insertion_submaps );
CHECK_EQ(node_id.trajectory_id, trajectory_id_);
// 重新封装前端匹配的后的submap结果,并增加一个node_id
insertion_result = absl::make_unique<InsertionResult>( InsertionResult{
node_id, matching_result->insertion_result->constant_data,
std::vector<std::shared_ptr<const Submap>>(
matching_result->insertion_result->insertion_submaps.begin(),
matching_result->insertion_result->insertion_submaps.end()) } );
}
// 如果提供了回调函数,就调用之,并将前端的输出和刚刚构建的
//insertion_result对象传参,也就是采用回调函数将前端匹配的结果信息传递出去
if (local_slam_result_callback_)
{
local_slam_result_callback_(
trajectory_id_, matching_result->time, matching_result->local_pose,
std::move(matching_result->range_data_in_local),
std::move(insertion_result) );
}
}
基类TrajectoryBuilderInterface
中有个成员变量InsertionResult
,它描述前端的一次子图更新操作,将传感器的扫描数据插入子图中。1
2
3
4
5
6
7
8
9struct InsertionResult
{
// 有两个字段,分别记录了轨迹的索引(trajectory_id)和一个从零开始计数的节点编号(node_index)
NodeId node_id;
// 子图更新时在局部地图中的位姿,以及有传感器原始数据转换之后的点云信息
std::shared_ptr<const TrajectoryNode::Data> constant_data;
// 被更新的子图对象
std::vector<std::shared_ptr<const Submap2D>> insertion_submaps;
};
根据NodeId,可以把一条轨迹理解为由若干个节点串联起来的一个数据结构。
类LocalTrajectoryBuilder2D
中也有一个InsertionResult
,但是注意前端类不是继承自 TrajectoryBuilderInterface, 这两个没有直接关系,前端只关心更新子图时的位姿和点云信息,不考虑在它与整个运动轨迹之间的关系,所以缺少了字段node_id
1
2
3
4
5
6struct InsertionResult
{
// 插入的节点数据,TrajectoryNode::Data 包含了处理之后的点云数据
std::shared_ptr<const TrajectoryNode::Data> constant_data;
std::vector<std::shared_ptr<const Submap2D>> insertion_submaps;
};
里程计和IMU的 AddSensorData
1 | void AddSensorData(const std::string& sensor_id, |
先判定存在前端对象,将数据喂给前端对象进入PoseExtrapolator
,然后通过pose_graph
将传感器的信息添加到全局地图中。
Cartographer将类似GPS这种具有全局定位能力的传感器输出的位姿称为 固定坐标系位姿(fixed frame pose)。由于它们的测量结果是全局的信息,所以没有喂给前端用于局部定位。此外路标数据也可以认为是全局的定位信息,也直接喂给了后端