cartographer_learn_18OptimizationProblem2D介绍及轨迹间的连接关系的管理

cartographer_learn_18OptimizationProblem2D介绍及轨迹间的连接关系的管理

续接上篇

上一篇我们讨论到了后端如何存储节点和子图,及节点和子图的存储过程中都是存储的那些东西。其中子图的存储时存储了一根指向submap的指针及一个set用以记录那些与子图有关联的节点,节点存储的是Data这个结构体和节点再global中的位姿。同时我们还学习了cartographer线程池,主要有两点。一点是如何保证在任务队列中没有任务时使得线程阻塞,作者认为这是通过条件变量实现的。一点的如何使得各个任务保持逻辑上的先后顺序,而不是仅仅依靠任务添加的时间顺序执行任务,cartographer的实现是每个任务都有自己的依赖任务,只有依赖任务被执行完毕后才会执行自己的这个任务。
让我们从线程池中回来,继续学习AddNode这个函数的剩余部分。

子图加入优化问题(OptimizationProblem2D)

看看AddNode剩余部分的代码:

NodeId PoseGraph2D::AddNode(
    std::shared_ptr<const TrajectoryNode::Data> constant_data,
    const int trajectory_id,
    const std::vector<std::shared_ptr<const Submap2D>>& insertion_submaps) {
......//计算节点在global中的位姿
......//创建节点存储节点,存储子图
//看看旧的submap是否完成插入
  const bool newly_finished_submap =
      insertion_submaps.front()->insertion_finished();
 
// 把计算约束的工作加入线程池
  AddWorkItem([=]() LOCKS_EXCLUDED(mutex_) {
    return ComputeConstraintsForNode(node_id, insertion_submaps,
                                     newly_finished_submap);
  });
  return node_id;
}

可以看到仍给线程池的任务是一个叫ComputeConstraintsForNode的任务,从名字看这应该是计算节点约束的任务。我们进去看看。

WorkItem::Result PoseGraph2D::ComputeConstraintsForNode(
    const NodeId& node_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没有加入优化问题则加入,并返回insertion_submaps中submap的submap的id
    submap_ids = InitializeGlobalSubmapPoses(
        node_id.trajectory_id, constant_data->time, insertion_submaps);
   ......//一堆其他操作
  }

这个函数一开始没有什么特别的,直到InitializeGlobalSubmapPoses这个函数,我们再进去看看:

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,submap_data类型是MapById
  const auto& submap_data = optimization_problem_->submap_data();
  ......//一堆其他的操作
}

这个函数一来就遇到了一个我们陌生的类optimization_problem_,它的类型是OptimizationProblem2D,从名字看想必它就是做优化的类了。我们先来学习一下这个类。

OptimizationProblem2D

这个类在cartographer/cartographer/mapping/internal/optimization/optimization_2d.h中,上代码:

class OptimizationProblem2D
    : public OptimizationProblemInterface<NodeSpec2D, SubmapSpec2D,
                                          transform::Rigid2d> {
......//一堆类方法
private:
......//一堆类方法
  optimization::proto::OptimizationProblemOptions options_; // 配置项
  MapById<NodeId, NodeSpec2D> node_data_;                   // 节点的各种姿态,包括local坐标系下的,global坐标系下的
  MapById<SubmapId, SubmapSpec2D> submap_data_;             // submap在global下姿态
  std::map<std::string, transform::Rigid3d> landmark_data_; // landmark数据
  sensor::MapByTime<sensor::ImuData> empty_imu_data_;       //imu数据
  sensor::MapByTime<sensor::OdometryData> odometry_data_;   // 里程计数据
  sensor::MapByTime<sensor::FixedFramePoseData> fixed_frame_pose_data_; // gps数据
  std::map<int, PoseGraphInterface::TrajectoryData> trajectory_data_;   // 轨迹数据
}

我们先看看这个类的类成员,发现是一堆MapById,map和MapByTime。虽然这个MapByTime我们之前没有讨论过,但是作者稍微去看了看发现它和MapById差不多,我们到后面有机会想必也能讨论到它,这里我们就先这么认为,它是MapById的变种。这些结构都是用来存储数据的,想必是为优化问题存储数据。那我们先去看看那些数据是如何被加进来的。这里我们稍微列举几个,其他的大致类似。
子图的加入:

void OptimizationProblem2D::AddSubmap(
    const int trajectory_id, const transform::Rigid2d& global_submap_pose) {
  submap_data_.Append(trajectory_id, SubmapSpec2D{global_submap_pose});
}

这里使用的Append这个方法在上一篇的第二节有介绍过,注意这里存储的只有子图的位姿。有趣的是二维的建图优化是似乎不使用imu数据,所有这里加入imu数据的这个函数的空的。
节点的加入:

void OptimizationProblem2D::AddTrajectoryNode(const int trajectory_id,
                                              const NodeSpec2D& node_data) {
  node_data_.Append(trajectory_id, node_data);
  //如果没有储存过这个轨迹的数据,要在轨迹数据那为这个轨迹申请空间
  trajectory_data_[trajectory_id];
}

里程计数据的加入:

void OptimizationProblem2D::AddOdometryData(
    const int trajectory_id, const sensor::OdometryData& odometry_data) {
  odometry_data_.Append(trajectory_id, odometry_data);
}

本次对OptimizationProblem2D的介绍先到这,这个类很重要,想必我们往后还有机会继续学习它。

子图的加入

我们继续回到InitializeGlobalSubmapPoses这个函数中,看看它剩余的部分:

td::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,submap_data类型是MapById
  const auto& submap_data = optimization_problem_->submap_data();
  //插入的submap只有一个的情况,即刚刚启动那会
  if (insertion_submaps.size() == 1) {
    //判断这个子图是不是被加过了,如果没有就加入
    if (submap_data.SizeOfTrajectoryOrZero(trajectory_id) == 0) {
      // 如果没设置初始位姿就是0, 设置了就是1
      if (data_.initial_trajectory_poses.count(trajectory_id) > 0) {
        //这里可以先跳过,我们下一节讨论
        data_.trajectory_connectivity_state.Connect(
            trajectory_id,
            data_.initial_trajectory_poses.at(trajectory_id).to_trajectory_id,
            time);
      }
      //加入
      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));

    const SubmapId submap_id{trajectory_id, 0};
    CHECK(data_.submap_data.at(submap_id).submap == insertion_submaps.front());
    return {submap_id};
  }
  //插入的子图有两个的情况
  CHECK_EQ(2, insertion_submaps.size());

  // EndOfTrajectory有介绍过,就是获取该轨迹最后一个数据的后一个数据
  const auto end_it = submap_data.EndOfTrajectory(trajectory_id);
  CHECK(submap_data.BeginOfTrajectory(trajectory_id) != end_it);
  // prev也介绍过,结合前一句就是获取该轨迹最后一个数据
  const SubmapId last_submap_id = std::prev(end_it)->id;
  // 如果是等于insertion_submaps的第一个元素, 说明insertion_submaps的第二个元素还没有在加入到optimization_problem_中
  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.
    //取第一个子图在global中的位姿
    const auto& first_submap_pose = submap_data.at(last_submap_id).global_pose;
    //在优化问题中插入第二个子图
    optimization_problem_->AddSubmap(
        trajectory_id,
        //wT2 = wT1 * 1Tl * lT2
        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}};
  }
  CHECK(data_.submap_data.at(last_submap_id).submap ==
        insertion_submaps.back());
  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};
}

这个函数分成两种情况,一种是slam过程刚刚开始,插入的子图只有一个的时候和插入的子图是两个的时候。大致的流程都写在备注中了。其中有两个子图时,如何求第二个子图在global中的位姿呢?做法是先找到第一个子图在global中的位姿,再通过第一个子图和第二个子图的关系(在local坐标系下做的转换)求得第二个子图的位姿。同时注意看返回的结果。

轨迹间的连接关系的管理

我们回头看看上面跳过的那个东西——trajectory_connectivity_state,这又是一个新的类,在第16篇时我们也看到过它,但是当时我们不知道这是什么东西。我们现在来看看这是什么。

class TrajectoryConnectivityState {
 public:
 ......//一堆类方法
 private:
//mutable时c++的保留字符
  mutable ConnectedComponents connected_components_;
//记录轨迹间的连接关系即这些连接关系建立的时间
  std::map<std::pair<int, int>, common::Time> last_connection_time_map_;
};

先看这个类的类成员,一来就是一个没见过的类型。这里介绍一下mutable,它的作用时标记这个标量,这个被标记的变量在const的类函数中是可以被改变的。第二个是一个map,记录轨迹间的联系。我们在去看看这个不太熟悉的类ConnectedComponents

class ConnectedComponents {
 public:
......//一堆类方法

 private:
......//一堆类方法

  absl::Mutex lock_;
  // Tracks transitive connectivity using a disjoint set forest, i.e. each
  // entry points towards the representative for the given trajectory.
  std::map<int, int> forest_ GUARDED_BY(lock_);
  // Tracks the number of direct connections between a pair of trajectories.
  std::map<std::pair<int, int>, int> connection_map_ GUARDED_BY(lock_);
};

先看这个类的的两个类成员,两个map,第一个是记录哪些轨迹间有联系,同时它也记录了每条轨迹自己与自己间的联系。比如有1,2,3条轨迹,其中1和2有联系,1和3有联系。则forest_中状态如下图:
在这里插入图片描述

第二个map记录的是这两个轨迹被记录连接的次数。由于学习还不够深入,作者目前还不能完全把握这个类的作用,只值直到这个类的部分作用。所以作者在这里介绍这个类的作用时可能还不够全。如同我们上面举得栗子,有1,2,3这3条轨迹,1和2有联系,1和3有联系。那请问2和3有没有联系呢?是有的,所以仅仅依靠TrajectoryConnectivityState这个类我们无法得知各个轨迹间准确的联系关系,ConnectedComponents这个类就是解决这一问题的。我们下面将把这种联系称之为间接联系。由于对这两个类的把握还不够全面,我们对它们类函数的介绍是遇到一个介绍一个,逐步加深理解。
回到InitializeGlobalSubmapPoses看看我们跳过的代码:

td::vector<SubmapId> PoseGraph2D::InitializeGlobalSubmapPoses(
    const int trajectory_id, const common::Time time,
    const std::vector<std::shared_ptr<const Submap2D>>& insertion_submaps) {

.......//一堆其他操作
if (data_.initial_trajectory_poses.count(trajectory_id) > 0) {
        //为两条轨迹间添加联系
        data_.trajectory_connectivity_state.Connect(
            trajectory_id,
            data_.initial_trajectory_poses.at(trajectory_id).to_trajectory_id,
            time);
      }
 ......//一堆其他操作
 }

去看看Connect这个函数:

void TrajectoryConnectivityState::Connect(const int trajectory_id_a,
                                          const int trajectory_id_b,
                                          const common::Time time) {
  //通过ConnectedComponents这个类去查看这两条轨迹之前是否有联系,有的话直接添加接好了
  if (TransitivelyConnected(trajectory_id_a, trajectory_id_b)) {
    // The trajectories are transitively connected, i.e. they belong to the same
    // connected component. In this case we only update the last connection time
    // of those two trajectories.
    auto sorted_pair = std::minmax(trajectory_id_a, trajectory_id_b);
    if (last_connection_time_map_[sorted_pair] < time) {
      last_connection_time_map_[sorted_pair] = time;
    }
  } else {
    // The connection between these two trajectories is about to join to
    // connected components. Here we update all bipartite trajectory pairs for
    // the two connected components with the connection time. This is to quickly
    // change to a more efficient loop closure search (by constraining the
    // search window) when connected components are joined.
    //如果之间没有联系,除了要添加当前的联系外,还要添加所有间接的联系
    std::vector<int> component_a =
        connected_components_.GetComponent(trajectory_id_a);
    //找到所有与trajectory_id_b有联系的其他轨迹
    std::vector<int> component_b =
        connected_components_.GetComponent(trajectory_id_b);
    for (const auto id_a : component_a) {
      for (const auto id_b : component_b) {
        auto id_pair = std::minmax(id_a, id_b);
        last_connection_time_map_[id_pair] = time;
      }
    }
  }
  connected_components_.Connect(trajectory_id_a, trajectory_id_b);
}

我们先去看看如何判断两条轨迹间之前有无联系的

bool TrajectoryConnectivityState::TransitivelyConnected(
    const int trajectory_id_a, const int trajectory_id_b) const {
  return connected_components_.TransitivelyConnected(trajectory_id_a,
                                                     trajectory_id_b);
}
//调用函数1
bool ConnectedComponents::TransitivelyConnected(const int trajectory_id_a,
                                                const int trajectory_id_b) {
  if (trajectory_id_a == trajectory_id_b) {
    return true;
  }

  absl::MutexLock locker(&lock_);

  if (forest_.count(trajectory_id_a) == 0 ||
      forest_.count(trajectory_id_b) == 0) {
    return false;
  }
  return FindSet(trajectory_id_a) == FindSet(trajectory_id_b);
}
//调用函数2
int ConnectedComponents::FindSet(const int trajectory_id) {
  auto it = forest_.find(trajectory_id);
  CHECK(it != forest_.end());
  if (it->first != it->second) {
    // Path compression for efficiency.
    it->second = FindSet(it->second);
  }
  return it->second;
}

调用函数2实现的过程有点绕,我们使用之前那个栗子说明调用函数2实现的过程。比如说调用函数1的两个参数是2,3。则这个函数会调用FindSet两次,即FindSet(2)和FindSet(3)。这里我们说明FindSet(2),另外一个类似。这里我们追踪迭代器it的变化,如下图:
在这里插入图片描述
所以它的返回是1,FindSet(3)同理。我们理解了FindSet这个函数的实现过程后,我们再去看看如何找到所有与某条轨迹有联系的其他轨迹的呢?即GetComponent这个函数,作者这里偷个懒,把代码贴出来,读者自己理解一下。。。

std::vector<int> ConnectedComponents::GetComponent(const int trajectory_id) {
  absl::MutexLock locker(&lock_);
  const int set_id = FindSet(trajectory_id);
  std::vector<int> trajectory_ids;
  for (const auto& entry : forest_) {
    if (FindSet(entry.first) == set_id) {
      trajectory_ids.push_back(entry.first);
    }
  }
  return trajectory_ids;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值