上一节介绍了cartographer_ros对cartographer调用的流程与接口,本节开始重点关注cartographer源码前端部分,主要包括local_trajectoroy_builder中进行点云与地图的匹配,以及子图更新。
我们从上节提到的MapBuilder类的实例开始,我们知道ros节点初始化时会调用AddTrajectory函数,源码位于cartographer/mapping/map_builder.cc中。
// ros封装调用该函数初始化路径构建器 int MapBuilder::AddTrajectoryBuilder( const std::set<SensorId>& expected_sensor_ids, const proto::TrajectoryBuilderOptions& trajectory_options, LocalSlamResultCallback local_slam_result_callback) { const int trajectory_id = trajectory_builders_.size(); // 选择实际初始化的局部路径构造器对象,3d/2d if (options_.use_trajectory_builder_3d()) { std::unique_ptr<LocalTrajectoryBuilder3D> local_trajectory_builder; if (trajectory_options.has_trajectory_builder_3d_options()) { local_trajectory_builder = absl::make_unique<LocalTrajectoryBuilder3D>( trajectory_options.trajectory_builder_3d_options(), SelectRangeSensorIds(expected_sensor_ids)); } DCHECK(dynamic_cast<PoseGraph3D*>(pose_graph_.get())); trajectory_builders_.push_back(absl::make_unique<CollatedTrajectoryBuilder>( trajectory_options, sensor_collator_.get(), trajectory_id, expected_sensor_ids, CreateGlobalTrajectoryBuilder3D( std::move(local_trajectory_builder), trajectory_id, static_cast<PoseGraph3D*>(pose_graph_.get()), local_slam_result_callback))); } else { std::unique_ptr<LocalTrajectoryBuilder2D> local_trajectory_builder; if (trajectory_options.has_trajectory_builder_2d_options()) { local_trajectory_builder = absl::make_unique<LocalTrajectoryBuilder2D>( trajectory_options.trajectory_builder_2d_options(), SelectRangeSensorIds(expected_sensor_ids)); } DCHECK(dynamic_cast<PoseGraph2D*>(pose_graph_.get())); // 创建全局路径构造器对象 trajectory_builders_.push_back(absl::make_unique<CollatedTrajectoryBuilder>( trajectory_options, sensor_collator_.get(), trajectory_id, expected_sensor_ids, CreateGlobalTrajectoryBuilder2D( std::move(local_trajectory_builder), trajectory_id, static_cast<PoseGraph2D*>(pose_graph_.get()), local_slam_result_callback))); } MaybeAddPureLocalizationTrimmer(trajectory_id, trajectory_options, pose_graph_.get()); if (trajectory_options.has_initial_trajectory_pose()) { const auto& initial_trajectory_pose = trajectory_options.initial_trajectory_pose(); pose_graph_->SetInitialTrajectoryPose( trajectory_id, initial_trajectory_pose.to_trajectory_id(), transform::ToRigid3(initial_trajectory_pose.relative_pose()), common::FromUniversal(initial_trajectory_pose.timestamp())); } proto::TrajectoryBuilderOptionsWithSensorIds options_with_sensor_ids_proto; for (const auto& sensor_id : expected_sensor_ids) { *options_with_sensor_ids_proto.add_sensor_id() = ToProto(sensor_id); } *options_with_sensor_ids_proto.mutable_trajectory_builder_options() = trajectory_options; all_trajectory_builder_options_.push_back(options_with_sensor_ids_proto); CHECK_EQ(trajectory_builders_.size(), all_trajectory_builder_options_.size()); return trajectory_id; }
该函数会根据配置参数创建2d或3d的local_trajectory_builder并以此为参数创建带校核(collated)的global_trajectory_builder,最终返回当前创建的路径编号。至此程序初始化基本完成,我们将关注点转移到上节末尾提到的SensorBridge::HandleRangefinder函数,每当监听到激光雷达数据,该函数会激活并最终调用trajectory_builder_->AddSensorData函数。
void SensorBridge::HandleRangefinder( const std::string& sensor_id, const carto::common::Time time, const std::string& frame_id, const carto::sensor::TimedPointCloud& ranges) { if (!ranges.empty()) { CHECK_LE(ranges.back().time, 0.f); } // 获取两帧tf之间的平移变换 const auto sensor_to_tracking = tf_bridge_.LookupToTracking(time, CheckNoLeadingSlash(frame_id)); // 调用cartographer中global_trajectory_builder的对应函数 if (sensor_to_tracking != nullptr) { trajectory_builder_->AddSensorData( sensor_id, carto::sensor::TimedPointCloudData{ time, sensor_to_tracking->translation().cast<float>(), carto::sensor::TransformTimedPointCloud( ranges, sensor_to_tracking->cast<float>())}); } }
由上文我们知道此处trajectory_builder实际上是CollatedTrajectoryBuilder类的实例,通过观察该类定义,我们大概知道这是一个对TrajectoryBuilder进行封装的校核器类,暂时忽略封装,直接将其当做对应的TrajectoryBuilder类的实例处理,那么此处trajectory_builder可看做GlobalTrajectoryBuilder的实例,通过该实例的AddSensorData函数,实现传感器数据从cartographer_ros节点到cartographer内部的传递。AddSensorData经过重构,可以接收并处理各传感器数据(点云、IMU、里程计等)。此处主要研究处理激光雷达点云的函数,具体实现如下(位于cartographer/mapping/internal/global_trajectory_builder.cc)。
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,将点云与地图匹配并返回更新的submaps 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(); // 将前端产生的Insertion_result(时间戳、位姿、点云、子图等)放入pose_graph_中,供后端自行优化 std::unique_ptr<InsertionResult> insertion_result; 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_); 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.cc中的AddRangeData函数的实现。
// 由全局路径构造器调用,传入激光雷达数据 std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult> LocalTrajectoryBuilder2D::AddRangeData( const std::string& sensor_id, const sensor::TimedPointCloudData& unsynchronized_data) { // LOG(INFO)<<"local trajectory add range data called."; 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. if (!options_.use_imu_data()) { InitializeExtrapolator(time); } if (extrapolator_ == nullptr) { // Until we've 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); 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(); } 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{{}, {}, {}}; } // 点云根据距离限制分类为"returns"和"messes" // Rays begin at 'origin'. 'returns' are the points where obstructions were // detected. 'misses' are points in the direction of rays for which no return // was detected, and were inserted at a configured distance. It is assumed that // between the 'origin' and 'misses' is free space. // 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) { const sensor::TimedRangefinderPoint& hit = synchronized_data.ranges[i].point_time; const Eigen::Vector3f origin_in_local = range_data_poses[i] * synchronized_data.origins.at(synchronized_data.ranges[i].origin_index); 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(); if (range >= options_.min_range()) { if (range <= options_.max_range()) { accumulated_range_data_.returns.push_back(hit_in_local); } else { hit_in_local.position = origin_in_local + options_.missing_data_ray_length() / range * delta; accumulated_range_data_.misses.push_back(hit_in_local); } } } ++num_accumulated_; if (num_accumulated_ >= options_.num_accumulated_range_data()) { 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, TransformToGravityAlignedFrameAndFilter( gravity_alignment.cast<float>() * range_data_poses.back().inverse(), accumulated_range_data_), gravity_alignment, sensor_duration); } return nullptr; }
该函数做了一些时间同步,计数与点云分类的操作,观察返回时调用的AddAccumulatedRangeData函数。
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) { 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); const transform::Rigid2d pose_prediction = transform::Project2D( non_gravity_aligned_pose_prediction * gravity_alignment.inverse()); // 体素滤波降低优化耗时 const sensor::PointCloud& filtered_gravity_aligned_point_cloud = sensor::AdaptiveVoxelFilter(options_.adaptive_voxel_filter_options()) .Filter(gravity_aligned_range_data.returns); if (filtered_gravity_aligned_point_cloud.empty()) { return nullptr; } // local map frame <- gravity-aligned frame // 调用ScanMatch,用ceres找到精定位信息 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); // submap更新制图 sensor::RangeData range_data_in_local = TransformRangeData(gravity_aligned_range_data, transform::Embed3D(pose_estimate_2d->cast<float>())); 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)); } } // 计算该线程时间花销 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)}); }
至此,一帧激光雷达点云数据传入后的调用流程基本介绍完毕。下一节将从点云与地图的匹配(ScanMatch函数)开始介绍。