GlobalTrajectoryBuilder类用于连接slam的前端与后端,它的构造函数主要如下:
/**
* @brief 完整的slam, 连接起了前端与后端
*
* @param[in] local_trajectory_builder 2d or 3d local slam 前端
* @param[in] trajectory_id 轨迹id
* @param[in] pose_graph 2d or 3d pose_graph 后端
* @param[in] local_slam_result_callback 前端的回调函数
* @param[in] pose_graph_odometry_motion_filter 里程计的滤波器
*/
GlobalTrajectoryBuilder(
std::unique_ptr<LocalTrajectoryBuilder> local_trajectory_builder,
const int trajectory_id, PoseGraph* const pose_graph,
const LocalSlamResultCallback& local_slam_result_callback,
const absl::optional<MotionFilter>& pose_graph_odometry_motion_filter)
: trajectory_id_(trajectory_id),
pose_graph_(pose_graph),
local_trajectory_builder_(std::move(local_trajectory_builder)),
local_slam_result_callback_(local_slam_result_callback),
pose_graph_odometry_motion_filter_(pose_graph_odometry_motion_filter) {}
~GlobalTrajectoryBuilder() override {}
然后这里面还定义了一些AddSensorData函数,这些函数与上一篇雷达数据处理中最后数据传递到的函数是同一个,也就是说参数最后是给到了这边。
可以看一下雷达的数据处理函数:
/**
* @brief 点云数据的处理, 先进行扫描匹配, 然后将扫描匹配的结果当做节点插入到后端的位姿图中
*
* @param[in] sensor_id topic名字
* @param[in] 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.";
// 进行扫描匹配, 返回匹配后的结果
std::unique_ptr<typename LocalTrajectoryBuilder::MatchingResult>
matching_result = local_trajectory_builder_->AddRangeData(
sensor_id, timed_point_cloud_data);
if (matching_result == nullptr) {
// The range data has not been fully accumulated yet.
return;
}
kLocalSlamMatchingResults->Increment();
std::unique_ptr<InsertionResult> insertion_result;
// matching_result->insertion_result 的类型是 LocalTrajectoryBuilder2D::InsertionResult
// 如果雷达成功插入到地图中
if (matching_result->insertion_result != nullptr) {
kLocalSlamInsertionResults->Increment();
// 将匹配后的结果 当做节点 加入到位姿图中
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_);
// 这里的InsertionResult的类型是 TrajectoryBuilderInterface::InsertionResult
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())});
}
// 将结果数据传入回调函数中, 进行保存
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));
}
}
可以看到函数首先是调用了local_trajectory_builder_->AddRangeData函数进行了依据局部位姿匹配并返回了匹配结果。注意matching_result是一个结构体,里面存储了时间、local_pose、匹配后的激光雷达校准数据等内容。如果matching_result为空说明匹配失败跳出函数。
local_trajectory_builder_->AddRangeData函数应该定义在local_trajector_builder_2d.cc中:
/**
* @brief 处理点云数据, 进行扫描匹配, 将点云写成地图
*
* @param[in] sensor_id 点云数据对应的话题名称
* @param[in] unsynchronized_data 传入的点云数据
* @return std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult> 匹配后的结果
*/
std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult>
LocalTrajectoryBuilder2D::AddRangeData(
const std::string& sensor_id,
const sensor::TimedPointCloudData& unsynchronized_data) {
// Step: 1 进行多个雷达点云数据的时间同步, 点云的坐标是相对于tracking_frame的
auto synchronized_data =
range_data_collator_.AddRangeData(sensor_id, unsynchronized_data);
if (synchronized_data.ranges.empty()) {
LOG(INFO) << "Range data collator filling buffer.";
return nullptr;
}
const common::Time& time = synchronized_data.time;
// Initialize extrapolator now if we do not ever use an IMU.
// 如果不用imu, 就在雷达这初始化位姿推测器
if (!options_.use_imu_data()) {
InitializeExtrapolator(time);
}
if (extrapolator_ == nullptr) {
// Until we have initialized the extrapolator with our first IMU message, we
// cannot compute the orientation of the rangefinder.
LOG(INFO) << "Extrapolator not yet initialized.";
return nullptr;
}
CHECK(!synchronized_data.ranges.empty());
// TODO(gaschler): Check if this can strictly be 0.
CHECK_LE(synchronized_data.ranges.back().point_time.time, 0.f);
// 计算第一个点的时间
const common::Time time_first_point =
time +
common::FromSeconds(synchronized_data.ranges.front().point_time.time);
// 只有在extrapolator_初始化时, GetLastPoseTime()是common::Time::min()
if (time_first_point < extrapolator_->GetLastPoseTime()) {
LOG(INFO) << "Extrapolator is still initializing.";
return nullptr;
}
std::vector<transform::Rigid3f> range_data_poses;
range_data_poses.reserve(synchronized_data.ranges.size());
bool warned = false;
// 预测得到每一个时间点的位姿
for (const auto& range : synchronized_data.ranges) {
common::Time time_point = time + common::FromSeconds(range.point_time.time);
// 如果该时间比上次预测位姿的时间还要早,说明这个点的时间戳往回走了, 就报错
if (time_point < extrapolator_->GetLastExtrapolatedTime()) {
// 一个循环只报一次错
if (!warned) {
LOG(ERROR)
<< "Timestamp of individual range data point jumps backwards from "
<< extrapolator_->GetLastExtrapolatedTime() << " to " << time_point;
warned = true;
}
time_point = extrapolator_->GetLastExtrapolatedTime();
}
// Step: 2 预测出 每个点的时间戳时刻, tracking frame 在 local slam 坐标系下的位姿
range_data_poses.push_back(
extrapolator_->ExtrapolatePose(time_point).cast<float>());
}
if (num_accumulated_ == 0) {
// 'accumulated_range_data_.origin' is uninitialized until the last
// accumulation.
accumulated_range_data_ = sensor::RangeData{{}, {}, {}};
}
// Drop any returns below the minimum range and convert returns beyond the
// maximum range into misses.
// 对每个数据点进行处理
for (size_t i = 0; i < synchronized_data.ranges.size(); ++i) {
// 获取在tracking frame 下点的坐标
const sensor::TimedRangefinderPoint& hit =
synchronized_data.ranges[i].point_time;
// 将点云的origins坐标转到 local slam 坐标系下
const Eigen::Vector3f origin_in_local =
range_data_poses[i] *
synchronized_data.origins.at(synchronized_data.ranges[i].origin_index);
// Step: 3 运动畸变的去除, 将相对于tracking_frame的hit坐标 转成 local坐标系下的坐标
sensor::RangefinderPoint hit_in_local =
range_data_poses[i] * sensor::ToRangefinderPoint(hit);
// 计算这个点的距离, 这里用的是去畸变之后的点的距离
const Eigen::Vector3f delta = hit_in_local.position - origin_in_local;
const float range = delta.norm();
// param: min_range max_range
if (range >= options_.min_range()) {
if (range <= options_.max_range()) {
// 在这里可以看到, returns里保存的是local slam下的去畸变之后的点的坐标
accumulated_range_data_.returns.push_back(hit_in_local);
} else {
// Step: 4 超过max_range时的处理: 用一个距离进行替代, 并放入misses里
hit_in_local.position =
origin_in_local +
// param: missing_data_ray_length, 是个比例, 不是距离
options_.missing_data_ray_length() / range * delta;
accumulated_range_data_.misses.push_back(hit_in_local);
}
}
} // end for
// 有一帧有效的数据了
++num_accumulated_;
// param: num_accumulated_range_data 几帧有效的点云数据进行一次扫描匹配
if (num_accumulated_ >= options_.num_accumulated_range_data()) {
// 计算2次有效点云数据的的时间差
const common::Time current_sensor_time = synchronized_data.time;
absl::optional<common::Duration> sensor_duration;
if (last_sensor_time_.has_value()) {
sensor_duration = current_sensor_time - last_sensor_time_.value();
}
last_sensor_time_ = current_sensor_time;
// 重置变量
num_accumulated_ = 0;
// 获取机器人当前姿态
const transform::Rigid3d gravity_alignment = transform::Rigid3d::Rotation(
extrapolator_->EstimateGravityOrientation(time));
// TODO(gaschler): This assumes that 'range_data_poses.back()' is at time
// 'time'.
// 以最后一个点的时间戳估计出的坐标为这帧数据的原点
accumulated_range_data_.origin = range_data_poses.back().translation();
return AddAccumulatedRangeData(
time,
// 将点云变换到local原点处, 且姿态为0
TransformToGravityAlignedFrameAndFilter(
gravity_alignment.cast<float>() * range_data_poses.back().inverse(),
accumulated_range_data_),
gravity_alignment, sensor_duration);
}
return nullptr;
}
可以看出这个地方主要是处理点云数据, 进行扫描匹配, 将点云写成地图。它的步骤分为以下几个:
auto synchronized_data =
range_data_collator_.AddRangeData(sensor_id, unsynchronized_data);
if (synchronized_data.ranges.empty()) {
LOG(INFO) << "Range data collator filling buffer.";
return nullptr;
}
输入是sensor_id与未经过时间同步的点云数据,输出时间同步后的点云数据:
/**
* @brief 多个雷达数据的时间同步
*
* @param[in] sensor_id 雷达数据的话题
* @param[in] timed_point_cloud_data 雷达数据
* @return sensor::TimedPointCloudOriginData 根据时间处理之后的数据
*/
sensor::TimedPointCloudOriginData RangeDataCollator::AddRangeData(
const std::string& sensor_id,
sensor::TimedPointCloudData timed_point_cloud_data) { // 第一次拷贝
CHECK_NE(expected_sensor_ids_.count(sensor_id), 0);
// 从sensor_bridge传过来的数据的intensities为空
timed_point_cloud_data.intensities.resize(
timed_point_cloud_data.ranges.size(), kDefaultIntensityValue);
// TODO(gaschler): These two cases can probably be one.
// 如果同话题的点云, 还有没处理的, 就先处同步没处理的点云, 将当前点云保存
if (id_to_pending_data_.count(sensor_id) != 0) {
// current_end_为上一次时间同步的结束时间
// current_start_为本次时间同步的开始时间
current_start_ = current_end_;
// When we have two messages of the same sensor, move forward the older of
// the two (do not send out current).
// 本次时间同步的结束时间为这帧点云数据的结束时间
current_end_ = id_to_pending_data_.at(sensor_id).time;
auto result = CropAndMerge();
// 保存当前点云
id_to_pending_data_.emplace(sensor_id, std::move(timed_point_cloud_data));
return result;
}
// 先将当前点云添加到 等待时间同步的map中
id_to_pending_data_.emplace(sensor_id, std::move(timed_point_cloud_data));
// 等到range数据的话题都到来之后再进行处理
if (expected_sensor_ids_.size() != id_to_pending_data_.size()) {
return {};
}
current_start_ = current_end_;
// We have messages from all sensors, move forward to oldest.
common::Time oldest_timestamp = common::Time::max();
// 找到所有传感器数据中最早的时间戳(点云最后一个点的时间)
for (const auto& pair : id_to_pending_data_) {
oldest_timestamp = std::min(oldest_timestamp, pair.second.time);
}
// current_end_是本次时间同步的结束时间
// 是待时间同步map中的 所有点云中最早的时间戳
current_end_ = oldest_timestamp;
return CropAndMerge();
}
这里的时间同步不是为最初理解的将所有点云的时间戳设置成一样,而是截取一段时间内的所有点云。这个处理一般是用于多个激光雷达或者激光雷达数据时间戳有问题的情况下,对于正常情况下单激光传感器这个函数基本上是什么都没干。
在时间同步之后,根据算法是否使用IMU决定是否使用位姿估计器进行位姿初始化,如果没有IMU会调用一个位姿估计器进行位姿估计,如果有IMU则使用IMU进行位姿估计:
if (!options_.use_imu_data()) {
InitializeExtrapolator(time);
}
将相对于tracking_frame的hit坐标 转成 local坐标系下的坐标
// Step: 3 运动畸变的去除, 将相对于tracking_frame的hit坐标 转成 local坐标系下的坐标
sensor::RangefinderPoint hit_in_local =
range_data_poses[i] * sensor::ToRangefinderPoint(hit);
这里的去除运动畸变也是根据时间戳预测出来的。
接下来根据去畸变之后的数据判断点的距离,去除一些超出范围的点。对于小于最小值的点直接抛弃,大于最大值的点使用了另外一个距离值去代替。
在这步结束的时候就已经将所有点云从tracking_frame坐标系转变到了local_slam坐标系下了。
这里算法进行了一个判断:
// param: num_accumulated_range_data 几帧有效的点云数据进行一次扫描匹配
if (num_accumulated_ >= options_.num_accumulated_range_data()) {
这里的判断用于决定当前的点云是否进行扫描匹配,默认值是1,也就是每一次都进行匹配。
然后满足条件的话会进入下面语句中:
// 获取机器人当前姿态
const transform::Rigid3d gravity_alignment = transform::Rigid3d::Rotation(
extrapolator_->EstimateGravityOrientation(time));
这里首先进行了一个位姿的获取,获取的应该是全局坐标。
在获取位姿之后,将机器人的点云信息转换到局部坐标系下:
return AddAccumulatedRangeData(
time,
// 将点云变换到local原点处, 且姿态为0
TransformToGravityAlignedFrameAndFilter(
gravity_alignment.cast<float>() * range_data_poses.back().inverse(),
accumulated_range_data_),
gravity_alignment, sensor_duration);
}
这里面调用了一个TransformToGravityAlignedFrameAndFilter函数,它的主要函数有两个作用:
第一个是将原点位于机器人当前位姿处的点云 转成 原点位于local坐标系原点处的点云, 再进行z轴上的过滤。
第二个是对点云进行体素滤波。体素滤波是指在一个格子内只取一个点,默认格子大小为0.025。点的取法好像是随机取的,如果有多个点的话。
获取滤波之后的点云传入到AddAccumulatedRangeData下:
/**
* @brief 进行扫描匹配, 将点云写入地图
*
* @param[in] time 点云的时间戳
* @param[in] gravity_aligned_range_data 原点位于local坐标系原点处的点云
* @param[in] gravity_alignment 机器人当前姿态
* @param[in] sensor_duration 2帧点云数据的时间差
* @return std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult>
*/
std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult>
LocalTrajectoryBuilder2D::AddAccumulatedRangeData(
const common::Time time,
const sensor::RangeData& gravity_aligned_range_data,
const transform::Rigid3d& gravity_alignment,
const absl::optional<common::Duration>& sensor_duration) {
// 如果处理完点云之后数据为空, 就报错. 使用单线雷达时不要设置min_z
if (gravity_aligned_range_data.returns.empty()) {
LOG(WARNING) << "Dropped empty horizontal range data.";
return nullptr;
}
// Computes a gravity aligned pose prediction.
// 进行位姿的预测, 先验位姿
const transform::Rigid3d non_gravity_aligned_pose_prediction =
extrapolator_->ExtrapolatePose(time);
// 将三维位姿先旋转到姿态为0, 再取xy坐标将三维位姿转成二维位姿
const transform::Rigid2d pose_prediction = transform::Project2D(
non_gravity_aligned_pose_prediction * gravity_alignment.inverse());
// Step: 7 对 returns点云 进行自适应体素滤波,返回的点云的数据类型是PointCloud
const sensor::PointCloud& filtered_gravity_aligned_point_cloud =
sensor::AdaptiveVoxelFilter(gravity_aligned_range_data.returns,
options_.adaptive_voxel_filter_options());
if (filtered_gravity_aligned_point_cloud.empty()) {
return nullptr;
}
// local map frame <- gravity-aligned frame
// 扫描匹配, 进行点云与submap的匹配
std::unique_ptr<transform::Rigid2d> pose_estimate_2d =
ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);
if (pose_estimate_2d == nullptr) {
LOG(WARNING) << "Scan matching failed.";
return nullptr;
}
// 将二维坐标旋转回之前的姿态
const transform::Rigid3d pose_estimate =
transform::Embed3D(*pose_estimate_2d) * gravity_alignment;
// 校准位姿估计器
extrapolator_->AddPose(time, pose_estimate);
// Step: 8 将 原点位于local坐标系原点处的点云 变换成 原点位于匹配后的位姿处的点云
sensor::RangeData range_data_in_local =
TransformRangeData(gravity_aligned_range_data,
transform::Embed3D(pose_estimate_2d->cast<float>()));
// 将校正后的雷达数据写入submap
std::unique_ptr<InsertionResult> insertion_result = InsertIntoSubmap(
time, range_data_in_local, filtered_gravity_aligned_point_cloud,
pose_estimate, gravity_alignment.rotation());
// 计算耗时
const auto wall_time = std::chrono::steady_clock::now();
if (last_wall_time_.has_value()) {
const auto wall_time_duration = wall_time - last_wall_time_.value();
kLocalSlamLatencyMetric->Set(common::ToSeconds(wall_time_duration));
if (sensor_duration.has_value()) {
kLocalSlamRealTimeRatio->Set(common::ToSeconds(sensor_duration.value()) /
common::ToSeconds(wall_time_duration));
}
}
// 计算cpu耗时
const double thread_cpu_time_seconds = common::GetThreadCpuTimeSeconds();
if (last_thread_cpu_time_seconds_.has_value()) {
const double thread_cpu_duration_seconds =
thread_cpu_time_seconds - last_thread_cpu_time_seconds_.value();
if (sensor_duration.has_value()) {
kLocalSlamCpuRealTimeRatio->Set(
common::ToSeconds(sensor_duration.value()) /
thread_cpu_duration_seconds);
}
}
last_wall_time_ = wall_time;
last_thread_cpu_time_seconds_ = thread_cpu_time_seconds;
// 返回结果
return absl::make_unique<MatchingResult>(
MatchingResult{time, pose_estimate, std::move(range_data_in_local),
std::move(insertion_result)});
}
可以看到该函数传入了机器人的姿态以及点云数据还有时间参数。
其中还进行了一次自适应体素滤波,距远离原点超过max_range的点被移除。体素滤波需要保证点的数量在一定范围内,如果当然如果点云足够稀疏的话会直接返回不进行滤波;如果滤波前值超过最小值但是滤波之后点云数量少于阈值,则减少滤波器的边长直到点云数量满足要求。合适的边长通过二分法查找。
在扫描匹配之后,数据传入了一个ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud)。这里就是算法最后进行扫描匹配的地方。根据这个函数得到一个估计出来的位姿,再根据这个位姿对点云进行坐标变换得到关于估计位姿下的点云。
最后结果返回到local_slam_result_callback中。在map_builder_bridge中GetLocalTrajectoryDtat中使用。这个值通过node.cc中的PublishLocalTrajectoryData函数发布。发布的话题为scan_matched_point_cloud这个话题
然后定义了一个数据结构:
std::unique_ptr<InsertionResult> insertion_result;
这个数据结构定义在TrajectoryBuilderInterface中,它包含了一个节点id,一个指向数据的指针,还有一个指向两个子图的指针。如果雷达数据成功插入到子图中,则将匹配后的结果当做节点加入到位姿图中
最后将结果数据传入回调函数中, 进行保存。这个保存下来的结果最后似乎是给到了MapBuilderBridge::GetLocalTrajectoryData()中使用。而GetLocalTrajectoryData在node.cc中被调用,进行位姿以及点云的发布。
当然这里的AddSensorData不知是用于处理雷达也用于处理别的函数,例如IMU:
// imu数据的处理, 数据走向有两个,一个是进入前端local_trajectory_builder_,一个是进入后端pose_graph_
void AddSensorData(const std::string& sensor_id,
const sensor::ImuData& imu_data) override {
if (local_trajectory_builder_) {
local_trajectory_builder_->AddImuData(imu_data);
}
pose_graph_->AddImuData(trajectory_id_, imu_data);
}
以上面这个函数为例,首先判断local_trajectory_builder_是否有值,如果有值就将其传入AddImuData处理。注意这里有两个AddImuData。local_trajectory_builder_对应的前端位姿估计,pose_graph_对应后端位姿估计
在里程计的处理函数中与IMU也是一样的:
// 里程计数据的处理, 数据走向有两个,一个是进入前端local_trajectory_builder_, 一个是进入后端pose_graph_
// 加入到后端之前, 先做一个距离的计算, 只有时间,移动距离,角度 变换量大于阈值才加入到后端中
void AddSensorData(const std::string& sensor_id,
const sensor::OdometryData& odometry_data) override {
CHECK(odometry_data.pose.IsValid()) << odometry_data.pose;
if (local_trajectory_builder_) {
local_trajectory_builder_->AddOdometryData(odometry_data);
}
// TODO(MichaelGrupp): Instead of having an optional filter on this level,
// odometry could be marginalized between nodes in the pose graph.
// Related issue: cartographer-project/cartographer/#1768
if (pose_graph_odometry_motion_filter_.has_value() &&
pose_graph_odometry_motion_filter_.value().IsSimilar(
odometry_data.time, odometry_data.pose)) {
return;
}
pose_graph_->AddOdometryData(trajectory_id_, odometry_data);
}
中间有个:
if (pose_graph_odometry_motion_filter_.has_value() &&
pose_graph_odometry_motion_filter_.value().IsSimilar(
odometry_data.time, odometry_data.pose)) {
return;
}
判断语句,由于pose_graph_odometry_motion_filter_没有初始化,无参数,所以这个if不执行。
GPS数据的处理与前两个不同,它只有一个处理流程:
// gps数据只在后端中使用
void AddSensorData(
const std::string& sensor_id,
const sensor::FixedFramePoseData& fixed_frame_pose) override {
if (fixed_frame_pose.pose.has_value()) {
CHECK(fixed_frame_pose.pose.value().IsValid())
<< fixed_frame_pose.pose.value();
}
pose_graph_->AddFixedFramePoseData(trajectory_id_, fixed_frame_pose);
}
可以看到它只在后端使用不在前端使用。
然后landmark也是只能在后端使用:
// Landmark的数据只在后端中使用
void AddSensorData(const std::string& sensor_id,
const sensor::LandmarkData& landmark_data) override {
pose_graph_->AddLandmarkData(trajectory_id_, landmark_data);
}
除了数据处理函数,后面还有一个AddLocalSlamResultData函数,该函数是将local slam的结果加入到后端中, 作为位姿图的一个节点
// 将local slam的结果加入到后端中, 作为位姿图的一个节点
void AddLocalSlamResultData(std::unique_ptr<mapping::LocalSlamResultData>
local_slam_result_data) override {
CHECK(!local_trajectory_builder_) << "Can't add LocalSlamResultData with "
"local_trajectory_builder_ present.";
local_slam_result_data->AddToPoseGraph(trajectory_id_, pose_graph_);
}