Cartographer源码阅读08 回环检测 计算约束

本篇文章,将就如何实现回环检测和计算约束来进行深一步的讨论。

代码位于cartographer\mapping\internal\2d\pose_graph_2d.cc

1.回环检测

为Node进行约束计算:包括当前node_id跟所有submap的约束
和所有的node和insertion_submaps之间的约束
/调用ComputeConstraint()函数实现.

三个参数,一个是刚加入的节点ID;一个是Local Slam返回的insertion_submaps;一个是是否有新finished的submap的判断标志

WorkItem::Result PoseGraph2D::ComputeConstraintsForNode(
    const NodeId& node_id,//刚加入的节点ID
    std::vector<std::shared_ptr<const Submap2D>> insertion_submaps,
    const bool newly_finished_submap)
{
  std::vector<SubmapId> submap_ids;
  std::vector<SubmapId> finished_submap_ids;
  std::set<NodeId> newly_finished_submap_node_ids;

  {
    absl::MutexLock locker(&mutex_);

    //得到该节点的数据.
    const auto& constant_data =
        data_.trajectory_nodes.at(node_id).constant_data;

    // 根据节点数据的时间获取最新的submap的id
    submap_ids = InitializeGlobalSubmapPoses(
        node_id.trajectory_id, constant_data->time, insertion_submaps);

    CHECK_EQ(submap_ids.size(), insertion_submaps.size());
    const SubmapId matching_id = submap_ids.front();

    //当前节点在局部地图中的位姿.
    const transform::Rigid2d local_pose_2d =
        transform::Project2D(constant_data->local_pose *
                             transform::Rigid3d::Rotation(
                                 constant_data->gravity_alignment.inverse()));

    //当前节点在全局地图中的位姿.
    const transform::Rigid2d global_pose_2d =
        optimization_problem_->submap_data().at(matching_id).global_pose *
        constraints::ComputeSubmapPose(*insertion_submaps.front()).inverse() *
        local_pose_2d;

    //把当前节点加入到pose-graph中.
    optimization_problem_->AddTrajectoryNode(
        matching_id.trajectory_id,
        optimization::NodeSpec2D{constant_data->time, local_pose_2d,
                                 global_pose_2d,
                                 constant_data->gravity_alignment});

    //加入当前节点和插入的子图之间的约束.插入的子图一般是两个
    for (size_t i = 0; i < insertion_submaps.size(); ++i)
    {
      const SubmapId submap_id = submap_ids[i];
      // Even if this was the last node added to 'submap_id', the submap will
      // only be marked as finished in 'data_.submap_data' further below.
      CHECK(data_.submap_data.at(submap_id).state ==
            SubmapState::kNoConstraintSearch);
            
      // 加入到PoseGraph维护的容器中
      data_.submap_data.at(submap_id).node_ids.emplace(node_id);

       // 计算相对位姿
      const transform::Rigid2d constraint_transform =
          constraints::ComputeSubmapPose(*insertion_submaps[i]).inverse() *
          local_pose_2d;

      data_.constraints.push_back(
          Constraint{submap_id,
                     node_id,
                     {transform::Embed3D(constraint_transform),
                      options_.matcher_translation_weight(),
                      options_.matcher_rotation_weight()},
                     Constraint::INTRA_SUBMAP});
    }

    // TODO(gaschler): Consider not searching for constraints against
    // trajectories scheduled for deletion.
    // TODO(danielsievers): Add a member variable and avoid having to copy
    // them out here.
    // 得到所有完成了的submap.
    for (const auto& submap_id_data : data_.submap_data)
    {
      if (submap_id_data.data.state == SubmapState::kFinished)
      {
        CHECK_EQ(submap_id_data.data.node_ids.count(node_id), 0);
        finished_submap_ids.emplace_back(submap_id_data.id);
      }
    }

    //如果最新的子图已经完成,把state设置为完成.
    if (newly_finished_submap)
    {
      const SubmapId newly_finished_submap_id = submap_ids.front();
      InternalSubmapData& finished_submap_data =
          data_.submap_data.at(newly_finished_submap_id);

      CHECK(finished_submap_data.state == SubmapState::kNoConstraintSearch);

      finished_submap_data.state = SubmapState::kFinished;

      newly_finished_submap_node_ids = finished_submap_data.node_ids;
    }
  }

  • 新加入的节点与过去所有的子图进行匹配
  • 新建立的子图,与过去所有的节点进行匹配
  • 匹配的过程就是建立约束的过程,也就是回环检测的过程,因为Cartographer 采用的是图优化,有节点和约束的概念,,节点可以理解为激光点的位姿,约束就是激光数据与子图之间相对位姿
  • 阅读代码发现Cartographer采用暴力搜索的方式,计算量巨大
  //对于所有的完成的子图都计算约束---实际上就是回环检测.
  //当前帧激光数据和以往所有的子图进行匹配.
  for (const auto& submap_id : finished_submap_ids)
  {
    ComputeConstraint(node_id, submap_id);
  }
 
  //如果新完成了一个submap,则需要计算当前pose-graph上的所有node,跟当前地图是否有匹配.
  //也可以认为是一个回环检测,不过是反过来的.计算过去的位姿跟当前子图的匹配.
  if (newly_finished_submap)
  {
    const SubmapId newly_finished_submap_id = submap_ids.front();
    // We have a new completed submap, so we look into adding constraints for
    // old nodes.
    // 所有的老的node和当前子图的约束关系.
    for (const auto& node_id_data : optimization_problem_->node_data())
    {
      const NodeId& node_id = node_id_data.id;
      if (newly_finished_submap_node_ids.count(node_id) == 0)
      {
        ComputeConstraint(node_id, newly_finished_submap_id);
      }
    }
  }

  //约束计算完毕.
  constraint_builder_.NotifyEndOfNode();
  absl::MutexLock locker(&mutex_);

  //判断是否需要进行优化.
  ++num_nodes_since_last_loop_closure_;
  if (options_.optimize_every_n_nodes() > 0 &&
      num_nodes_since_last_loop_closure_ > options_.optimize_every_n_nodes())
  {
    return WorkItem::Result::kRunOptimization;
  }
  return WorkItem::Result::kDoNotRunOptimization;
}

Cartographer的回环检测包括节点与子图,子图与节点的回环检测。回环检测完成后,函数最返回是否需要优化,如果需要优化,就开始后端优化

2.计算约束ComputeConstraint

进行约束计算,计算节点node_id和子图submap_id之间的约束.

在ComputeConstraintsForNode()中被调用.

void PoseGraph2D::ComputeConstraint(const NodeId& node_id,
                                    const SubmapId& submap_id)
{
  bool maybe_add_local_constraint = false;
  bool maybe_add_global_constraint = false;
  const TrajectoryNode::Data* constant_data;
  const Submap2D* submap;

  {
    absl::MutexLock locker(&mutex_);
    CHECK(data_.submap_data.at(submap_id).state == SubmapState::kFinished);
    if (!data_.submap_data.at(submap_id).submap->insertion_finished())
    {
      // Uplink server only receives grids when they are finished, so skip
      // constraint search before that.
      return;
    }

    //获取该node和该submap最后一次具有约束的时间
    const common::Time node_time = GetLatestNodeTime(node_id, submap_id);
    // 获取该node和该submap各自对应的trajectory上次连接的时间
    const common::Time last_connection_time =
        data_.trajectory_connectivity_state.LastConnectionTime(
            node_id.trajectory_id, submap_id.trajectory_id);

    //如果节点和子图属于同一条轨迹或者
    //最近有一个约束把该节点和子图的轨迹连接起来了,则有必要进行一个局部窗口搜索匹配.
    //因为他们很可能能连接上.
    if (node_id.trajectory_id == submap_id.trajectory_id ||
        node_time <
            last_connection_time +
                common::FromSeconds(
                    options_.global_constraint_search_after_n_seconds()))
    {
      // If the node and the submap belong to the same trajectory or if there
      // has been a recent global constraint that ties that node's trajectory to
      // the submap's trajectory, it suffices to do a match constrained to a
      // local search window.
      // 为两者添加一个局部约束。
      maybe_add_local_constraint = true;
    }
    //如果两个不属于同一条轨迹 并且 最近也没有什么连接.
    //则说明局部窗口搜索很可能连接不上,因此需要进行全局搜索.
    else if (global_localization_samplers_[node_id.trajectory_id]->Pulse())
    {
      maybe_add_global_constraint = true;
    }

    constant_data = data_.trajectory_nodes.at(node_id).constant_data.get();
    submap = static_cast<const Submap2D*>(
        data_.submap_data.at(submap_id).submap.get());
  }

  //如果是进行局部窗口搜索.--即有一个相对位姿.
  if (maybe_add_local_constraint)
  {
    //如果是同一条轨迹,则可以计算初始的相对位姿.
    const transform::Rigid2d initial_relative_pose =
        optimization_problem_->submap_data()
            .at(submap_id)
            .global_pose.inverse() *
        optimization_problem_->node_data().at(node_id).global_pose_2d;

    //计算约束.
    constraint_builder_.MaybeAddConstraint(
        submap_id, submap, node_id, constant_data, initial_relative_pose);
  }
  //如果是进行全局搜索.--即没有相对位姿.
  else if (maybe_add_global_constraint)
  {
    constraint_builder_.MaybeAddGlobalConstraint(submap_id, submap, node_id,
                                                 constant_data);
  }
}

判断节点和子图是不是同一个轨迹,如果是同一个轨迹,就通过局部位姿,建立局部约束;如果不是同一个轨迹,就建立全局约束。
从本部分最后的代码看出,调用 constraint_builder_.MaybeAddConstraint建立局部约束;调用 constraint_builder_.MaybeAddGlobalConstraint建立局部约束。因此,constraint_builder_是具体的实现了约束的建立。

3.建立约束的具体实现ConstraintBuilder

该代码位于cartographer\mapping\internal\constraints、constraint_builder_2d

这部分代码根据建立局部约束还是全局约束,构造了不同的函数来调用同一个实现函数ComputeConstrain,这个函数最后调用了fast CSM ,这里就是实现了分支定界了的CSM

3.1局部约束

计算节点和子图之间的约束,本函数计算约束需要有一个初始位姿.
因此必须在同一个轨迹上才能满足.

void ConstraintBuilder2D::MaybeAddConstraint(
    const SubmapId& submap_id, const Submap2D* const submap,
    const NodeId& node_id, const TrajectoryNode::Data* const constant_data,
    const transform::Rigid2d& initial_relative_pose)
{
  //超过搜索框的大小,则不进行搜索.
  if (initial_relative_pose.translation().norm() >
      options_.max_constraint_distance())
  {
    return;
  }

  if (!sampler_.Pulse()) return;

  absl::MutexLock locker(&mutex_);
  if (when_done_)
  {
    LOG(WARNING)
        << "MaybeAddConstraint was called while WhenDone was scheduled.";
  }

  constraints_.emplace_back();
  kQueueLengthMetric->Set(constraints_.size());

  auto* const constraint = &constraints_.back();

  //子图对应的fast scan-matcher.每一个submap都有一个fast scan-matcher.
  //为每一个子图,分配一个匹配器
  const auto* scan_matcher =
      DispatchScanMatcherConstruction(submap_id, submap->grid());

  //进行约束计算,类似于后端优化那一部分使用线性池,调用ComputeConstraint
  auto constraint_task = absl::make_unique<common::Task>();
  constraint_task->SetWorkItem([=]() LOCKS_EXCLUDED(mutex_)
  {
    ComputeConstraint(submap_id, submap, node_id, false, /* match_full_submap */
                      constant_data, initial_relative_pose, *scan_matcher,
                      constraint);
  });

  constraint_task->AddDependency(scan_matcher->creation_task_handle);

  //线程池的调度.
  auto constraint_task_handle =
      thread_pool_->Schedule(std::move(constraint_task));

  finish_node_task_->AddDependency(constraint_task_handle);
}

重要步骤:

  • 每一个子图分配一个子图匹配器,调用了 DispatchScanMatcherConstruction()
  • 计算约束,调用了 ComputeConstraint

3.2建立全局约束

进行节点和子图之间的约束的计算,本次函数是全局匹配,不需要初始位姿.因此位姿设置为(0,0,0)
同时也不要求在同一个trajectory上.

void ConstraintBuilder2D::MaybeAddGlobalConstraint(
    const SubmapId& submap_id, const Submap2D* const submap,
    const NodeId& node_id, const TrajectoryNode::Data* const constant_data)
{
  absl::MutexLock locker(&mutex_);
  if (when_done_)
  {
    LOG(WARNING)
        << "MaybeAddGlobalConstraint was called while WhenDone was scheduled.";
  }

  constraints_.emplace_back();
  kQueueLengthMetric->Set(constraints_.size());
  auto* const constraint = &constraints_.back();

  //得到子图对应的scan-matcher.
  const auto* scan_matcher =
      DispatchScanMatcherConstruction(submap_id, submap->grid());

  //进行约束计算.
  auto constraint_task = absl::make_unique<common::Task>();
  constraint_task->SetWorkItem([=]() LOCKS_EXCLUDED(mutex_) {
    ComputeConstraint(submap_id, submap, node_id, true, /* match_full_submap */
                      constant_data, transform::Rigid2d::Identity(),
                      *scan_matcher, constraint);
  });

  //放入线程池中调用.
  constraint_task->AddDependency(scan_matcher->creation_task_handle);
  auto constraint_task_handle =
      thread_pool_->Schedule(std::move(constraint_task));
  finish_node_task_->AddDependency(constraint_task_handle);
}

步骤跟建立局部子图一样

3.3分配子图匹配器

为每一个子图submap_id分配一个scan-matcher.如果该子图已经有scan-matcher则直接返回,如果该子图没有scan-matcher.则为该子图分配一个。
涉及到金字塔的计算,因此需要通过线程的方法来进行计算.

const ConstraintBuilder2D::SubmapScanMatcher*
ConstraintBuilder2D::DispatchScanMatcherConstruction(const SubmapId& submap_id,
                                                     const Grid2D* const grid)
{
  CHECK(grid);
  if (submap_scan_matchers_.count(submap_id) != 0)
  {
    return &submap_scan_matchers_.at(submap_id);
  }

  auto& submap_scan_matcher = submap_scan_matchers_[submap_id];

  submap_scan_matcher.grid = grid;
  //快速CSM
  auto& scan_matcher_options = options_.fast_correlative_scan_matcher_options();

  auto scan_matcher_task = absl::make_unique<common::Task>();

  scan_matcher_task->SetWorkItem(
      [&submap_scan_matcher, &scan_matcher_options]() {
        submap_scan_matcher.fast_correlative_scan_matcher =
            absl::make_unique<scan_matching::FastCorrelativeScanMatcher2D>(
                *submap_scan_matcher.grid, scan_matcher_options);
      });
  submap_scan_matcher.creation_task_handle =
      thread_pool_->Schedule(std::move(scan_matcher_task));
  return &submap_scan_matchers_.at(submap_id);
}

3.4计算约束

计算节点和子图之间的位姿关系,用fast csm来进行匹配,然后用csm来进行优化.

void ConstraintBuilder2D::ComputeConstraint(
    const SubmapId& submap_id, const Submap2D* const submap,
    const NodeId& node_id, bool match_full_submap,
    const TrajectoryNode::Data* const constant_data,
    const transform::Rigid2d& initial_relative_pose,
    const SubmapScanMatcher& submap_scan_matcher,
    std::unique_ptr<ConstraintBuilder2D::Constraint>* constraint)
{
  CHECK(submap_scan_matcher.fast_correlative_scan_matcher);

  //匹配的初始位姿.
  const transform::Rigid2d initial_pose =
      ComputeSubmapPose(*submap) * initial_relative_pose;

  // The 'constraint_transform' (submap i <- node j) is computed from:
  // - a 'filtered_gravity_aligned_point_cloud' in node j,
  // - the initial guess 'initial_pose' for (map <- node j),
  // - the result 'pose_estimate' of Match() (map <- node j).
  // - the ComputeSubmapPose() (map <- submap i)
  // 节点j到子图i之间的约束关系:
  //
  float score = 0.;

  //初始位姿
  transform::Rigid2d pose_estimate = transform::Rigid2d::Identity();

  // Compute 'pose_estimate' in three stages:
  // 1. Fast estimate using the fast correlative scan matcher.
  // 2. Prune if the score is too low.
  // 3. Refine.
  //make_full_submap就是无初始值,全局约束
  if (match_full_submap)
  {
    kGlobalConstraintsSearchedMetric->Increment();

    //进行无初始位姿fast csm匹配.
    //即全局匹配.
    if (submap_scan_matcher.fast_correlative_scan_matcher->MatchFullSubmap(
            constant_data->filtered_gravity_aligned_point_cloud,
            options_.global_localization_min_score(), &score, &pose_estimate))
    {
      CHECK_GT(score, options_.global_localization_min_score());
      CHECK_GE(node_id.trajectory_id, 0);
      CHECK_GE(submap_id.trajectory_id, 0);
      kGlobalConstraintsFoundMetric->Increment();
      kGlobalConstraintScoresMetric->Observe(score);
    }
    else
    {
      return;
    }
  }
  else
  {
    kConstraintsSearchedMetric->Increment();
    //进行有初始位姿的fast csm匹配.
    if (submap_scan_matcher.fast_correlative_scan_matcher->Match(
            initial_pose, constant_data->filtered_gravity_aligned_point_cloud,
            options_.min_score(), &score, &pose_estimate))
    {
      // We've reported a successful local match.
      CHECK_GT(score, options_.min_score());
      kConstraintsFoundMetric->Increment();
      kConstraintScoresMetric->Observe(score);
    }
    else
    {
      return;
    }
  }
  
  {
    absl::MutexLock locker(&mutex_);
    score_histogram_.Add(score);
  }

  // Use the CSM estimate as both the initial and previous pose. This has the
  // effect that, in the absence of better information, we prefer the original
  // CSM estimate.
  // 进行ceres-scan-match优化.
  ceres::Solver::Summary unused_summary;
  ceres_scan_matcher_.Match(pose_estimate.translation(), pose_estimate,
                            constant_data->filtered_gravity_aligned_point_cloud,
                            *submap_scan_matcher.grid, &pose_estimate,
                            &unused_summary);

  //计算得到的约束.
  const transform::Rigid2d constraint_transform =
      ComputeSubmapPose(*submap).inverse() * pose_estimate;

  //根据计算的位姿,重新设置约束的值.--可以认为增加了新的约束.
  constraint->reset(new Constraint{submap_id,
                                   node_id,
                                   {transform::Embed3D(constraint_transform),
                                    options_.loop_closure_translation_weight(),
                                    options_.loop_closure_rotation_weight()},
                                   Constraint::INTER_SUBMAP});

  if (options_.log_matches())
  {
    std::ostringstream info;
    info << "Node " << node_id << " with "
         << constant_data->filtered_gravity_aligned_point_cloud.size()
         << " points on submap " << submap_id << std::fixed;
    if (match_full_submap)
    {
      info << " matches";
    }
    else
    {
      const transform::Rigid2d difference =
          initial_pose.inverse() * pose_estimate;
      info << " differs by translation " << std::setprecision(2)
           << difference.translation().norm() << " rotation "
           << std::setprecision(3) << std::abs(difference.normalized_angle());
    }
    info << " with score " << std::setprecision(1) << 100. * score << "%.";
    LOG(INFO) << info.str();
  }
}


重要步骤:

  • 全局匹配调用MatchFullSubmap,局部匹配调用Match函数;这两个函数均在fast_correlative_scan_matcher这个类里,而且调用的是同一个基函数
  • 再进行ceres优化
  • 计算约束(我的理解是进行一个位姿转换)
  • 增加新约束,或者重设约束

小结

这部分的代码还是很清楚的,大家通读完代码之后就可以意识到Cartographer的函数调用特别多,所以其代码可读性很高。

这部分的代码终于把分支定界的CSM引出来了,下一篇文章就对fast csm进行解读。

  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值