(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_ 位姿。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 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、付费专栏及课程。

余额充值