(02)Cartographer源码无死角解析-(62) 2D后端优化→InitializeGlobalSubmapPoses()子图全局位姿的来龙去脉

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

一、前言

首先这里重复前面的四个个疑问点:

疑问 1 : \color{red}疑问1: 疑问1: global_submap_poses 等价于 PoseGraph2D::data_.global_submap_poses_2d 是何时进行优化的。
疑问 2 : \color{red}疑问2: 疑问2: 为什么要等待约束计算完成之后再调用 PoseGraph2D::HandleWorkQueue(),同时源码又是如何实现的。
疑问 3 : \color{red}疑问3: 疑问3: data_.global_submap_poses_2d 中存储子图的全局位姿,那么子图的全局位姿又是从哪里来的呢?
疑问 4 : \color{red}疑问4: 疑问4: ComputeConstraintsForNode() 如果返回需要优化,源码中是在哪里执行优化的呢?

该篇博客主要是对 PoseGraph2D::ComputeConstraintsForNode() 函数中调用的 InitializeGlobalSubmapPoses() 进行讲解,其同样实现于 src/cartographer/cartographer/mapping/internal/2d/pose_graph_2d.cc 文件中。首先回顾一下调用该函数的过程:

    // 获取节点信息数据
    const auto& constant_data =
        data_.trajectory_nodes.at(node_id).constant_data;
    
    // 获取 trajectory_id 下的正处于活跃状态下的子图的SubmapId
    submap_ids = InitializeGlobalSubmapPoses(
        node_id.trajectory_id, constant_data->time, insertion_submaps);
    CHECK_EQ(submap_ids.size(), insertion_submaps.size());

其先获得节点的静态数据 constant_data (主要由前端计算而来),然后利用其t成员变量 constant_data->time 与 其对应的 node_id.trajectory_id 及 活跃的子图 insertion_submaps 作为形参传入。
 

二、整体注释

在进行细节分析之前,各位朋友可以简单的过一下整体注释(后面有十分详细的讲解):

// 返回指定轨迹id下的正处于活跃状态下的子图的SubmapId
std::vector<SubmapId> PoseGraph2D::InitializeGlobalSubmapPoses(
    const int trajectory_id, const common::Time time,
    const std::vector<std::shared_ptr<const Submap2D>>& insertion_submaps) {
    
    
  CHECK(!insertion_submaps.empty());

  // submap_data中存的: key 为 SubmapId, values 为对应id的Submap在global坐标系下的全局位姿
  const auto& submap_data = optimization_problem_->submap_data();
  
  // 只有slam刚启动时子图的个数才为1
  if (insertion_submaps.size() == 1) {
    
    
    // If we don't already have an entry for the first submap, add one.
    // 如果判断指定id的submap_data的size为0, 这条轨迹上还没有添加submap的pose
    if (submap_data.SizeOfTrajectoryOrZero(trajectory_id) == 0) {
    
    
      // 如果没设置初始位姿就是0, 设置了就是1
      if (data_.initial_trajectory_poses.count(trajectory_id) > 0) {
    
    
        // 把该trajectory_id与其初始位姿的基准轨迹的id关联起来
        data_.trajectory_connectivity_state.Connect(
            trajectory_id,
            data_.initial_trajectory_poses.at(trajectory_id).to_trajectory_id,
            time);
      }
      // 将该submap的global pose加入到optimization_problem_中
      optimization_problem_->AddSubmap(
          trajectory_id, transform::Project2D(
                             ComputeLocalToGlobalTransform(
                                 data_.global_submap_poses_2d, trajectory_id) *
                             insertion_submaps[0]->local_pose()));
    }
    CHECK_EQ(1, submap_data.SizeOfTrajectoryOrZero(trajectory_id));

    // 因为是第一个submap, 所以该submap的ID是(trajectory_id,0), 其中0是submap的index, 从0开始
    const SubmapId submap_id{
    
    trajectory_id, 0};
    // 检查这个SubmapId下的submap是否等于insertion_submaps的第一个元素.因为我们初始化第一个submap肯定是要被插入的那个submap
    CHECK(data_.submap_data.at(submap_id).submap == insertion_submaps.front());
    // 因为是第一个submap, 那就把刚刚建立的submap的id返回
    return {
    
    submap_id};
  }

  CHECK_EQ(2, insertion_submaps.size());

  // 获取 submap_data 的末尾 trajectory_id
  const auto end_it = submap_data.EndOfTrajectory(trajectory_id);
  CHECK(submap_data.BeginOfTrajectory(trajectory_id) != end_it);

  // end_it是最后一个元素的下一个位置, 所以它之前的一个submap的id就是submap_data中的最后一个元素
  // 注意, 这里的last_submap_id 是 optimization_problem_->submap_data() 中的
  const SubmapId last_submap_id = std::prev(end_it)->id;

  // 如果是等于第一个子图, 说明insertion_submaps的第二个子图还没有加入到optimization_problem_中
  // 拿着optimization_problem_中子图的索引, 根据这个索引在data_.submap_data中获取地图的指针
  if (data_.submap_data.at(last_submap_id).submap ==
      insertion_submaps.front()) {
    
    
    // In this case, 'last_submap_id' is the ID of
    // 'insertions_submaps.front()' and 'insertions_submaps.back()' is new.
    
    // 这种情况下, 要给新的submap分配id, 并把它加到OptimizationProblem的submap_data_这个容器中
    const auto& first_submap_pose = submap_data.at(last_submap_id).global_pose;
    // 解算新的submap的global pose, 插入到OptimizationProblem2D::submap_data_中
    optimization_problem_->AddSubmap(
        trajectory_id,
        // first_submap_pose * constraints::ComputeSubmapPose(*insertion_submaps[0]).inverse() = globla指向local的坐标变换
        // globla指向local的坐标变换 * 第二个子图原点在local下的坐标 = 第二个子图原点在global下的坐标
        first_submap_pose *
            constraints::ComputeSubmapPose(*insertion_submaps[0]).inverse() *
            constraints::ComputeSubmapPose(*insertion_submaps[1]));
    return {
    
    last_submap_id,
            SubmapId{
    
    trajectory_id, last_submap_id.submap_index + 1}};
  }

  // 如果是等于第二个子图, 说明第二个子图已经分配了id, 已经在OptimizationProblem的submap_data_中了
  CHECK(data_.submap_data.at(last_submap_id).submap ==
        insertion_submaps.back());
  // 那么第一个子图的index就是last_submap_id.submap_index的前一个, 所以要-1
  const SubmapId front_submap_id{
    
    trajectory_id,
                                 last_submap_id.submap_index - 1};
  CHECK(data_.submap_data.at(front_submap_id).submap ==
        insertion_submaps.front());
  return {
    
    front_submap_id, last_submap_id};
}

三、函数输入

前面已经提到了,该函数接收一个 trajectory_id,节点数据 constant_data 的时间 time,以及目前两个活跃的子图 insertion_submaps。接收到参数之后,首先执行了如下代码:

  // submap_data中存的: key 为 SubmapId, values 为对应id的Submap在global坐标系下的全局位姿
  const auto& submap_data = optimization_problem_->submap_data();

其首先获取 optimization_problem_ 中的所有子图数据,赋值给 submap_data。
 

四、初始处理

如果此时系统刚启动,那么此时 insertion_submaps 中只存储了一个子图,也就符合条件 insertion_submaps.size() == 1。此时会做那些处理呢?源码如下所示:

  // 只有slam刚启动时子图的个数才为1
  if (insertion_submaps.size() == 1) {
    
    
    // If we don't already have an entry for the first submap, add one.
    // 如果判断指定id的submap_data的size为0, 这条轨迹上还没有添加submap的pose
    if (submap_data.SizeOfTrajectoryOrZero(trajectory_id) == 0) {
    
    
      // 如果没设置初始位姿就是0, 设置了就是1
      if (data_.initial_trajectory_poses.count(trajectory_id) > 0) {
    
    
        // 把该trajectory_id与其初始位姿的基准轨迹的id关联起来
        data_.trajectory_connectivity_state.Connect(
            trajectory_id,
            data_.initial_trajectory_poses.at(trajectory_id).to_trajectory_id,
            time);
      }
      // 将该submap的global pose加入到optimization_problem_中
      optimization_problem_->AddSubmap(
          trajectory_id, transform::Project2D(
                             ComputeLocalToGlobalTransform(
                                 data_.global_submap_poses_2d, trajectory_id) *
                             insertion_submaps[0]->local_pose()));
    }
    CHECK_EQ(1, submap_data.SizeOfTrajectoryOrZero(trajectory_id));

    // 因为是第一个submap, 所以该submap的ID是(trajectory_id,0), 其中0是submap的index, 从0开始
    const SubmapId submap_id{
    
    trajectory_id, 0};
    // 检查这个SubmapId下的submap是否等于insertion_submaps的第一个元素.因为我们初始化第一个submap肯定是要被插入的那个submap
    CHECK(data_.submap_data.at(submap_id).submap == insertion_submaps.front());
    // 因为是第一个submap, 那就把刚刚建立的submap的id返回
    return {
    
    submap_id};
  }

其首先判断一下当前这个活跃的子图是否被添加到 submap_data 之中,如果没有添加,则调用 optimization_problem_->AddSubmap() 函数添加至 optimization_problem_ 之中。当然,在这之前首先会判断一下是否有为该轨迹设置初始位姿,如果设置了会调用 data_.trajectory_connectivity_state.Connect 把 trajectory_id 与其初始位姿的基准轨迹的id关联起来。简单的说,就是如果之前已经存在一条轨迹 t 了,可以同通过配置文件中的 initial_trajectory_pose 参数,为 trajectory_id 这条轨迹设置一个相对于轨迹 t 的基准位姿作为初始位置。

需要注意的是,在为 optimization_problem_ 添加子图时,其调用了 ComputeLocalToGlobalTransform 函数计算子图的全局位姿。该函数前面在前面的博客 (02)Cartographer源码无死角解析-(55) 2D后端优化→ComputeLocalToGlobalTransform(),TrajectoryNode 中有进行讲解,先放一下,稍后我们回过来再分析一下。

因为这是第一个子图,所以 trajectory_id 轨迹对应的子图只能存在一个,所以执行

CHECK_EQ(1, submap_data.SizeOfTrajectoryOrZero(trajectory_id));

这段代码判断一下,确保逻辑上的正确性。接着为该子图创建一个 SubmapId 对象,且子图的序列好为 0,表示 trajectory_id 轨迹上的第一个子图。然后以列表的形式返回这个 SubmapId 对象 {submap_id}。
 

四、添加第二个活跃的子图

完成初始处理之后,后续的都是常规处理了。除了建图开始阶段只有一个活跃的子图,后续都存在两个活跃的子图,地图更新或者插入点云时,都是同时往这两个活跃的子图中插入的。所以源码中执行了 CHECK_EQ(2, insertion_submaps.size()),同理是为了确保逻辑的正确性。接着可以看到如下代码:

  // 获取 submap_data 的末尾 trajectory_id
  const auto end_it = submap_data.EndOfTrajectory(trajectory_id);
  CHECK(submap_data.BeginOfTrajectory(trajectory_id) != end_it);

  // end_it是最后一个元素的下一个位置, 所以它之前的一个submap的id就是submap_data中的最后一个元素
  // 注意, 这里的last_submap_id 是 optimization_problem_->submap_data() 中的
  const SubmapId last_submap_id = std::prev(end_it)->id;

第一步是确保 submap_data 中轨迹 trajectory_id 对应的子图数不为0,另外获得最后一个子图的 SubmapId。随后,运行了如下这段代码:

  // 如果是等于第一个子图, 说明insertion_submaps的第二个子图还没有加入到optimization_problem_中
  // 拿着optimization_problem_中子图的索引, 根据这个索引在data_.submap_data中获取地图的指针
  if (data_.submap_data.at(last_submap_id).submap ==
      insertion_submaps.front()) {
    
    
    // In this case, 'last_submap_id' is the ID of
    // 'insertions_submaps.front()' and 'insertions_submaps.back()' is new.
    
    // 这种情况下, 要给新的submap分配id, 并把它加到OptimizationProblem的submap_data_这个容器中
    const auto& first_submap_pose = submap_data.at(last_submap_id).global_pose;
    // 解算新的submap的global pose, 插入到OptimizationProblem2D::submap_data_中
    optimization_problem_->AddSubmap(
        trajectory_id,
        // first_submap_pose * constraints::ComputeSubmapPose(*insertion_submaps[0]).inverse() = globla指向local的坐标变换
        // globla指向local的坐标变换 * 第二个子图原点在local下的坐标 = 第二个子图原点在global下的坐标
        first_submap_pose *
            constraints::ComputeSubmapPose(*insertion_submaps[0]).inverse() *
            constraints::ComputeSubmapPose(*insertion_submaps[1]));
    return {
    
    last_submap_id,
            SubmapId{
    
    trajectory_id, last_submap_id.submap_index + 1}};
  }

其先判断一下 trajectory_id 对应的最后子图 last_submap_id,其是否为活跃子图的第一个子图,如果是,则说明第二个活跃的子图还没有添加到 optimization_problem_ 之中,接下来的操作不用多说,也知道就是调用 optimization_problem_->AddSubmap() 函数进行添加。first_submap_pose 活跃的第一个子图(第二个此时还没有添加)的 global 位姿,这里我们记为 S u b m a p 1 p o s e g l o b a l \mathbf {Submap1}^{global}_{pose} Submap1poseglobal,constraints::ComputeSubmapPose(*insertion_submaps[0]) 是第一个活跃子图的第 local 位姿,这里我们记为 S u b m a p 1 p o s e l o c a l \mathbf {Submap1}^{local}_{pose} Submap1poselocal,constraints::ComputeSubmapPose(*insertion_submaps[1]) 当然表示第二个子图的 local 位姿,同理记为 S u b m a p 2 p o s e l o c a l \mathbf {Submap2}^{local}_{pose} Submap2poselocal,那么最终等价的数学公式如下:
S u b m a p 2 p o s e g l o b a l = S u b m a p 1 p o s e g l o b a l ∗ [ S u b m a p 1 p o s e l o c a l ] − 1 ∗ S u b m a p 2 p o s e l o a c l (01) \color{Green} \tag{01} \mathbf {Submap2}^{global}_{pose} = \mathbf {Submap1}^{global}_{pose}*[\mathbf {Submap1}^{local}_{pose}]^{-1}*\mathbf {Submap2}^{loacl}_{pose} Submap2poseglobal=Submap1poseglobal[Submap1poselocal]1Submap2poseloacl(01)
可以很明显的知道最终求得 Submap2,也就是第二个活跃的子图 global 系下的位姿。添加了一个新的子图到 optimization_problem_ 之中,其构建的 SubmapId 对应的 submap_id 比之前进行 +1 操作。然后返回两个活跃子图的 SubmapId。
 

五、添加第二个活跃的子图

通过前面四、五的两种情况,就会为把所有活跃的子图都添加至 optimization_problem_ 之中了,且每个子图都只被添加了一次。添加之后,optimization_problem_ 就存储了子图对应的 SubmapId,后续自己获取返回即可,代码如下:

  // 如果是等于第二个子图, 说明第二个子图已经分配了id, 已经在OptimizationProblem的submap_data_中了
  CHECK(data_.submap_data.at(last_submap_id).submap ==
        insertion_submaps.back());
  // 那么第一个子图的index就是last_submap_id.submap_index的前一个, 所以要-1
  const SubmapId front_submap_id{
    
    trajectory_id,
                                 last_submap_id.submap_index - 1};
  CHECK(data_.submap_data.at(front_submap_id).submap ==
        insertion_submaps.front());
  return {
    
    front_submap_id, last_submap_id};

六、ComputeLocalToGlobalTransform

疑问 3 : \color{red}疑问3: 疑问3: data_.global_submap_poses_2d 中存储子图的全局位姿,那么子图的全局位姿又是从哪里来的呢?

现在就是要解答这个疑问了,回到前面提到的 ComputeLocalToGlobalTransform() 函数,首先要注意的是,其只在 optimization_problem_ 添加第一个子图的时候需要调用该函数,后续子图的 global 位姿都是依靠前一个子图的 global 位姿计算出来的。该函数又是依旧 data_.global_submap_poses_2d 推算子图 global 位姿的。但是, 注意 : \color{red}注意: 注意: , 此时第一个子图都还没有添加,那么也就是说 data_.global_submap_poses_2d 肯定是空的,也就是说 ComputeLocalToGlobalTransform() 函数执行的是下面这段代码:

  // 没找到这个轨迹id
  if (begin_it == end_it) {
    
    
    const auto it = data_.initial_trajectory_poses.find(trajectory_id);
    // 如果设置了初始位姿
    if (it != data_.initial_trajectory_poses.end()) {
    
    
      return GetInterpolatedGlobalTrajectoryPose(it->second.to_trajectory_id,
                                                 it->second.time) *
             it->second.relative_pose;
    }
    // note: 没设置初始位姿就将返回(0,0,0)的平移和旋转
    else {
    
    
      return transform::Rigid3d::Identity();
    }

总的来说,如果 trajectory_id 轨迹有设置先对于其他轨迹的初始位姿,则会使用线性插值计算出子图的 global 位姿,如果没有设置,则认为第一个子图的位姿就是 transform::Rigid3d::Identity()。
 

七、总结

这样我们就解答了

疑问 3 : \color{red}疑问3: 疑问3: data_.global_submap_poses_2d 中存储子图的全局位姿,那么子图的全局位姿又是从哪里来的呢?

总的来说,第一个子图的 global 位姿认为是 transform::Rigid3d::Identity(),后面的子图位姿都是参考结合子图的局部位姿,近而推算出 global_ 位姿。

猜你喜欢

转载自blog.csdn.net/weixin_43013761/article/details/131441945