(02)Cartographer源码无死角解析-(77) ROS数据发布→2D点云数据、tf、机器人tracking frame轨迹发布

讲解关于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官方认证
 

一、前言

上一篇博客中,讲解了3D点云地图、子图位姿、Landmark 数据消息的发布名,但是并没有对
Node::PublishLocalTrajectoryData()、Node::PublishTrajectoryNodeList()、Node::PublishConstraintList() 进行分析,不过需要注意一点。需要注意到node.cc 中构造函数的如下代码:

  // lx add
  if (node_options_.map_builder_options.use_trajectory_builder_3d()) {
    wall_timers_.push_back(node_handle_.createWallTimer(
        ::ros::WallDuration(kPointCloudMapPublishPeriodSec),  // 10s
        &Node::PublishPointCloudMap, this));
  }

如果使用的是3D轨迹,才会调用 Node::PublishPointCloudMap 发布 3D点云数据的画图。对于2D点云数据的发布是通过以一定频率调用 Node::PublishLocalTrajectoryData() 这个函数。在讲解之前呢,回顾一个知识点,那就是添加一条轨迹的时,调用到 MapBuilderBridge::AddTrajectory() 函数时,存在如下代码:

  // Step: 1 开始一条新的轨迹, 返回新轨迹的id,需要传入一个函数
  const int trajectory_id = map_builder_->AddTrajectoryBuilder(
      expected_sensor_ids, trajectory_options.trajectory_builder_options,
      // lambda表达式 local_slam_result_callback_
      [this](const int trajectory_id, 
             const ::cartographer::common::Time time,
             const Rigid3d local_pose,
             ::cartographer::sensor::RangeData range_data_in_local,
             const std::unique_ptr<
                 const ::cartographer::mapping::TrajectoryBuilderInterface::
                     InsertionResult>) {
        // 保存local slam 的结果数据 5个参数实际只用了4个
        OnLocalSlamResult(trajectory_id, time, local_pose, range_data_in_local);
      });

其往前端优化中注册了一个回调函数 OnLocalSlamResult(),每次前端扫描匹配进行节点位姿优化之后,都会把相关的结果与数据通过 OnLocalSlamResult() 在 MapBuilderBridge::local_slam_data_ 这个变量中,这里保存着每条轨迹轨迹最新扫描匹配的结果。

系统分发数据的时候,会调用到 src/cartographer/cartographer/sensor/internal/dispatchable.h 中的Dispatchable::AddToTrajectoryBuilder() 而执行到 GlobalTrajectoryBuilder::AddSensorData() 函数。该函数中会把点云数据添加到前端获得扫描匹配的结果,然后会执行如下代码:

      // 将匹配后的结果 当做节点 加入到位姿图中
      auto node_id = pose_graph_->AddNode(
          matching_result->insertion_result->constant_data, trajectory_id_,
          matching_result->insertion_result->insertion_submaps);

这样就把节点数据添加到后端,也就是存储到 PoseGraph2D::data_.trajectory_nodes 中,且后续后端优化中会对其global 位姿进行调整。有了这些基础之后正式来分析 Node::PublishLocalTrajectoryData() 函数。

二、数据获取

Node::PublishLocalTrajectoryData() 函数第一步执行的代码如下所示:

/**
 * @brief 每5e-3s发布一次tf与tracked_pose
 *
 * @param[in] timer_event
 */
void Node::PublishLocalTrajectoryData(const ::ros::TimerEvent& timer_event) {
  absl::MutexLock lock(&mutex_);
  for (const auto& entry : map_builder_bridge_.GetLocalTrajectoryData()) {
    // entry的数据类型为std::unordered_map<int,MapBuilderBridge::LocalTrajectoryData>
    // entry.first 就是轨迹的id, entry.second 就是 LocalTrajectoryData

    // 获取对应轨迹id的trajectory_data
    const auto& trajectory_data = entry.second;

    // 获取对应轨迹id的extrapolator
    auto& extrapolator = extrapolators_.at(entry.first);

其主要就是通过调用 MapBuilderBridge::GetLocalTrajectoryData() 函数获得所有轨迹最后一个前端扫描匹配结果数据,然后分别进行遍历,先获得遍历节点轨迹相关的数据 trajectory_data,以及其对应的位姿推断器 extrapolator。需要注意,该处的位姿推断器是在添加轨迹 Node::AddTrajectory() 函数中调用 AddExtrapolator() 创建的。其与前端的位姿推断其并非同一个。前端 local 中的推断器是根据里程计odomery,IMU等数据集进行位姿推断,结合点云匹配的结果对 extrapolator 进行优化,该处的 extrapolator 只利用到了点云匹配结果位姿,相对来说,应该会更加准一些,如果频率足够高。

三、点云数据发布

只有点云数据发生改变的时候才会进行发布,所以发布点云数据的频率不会非常高,且避免了重复计算。主题代码如下:

    // We only publish a point cloud if it has changed. It is not needed at high
    // frequency, and republishing it would be computationally wasteful.
    // 如果当前状态的时间与extrapolator的lastposetime不等
    if (trajectory_data.local_slam_data->time !=
        extrapolator.GetLastPoseTime()) {
      // 有订阅才发布scan_matched_point_cloud
      if (scan_matched_point_cloud_publisher_.getNumSubscribers() > 0) {
        // TODO(gaschler): Consider using other message without time
        // information.
        carto::sensor::TimedPointCloud point_cloud;
        point_cloud.reserve(trajectory_data.local_slam_data->range_data_in_local
                                .returns.size());

        // 获取local_slam_data的点云数据, 填入到point_cloud中
        for (const cartographer::sensor::RangefinderPoint point :
             trajectory_data.local_slam_data->range_data_in_local.returns) {
          // 这里的虽然使用的是带时间戳的点云结构, 但是数据点的时间全是0.f
          point_cloud.push_back(cartographer::sensor::ToTimedRangefinderPoint(
              point, 0.f /* time */));
        }

        // 先将点云转换成ROS的格式,再发布scan_matched_point_cloud点云
        scan_matched_point_cloud_publisher_.publish(ToPointCloud2Message(
            carto::common::ToUniversal(trajectory_data.local_slam_data->time),
            node_options_.map_frame,
            // 将雷达坐标系下的点云转换成地图坐标系下的点云
            carto::sensor::TransformTimedPointCloud(
                point_cloud, trajectory_data.local_to_map.cast<float>())));
      }  // end 发布scan_matched_point_cloud

      // 将当前的pose加入到extrapolator中, 更新extrapolator的时间与状态
      extrapolator.AddPose(trajectory_data.local_slam_data->time,
                           trajectory_data.local_slam_data->local_pose);
    } // end if

( 1 ) : \color{blue}(1): (1): 首先判断一下上一次轨迹数据对应的时间 trajectory_data.local_slam_data->time 其位姿是否等于最后一次获取位姿的时间,如果不是会进一步判断是否存在订阅者。

( 2 ) : \color{blue}(2): (2): 创建一个一个时间 carto::sensor::TimedPointCloud 类型实例 point_cloud,且预分配好空间,然后把前端保存的点云数据 trajectory_data.local_slam_data->range_data_in_local.returns 进行转换添加到 point_cloud。

( 3 ) : \color{blue}(3): (3): 将雷达坐标系下的点云转换成地图坐标系下的点云然后进行发布,获取到的点云数据是相对loval系的位姿,通过左乘 global 系到 local 的坐标变换 trajectory_data.local_to_map 把点云数据变换到 global 系进行发布

( 4 ) : \color{blue}(4): (4): 把将当前扫描匹配local系下的pose加入到extrapolator中, 更新extrapolator的时间与状态。主要是利用 trajectory_data.local_slam_data->local_pose 数据推断线速度与角速度。到这里为止,2D点云数据的发布就已经完成了,接着就是发布机器人的位姿。

四、机器人位姿发布

( 1 ) : \color{blue}(1): (1): 调用,Node::PublishLocalTrajectoryData() 函数即使没有发布点云数据,也会发布 tracked_pose(机器人位姿),该位姿的发布主要通过位姿推断器 extrapolator 推断机器人 pose。器首先通过如下代码:

    // 使用较新的时间戳
    const ::cartographer::common::Time now = std::max(
        FromRos(ros::Time::now()), extrapolator.GetLastExtrapolatedTime());
    stamped_transform.header.stamp =
        node_options_.use_pose_extrapolator
            ? ToRos(now)
            : ToRos(trajectory_data.local_slam_data->time);
  
    // 保存当前的时间戳, 以防止对同一时间戳进行重复更新
    last_published_tf_stamps_[entry.first] = stamped_transform.header.stamp;          

获得最 extrapolator 插值时间与目前 ros::Time::now() 比较之后的最新时间。last_published_tf_stamps_ 用于记录上一位姿发布的时间,也就是说判断一下该时刻的位姿是否发布过了,如果已经发布过则 continue 遍历下一条轨迹。总的来说,也是避免重复计算。

( 2 ) : \color{blue}(2): (2): 获取当前机器人(tracking)在local系的坐标坐标。需要注意,如果设置了在 .lua 文件中配置了 published_frame = “base_link” 以及 publish_frame_projected_to_2d = true,那么发布机器人的轨迹就是以 base_link 节点为基准,而非 tracking_frame。另外,还会利用 local系到global系的坐标变换 trajectory_data.local_to_map,把机器人位姿变换到 global 系。代码注释如下:

    const Rigid3d tracking_to_local_3d =
        node_options_.use_pose_extrapolator
            ? extrapolator.ExtrapolatePose(now)
            : trajectory_data.local_slam_data->local_pose;
    
    // 获取当前位姿在local坐标系下的坐标
    const Rigid3d tracking_to_local = [&] {
      // 是否将变换投影到平面上
      if (trajectory_data.trajectory_options.publish_frame_projected_to_2d) {
        return carto::transform::Embed3D(
            carto::transform::Project2D(tracking_to_local_3d));
      }
      return tracking_to_local_3d;
    }();

    // 求得当前位姿在map下的坐标
    const Rigid3d tracking_to_map =
        trajectory_data.local_to_map * tracking_to_local;

五、tf变换发布

先来看一下整体注释,在进行细节分析:

    // 根据lua配置文件发布tf
    if (trajectory_data.published_to_tracking != nullptr) {
      if (node_options_.publish_to_tf) {
        // 如果需要cartographer提供odom坐标系
        // 则发布 map_frame -> odom -> published_frame 的tf
        if (trajectory_data.trajectory_options.provide_odom_frame) {
          std::vector<geometry_msgs::TransformStamped> stamped_transforms;
          
          // map_frame -> odom
          stamped_transform.header.frame_id = node_options_.map_frame;
          stamped_transform.child_frame_id =
              trajectory_data.trajectory_options.odom_frame;
          // 将local坐标系作为odom坐标系
          stamped_transform.transform =
              ToGeometryMsgTransform(trajectory_data.local_to_map);
          stamped_transforms.push_back(stamped_transform);

          // odom -> published_frame
          stamped_transform.header.frame_id =
              trajectory_data.trajectory_options.odom_frame;
          stamped_transform.child_frame_id =
              trajectory_data.trajectory_options.published_frame;
          // published_to_tracking 是局部坐标系下的位姿
          stamped_transform.transform = ToGeometryMsgTransform(
              tracking_to_local * (*trajectory_data.published_to_tracking));
          stamped_transforms.push_back(stamped_transform);
          
          // 发布 map_frame -> odom -> published_frame 的tf
          tf_broadcaster_.sendTransform(stamped_transforms);
        } 
        // cartographer不需要提供odom坐标系,则发布 map_frame -> published_frame 的tf
        else {
          stamped_transform.header.frame_id = node_options_.map_frame;
          stamped_transform.child_frame_id =
              trajectory_data.trajectory_options.published_frame;
          stamped_transform.transform = ToGeometryMsgTransform(
              tracking_to_map * (*trajectory_data.published_to_tracking));
          // 发布 map_frame -> published_frame 的tf
          tf_broadcaster_.sendTransform(stamped_transform);
        }
      }
      

( 1 ) : \color{blue}(1): (1): 如果 trajectory_data 有保存 published frame 到 tracking frame 之间的变换关系,则再一步判断是否有配置 publish_to_tf 参数,如果配置,则表明发布 tf 数据。

( 2 ) : \color{blue}(2): (2): 若 trajectory_data.trajectory_options.provide_odom_frame 设置为 true,则表示有提供 odom_frame。则创建一个存储 geometry_msgs::TransformStamped 对象实例的容器 stamped_transforms。

( 3 ) : \color{blue}(3): (3): 构建第一个 TransformStamped 对象,其表示 local 到 map(global) 系的坐标变换,注意这里的 stamped_transform.child_frame_id = trajectory_data.trajectory_options.odom_frame,也就是说认为 odom 的坐标系为 local 系。接着构建 odom(local) 到 published_frame 系到published_frame 的坐标变换。最后在进行发布。

总的来说,就是构建 map_frame(global) 到 odom(local) 再到 published_frame(机器人) 的坐标变换,理想情况下是 map_frame(global) 到 odom(local) 为单位变换,即两坐标系重合。

( 4 ) : \color{blue}(4): (4): 如果没有配置 provide_odom_frame 参数,则只发布 map_frame 到 published_frame 的tf。

六、发布基于 tracking frame 的轨迹

      // publish_tracked_pose 默认为false, 默认不发布
      // 如果设置为true, 就发布一个在tracking_frame处的pose
      if (node_options_.publish_tracked_pose) {
        ::geometry_msgs::PoseStamped pose_msg;
        pose_msg.header.frame_id = node_options_.map_frame;
        pose_msg.header.stamp = stamped_transform.header.stamp;
        pose_msg.pose = ToGeometryMsgPose(tracking_to_map);
        tracked_pose_publisher_.publish(pose_msg);
      }

根据参数 node_options_.publish_tracked_pose 的配置决定是否发布。

七、结语

基于前面的分析,可以知道 Node::PublishLocalTrajectoryData() 主要内容为发布 D点云、tf、机器人tracking frame轨迹。当然,具体发布内容跟配置文件中的参数相关。下一篇博客就是对 Node::PublishConstraintList() 这个函数的细节分析了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在cartographer中,使用2D点云进行扫描匹配时,可以使用ceresscanmatch功能。这个功能是基于Ceres Solver库实现的。Ceres Solver是一个非线性优化库,用于解决各种最小化问题。在cartographer中,ceresscanmatch被用于解决2D点云匹配的问题。 具体来说,ceresscanmatch用于匹配两个相邻帧的2D点云。在进行扫描匹配时,需要先对数据进行滤波处理,然后使用ceres进行优化,找到两个点云之间的最佳匹配。在这个过程中,需要使用一种优化算法来最小化匹配误差,这个误差是通过计算点云之间的距离来得到的。 相比于其他扫描匹配方法,ceresscanmatch的优势在于它能够进行非常精准的匹配。这是因为它使用了一个非线性优化算法,能够处理复杂的误差函数和约束条件。此外,ceresscanmatch还支持使用多种不同的误差函数,以适应不同的应用场景。 总之,ceresscanmatch是cartographer中用于2D点云扫描匹配的一个非常重要的功能,它能够让我们更加准确、稳定地进行扫描匹配,并且支持广泛的应用场景。 ### 回答2: 本文将继续介绍cartographer中的ceres扫描匹配部分,ceres扫描匹配是利用Ceres Solver进行的位姿优化,可以准确估计机器人运动的姿态。 ceres扫描匹配部分主要包括ceres_scan_matcher.cc和ceres_scan_matcher.h两个文件。其中ceres_scan_matcher.cc包含了ceres扫描匹配算法的具体实现,而ceres_scan_matcher.h则是相关的头文件。 ceres_scan_matcher.cc中的函数主要有两个,分别是CeresScanMatcher::Match()和CeresScanMatcher::MatchFullSubmap()。其中,CeresScanMatcher::Match()函数用于实现一次扫描匹配,输入参数为当前激光数据和候选的位姿,输出参数为匹配的位姿和评估值。 在CeresScanMatcher::Match()函数中,先通过叶芽上下文来获取轨迹和submap,然后将当前激光数据转换为点云,并对点云进行滤波和预处理,接着定义优化问题和相关的参数,其中优化问题使用ceres::Problem类来定义,相关参数则定义在CeresScanMatcherOptions结构体中,最后通过ceres::Solve()函数进行位姿优化。 CeresScanMatcher::MatchFullSubmap()函数则用于在整个submap上进行匹配,并返回匹配的位姿和评估值。它的实现与CeresScanMatcher::Match()函数类似,只是输入参数为整个submap的信息。 综上所述,ceres扫描匹配部分利用Ceres Solver进行位姿优化,可以准确估计机器人运动的姿态,是cartographer中重要的功能之一。 ### 回答3: cartographer是一款开源的SLAM系统,其源代码完整透明,方便研究和理解。其中,2D点云扫描匹配是cartographer中的一个重要功能,而这一功能又是由ceres扫描匹配实现的。 ceresscanmatch是cartographer中的一个重要模块,用于实现2D点云的扫描匹配。在这个模块中,ceres solver被用来进行优化过程。具体来说,ceresscanmatch会将已知位姿下的实测点云与预测的点云进行匹配,得到匹配误差。随后,ceres solver会对这些匹配误差进行非线性优化,最终得到最优位姿。这样,就能够实现快速准确的2D点云扫描匹配,从而提高了SLAM系统的性能和精度。 在详细研究ceresscanmatch之前,首先需要了解一下ceres solver。ceres solver是一个基于C++的非线性优化库,用于解决复杂的数值优化问题。在cartographer中,ceres solver被用来进行扫描匹配的优化过程,应用目标函数和求解器来寻求最优解。其中,目标函数是由误差项和状态变量构成的,求解器则用来解决这个目标函数并确定状态变量的最优化值。 具体而言,在cartographer中,扫描匹配的目标函数是根据传感器数据得出的,其包括一系列误差项和参考帧的相对位姿。在每个迭代步骤中,ceres solver会计算目标函数的梯度和海森矩阵,并利用这些值来更新参考帧的位姿。当误差项最小化时,相对位姿就能够得到最优解。 总之,ceresscanmatch是cartographer中的一个重要模块,用于实现2D点云的扫描匹配。借助ceres solver进行优化,可以实现高效准确的扫描匹配,为SLAM系统的实现提供了重要的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江南才尽,年少无知!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值