cartographer 代码解读

cartographer 代码解读

编程基础

读懂 cartographer 代码所需编码知识主要包含一下内容:


  • C++ 基础知识 (必要程度:9.9)

    C++ 是嵌入式平台开发的主要语言,读懂cartographer代码需要一定的C++基础知识,同时,解读牛人代码也是提升编程能力必要手段。

    参考链接:(需要掌握该链接中,至少95%内容)

    https://www.runoob.com/cplusplus/cpp-tutorial.html


如果能掌握以上内容,解析cartographer代码肯定没问题,上文没提到的不常用语法功能,程序中基本都加入了注释。但是,也许没有这么多时间系统学习C++相关知识,可以参考C++ 手册,一边看,一边学,当然这样可能会慢点。

参考资料:(nice)

C++ 网络版手册 https://zh.cppreference.com/w/cpp

流程图:

_images/high_level_system_overview.png

一些概念:Submaps,可以理解为:地图是一片一片拼接逐渐形成的,一片地图便是一个submap。

激光数据(range data)经过体素滤波(Voxel Filter)和自适应滤波与当前子地图(submap)匹配。该匹配过程需要里程计(Odometry Pose)提供初始位置(PoseEstimate)。初始位置的确定可以单独采用里程计,也可以采用里程计和陀螺仪(IMU,如果没有IMU,仅仅采用里程计即可)融合的方式。如果里程计也没有呢?可以采用激光与地图直接匹配(算法相对复杂些)。(插入时,同时插入两个活跃的submaps。每个时刻两个submap是活跃的,当前submap和上一个submap)。

激光匹配后,准备插入submap,但并非所有匹配后的激光都要插入submap,只有运动一段时间或举例后才进行匹配后的激光插入,所以需要一个运动过滤器(Motion Filter),不符合运动过滤器的会被丢弃(Dropped)。


以上过程可以理解为局部SLAM,或者说前端: 采集传感信息,形成地图的过程,下面介绍全局优化的基本思路


经过局部SLAM形成结果InsertionResult,主要包括rangde_data,也就是所谓的node和当前的submaps;把结果插入到pose_graph中进一步计算node和submap以及其它约束(odom,landmark)的约束关系(constraints),并根据参数,options_.optimize_every_n_nodes()决定什么时候运行一次全局优化.

基本代码结构:

mapBuilder:实现整个地图构建,包括前端local slam和后端 global slam。

轨迹(trajectory): 可以理解为一次SLAM 从起点到终点过程中的机器人行走轨迹,建图中可以通过startTrajectory和finishTrajectory控制。在轨迹生成的过程中,完成sensor到sumap的生成,以及pose_graph的构建。TrajectoryBuilder(globalTrajectory类)主要通过sensor_collator(localTrajecory类)和pose_graph构成 。sensor_collator实现局部地图构建,最终结果传递给pose_graph;

节点图(poseGraph):具体参考图优化的知识。简单理解图优化(如果没接触过图优化,下面估计看不懂,后续会详细讲):每个插入的激光和生成sumap,以及landmark(后续讲)都可以理解为图优化的一个节点,建图过程中,生成这些点之间的关系,这些关系便是图中的线,最终优化,就是调整点的位置,得到最优值。可以理解为PoseGraph主要实现全局优化(global slam)功能。

代码流程:最终代码运行通过ROS node 方式实现。node中对应topic和service订阅和发布等功能通过MapBuilderBridge类实现。MapBuilderBridge顾名思义,实现ROS节点代码和cartographer功能代码之间的桥接,也可以理解为对cartographer主体代码的接口封装。cartographer主体代码主要功能通过构建地图MapBuilder类实现。此外,MapBuilderBridge 还包含sensorBridge类,实现传感信息ros格式和cartographer自定义格式之间的转换,这些传感器主要包括scan,imu,odom,tf 等。

MapBuilder 类源码注释:

// Wires up the complete SLAM stack with TrajectoryBuilders (for local submaps)
// and a PoseGraph for loop closure.
// 人话:把局部SLAM中submap,node等连接起来,形成 PoseGraph,以便回环优化。
class MapBuilder : public MapBuilderInterface{}

总结:

整个SLAM 过程主要通过TrajectoryBuilder 完成,实现sensor到submap的构建,以及pose_graph 的构建,最后通过pose_graph 实现最终全局优化(finalOptimization)。

其它代码说明:

mapping :实现建图功能

以下四个文件夹都是mapping中要用到的库函数。

common: 时间戳处理,数据统计,参数读取,线程池构建,任务队列等。

io: 读写文件,读写地图,数据类型(proto,pcd,地图grid)转换等。

sensor: range(激光),cloud,imu,odom数据类型封装。

transform: 坐标系转换。

scan处理流程

从scan处理过程理解代码结构。采取至上而下的方式。为了便于理解,以下代码中,删去了大部门源码,仅仅留存我们需要关注的部分。

cartographer_ros/node.cc

// ros node中处理订阅到的激光数据
void Node::HandleLaserScanMessage(const sensor_msgs::LaserScan::ConstPtr& msg) {
  map_builder_bridge_.sensor_bridge(trajectory_id)->HandleLaserScanMessage(sensor_id, msg);
}

cartographer_ros/sensor_bridge.cc

void SensorBridge::HandleLaserScanMessage(
    const std::string& sensor_id, const sensor_msgs::LaserScan::ConstPtr& msg) {
  std::tie(point_cloud, time) = ToPointCloudWithIntensities(*msg);
  HandleLaserScan(sensor_id, time, msg->header.frame_id, point_cloud)
  {
     carto::sensor::TimedPointCloud ranges getfrom(points);
     HandleRangefinder( ranges)
     {
        //trajectory_builder_:通过CollatedTrajectoryBuilder实现
        trajectory_builder_->AddSensorData(
        sensor_id, carto::sensor::TimedPointCloudData{
                       carto::sensor::TransformTimedPointCloud(ranges)});
     }
  }
}

cartographer/mapping/internal/collated_trajectory_builder.cc *****

class CollatedTrajectoryBuilder : public TrajectoryBuilderInterface{
  void AddSensorData(
    const std::string& sensor_id,
    const sensor::TimedPointCloudData& timed_point_cloud_data) override {
    AddData(sensor::MakeDispatchable(sensor_id, timed_point_cloud_data));
  }
}

void CollatedTrajectoryBuilder::AddData(std::unique_ptr<sensor::Data> data) {
  // sensor_collator_:通过Collator实现
  sensor_collator_->AddSensorData(trajectory_id_, std::move(data)); 
}

cartographer/sensor/internal/collator.cc

void Collator::AddSensorData(const int trajectory_id,
                             std::unique_ptr<Data> data) {
  QueueKey queue_key{trajectory_id, data->GetSensorId()};
  queue_.Add(std::move(queue_key), std::move(data));
}

cartographer/sensor/internal/Ordered_multi_queue.cc

void OrderedMultiQueue::Add(const QueueKey& queue_key,
                            std::unique_ptr<Data> data) {
  auto it = queues_.find(queue_key);  // 插入数据
  it->second.queue.Push(std::move(data));  // 并处理  通过call_back 函数处理数据
  Dispatch();//调用一次Add()就要调用一次Dispatch()
}

接下来,寻找处理数据的函数call_back 具体实现形式。从下往上回溯。

由于call_back 函数是在collator.cc 中Collator类的函数AddTrajectory定义的。

cartographer/sensor/internal/collator.cc

void Collator::AddTrajectory(
    const int trajectory_id,
    const absl::flat_hash_set<std::string>& expected_sensor_ids,
    const Callback& callback) {   // call_back  函数在此引入
  for (const auto& sensor_id : expected_sensor_ids) {
    const auto queue_key = QueueKey{trajectory_id, sensor_id};
    queue_.AddQueue(queue_key,
                    [callback, sensor_id](std::unique_ptr<Data> data) {
                      callback(sensor_id, std::move(data));
                    });
    queue_keys_[trajectory_id].push_back(queue_key);
  }
}

cartographer/mapping/internal/collated_trajectory_builder.cc *****

CollatedTrajectoryBuilder::CollatedTrajectoryBuilder(
    const proto::TrajectoryBuilderOptions& trajectory_options,
    sensor::CollatorInterface* const sensor_collator, const int trajectory_id,
    const std::set<SensorId>& expected_sensor_ids,
    std::unique_ptr<TrajectoryBuilderInterface> wrapped_trajectory_builder)
    : sensor_collator_(sensor_collator),
      collate_landmarks_(trajectory_options.collate_landmarks()),
      collate_fixed_frame_(trajectory_options.collate_fixed_frame()),
      trajectory_id_(trajectory_id),
      wrapped_trajectory_builder_(std::move(wrapped_trajectory_builder)),
      last_logging_time_(std::chrono::steady_clock::now()) {

      sensor_collator_->AddTrajectory(
      trajectory_id, expected_sensor_id_strings,
      [this](const std::string& sensor_id, std::unique_ptr<sensor::Data> data) {
        HandleCollatedSensorData(sensor_id, std::move(data));  // call_back 函数在此实现
      });
}

void CollatedTrajectoryBuilder::HandleCollatedSensorData(
    const std::string& sensor_id, std::unique_ptr<sensor::Data> data) {
    data->AddToTrajectoryBuilder(wrapped_trajectory_builder_.get());
}
// 到此,对data的处理过程: call_back 调用HandleCollatedSensorData  ,再调用AddToTrajectoryBuilder , 传入 wrapped_trajectory_builder_
wrapped_trajectory_builder_  的具体实现形式是在初始化CollatedTrajectoryBuilder过程中声明的。
也就是在定义trajectory_builder_ 时,定义的。

trajectory_builder_ 定义在cartographer_ros/sensor_bridge.cc 中。 具体通过 map_builder.cc 实现:

int MapBuilder::AddTrajectoryBuilder(
    const std::set<SensorId>& expected_sensor_ids,
    const proto::TrajectoryBuilderOptions& trajectory_options,
    LocalSlamResultCallback local_slam_result_callback) {
    ....
    trajectory_builders_.push_back(absl::make_unique<CollatedTrajectoryBuilder>(
        trajectory_options, sensor_collator_.get(), trajectory_id,
        expected_sensor_ids,
        CreateGlobalTrajectoryBuilder2D(  // call_back函数的具体形式在此定义
            std::move(local_trajectory_builder), trajectory_id,
            static_cast<PoseGraph2D*>(pose_graph_.get()),
            local_slam_result_callback)));
    ....
  }

到此,基本完成了从下到上对call_back 函数的最终,最后,通过CreateGlobalTrajectoryBuilder2D,由GlobalTrajectoryBuilder2D类实现。

总结

从ros node 在调用函数处理scan信息,最终压入队列queue,该过程中,同时定义了queue的call_back 函数,具体实现流程如下:

从node.cpp -> map_builder_bridge.cc(sensor_bridge <- map_builder) -> collated_trajectory_builder.cc ->collator.cc -> queue.cc

主要涉及的变量: map_builder_bridge <- trajectory_builder < queue_

同样的,对于imu和odom里程计数据,处理方式相似。接下来就需要研究这个核心类。看看scan怎么处理的。


参考上面程序 cartographer/mapping/internal/collated_trajectory_builder.cc,可知,处理data的方式,通过

data->AddToTrajectoryBuilder(wrapped_trajectory_builder_.get()) 实现,这里的wrapped_trajectory_builder_ 即为 GlobalTrajectoryBuilder 类型。

sensor/internal/dispatchable.h

class Dispatchable : public Data {
 public:
  void AddToTrajectoryBuilder(
      mapping::TrajectoryBuilderInterface *const trajectory_builder) override {
    trajectory_builder->AddSensorData(sensor_id_, data_);
  }

显然,data的处理方式是通过GlobalTrajectoryBuilder -> AddSensorData(...)函数来实现的。

cartographer/mapping/internal/global_trajectory_builder.cc

std::unique_ptr<TrajectoryBuilderInterface> CreateGlobalTrajectoryBuilder2D(
    std::unique_ptr<LocalTrajectoryBuilder3D> local_trajectory_builder,
    const int trajectory_id, mapping::PoseGraph3D* const pose_graph,
    const TrajectoryBuilderInterface::LocalSlamResultCallback&
        local_slam_result_callback) {
  return absl::make_unique<
      GlobalTrajectoryBuilder<LocalTrajectoryBuilder3D, mapping::PoseGraph3D>>(
      std::move(local_trajectory_builder), trajectory_id, pose_graph,
      local_slam_result_callback);
}
// 因此组成global_trajectory_builder主要有三个变量:
// 1. local_trajectory_builder
// 2. pose_graph
// 3. local_slam_result_callback

void AddSensorData(
      const std::string& sensor_id,
      const sensor::TimedPointCloudData& timed_point_cloud_data) override {
      matching_result = local_trajectory_builder_->AddRangeData(
            sensor_id, timed_point_cloud_data);
      
      auto node_id = pose_graph_->AddNode(
          matching_result->insertion_result->constant_data, trajectory_id_,
          matching_result->insertion_result->insertion_submaps);

      insertion_result = absl::make_unique<InsertionResult>(InsertionResult{
          node_id, matching_result->insertion_result->constant_data,
          std::vector<std::shared_ptr<const Submap>>(
              matching_result->insertion_result->insertion_submaps.begin(),
              matching_result->insertion_result->insertion_submaps.end())});
      }
    if (local_slam_result_callback_) {
      local_slam_result_callback_(
          trajectory_id_, matching_result->time, matching_result->local_pose,
          std::move(matching_result->range_data_in_local),
          std::move(insertion_result));
    }
  }
// 通过该函数我们知道,对激光信号处理主要经过了四个过程(可以与流程图对照):
// 1. local_trajectory_builder_  addRangeData() 结合里程计数据进行激光匹配
// 2. 把匹配结果加入全局优化图pose_graph 中
// 3. 插入当前局部地图submap中
// 4. 执行 local_slam_result_callback 函数。  local_slam_result_callback  在调用CreateGlobalTrajectoryBuilder2D 时候定义。CreateGlobalTrajectoryBuilder2D在map_builder_调用,map_builder 被 map_builder_bridge 调用。 最终,local_slam_result_callback的定义在map_builder_bridge

cartographer_ros/map_builder_bridge.cc

void MapBuilderBridge::OnLocalSlamResult(
    const int trajectory_id, const ::cartographer::common::Time time,
    const Rigid3d local_pose,
    ::cartographer::sensor::RangeData range_data_in_local) {
    std::shared_ptr<const LocalTrajectoryData::LocalSlamData> local_slam_data =
      std::make_shared<LocalTrajectoryData::LocalSlamData>(
          LocalTrajectoryData::LocalSlamData{time, local_pose,
                                             std::move(range_data_in_local)});
    local_slam_data_[trajectory_id] = std::move(local_slam_data);
}
// 最终把局部slam结果传到local_slam_data_ 这个变量中,便于用户调阅查看。

以上便事local_slam的过程。


pose_graph

pose_graph变量是在map_builder.cc定义的,负责处理全局优化问题 (后端). 全局优化过程与前端local_slam,同时进行,通过多线程的方式实现. 可以简单理解为:map_builder 在建立local_trajectory_builder 接受scan数据进行地图构建的过程中,同时构建全局优化(optimization_problem)对应的线程,进行后端优化.

cartographer/mapping/internal/2d/pose_graph_2d.h

  PoseGraph2D(
      const proto::PoseGraphOptions& options,  // 配置参数
      std::unique_ptr<optimization::OptimizationProblem2D> optimization_problem,//全局优化主体
      common::ThreadPool* thread_pool // 处理全局优化的线程池(可以是一个线程或多个线程) 
  );

简单看下,线程池使用方法:

cartographer/common/thread_pool.h

// 下面代码为源码简化版
class ThreadPool : public ThreadPoolInterface {
 public:
  explicit ThreadPool(int num_threads);//初始化一个线程数量固定的线程池。
  std::weak_ptr<Task> Schedule(std::unique_ptr<Task> task) //添加想要ThreadPool执行的task, 
     // 插入tasks_not_ready_,如果任务满足执行要求,直接插入task_queue_准备执行
      LOCKS_EXCLUDED(mutex_) override;
 private:
  void DoWork();//每个线程初始化时,执行DoWork()函数. 与线程绑定 
  std::deque<std::shared_ptr<Task>> task_queue_ GUARDED_BY(mutex_);  // 准备执行的task
task可能有依赖还未完成
      GUARDED_BY(mutex_);
};

// 线程池初始化时,每个线程与DoWork()函数绑定,也就是线程在后台不断执行DoWork()函数.
// DoWork()函数负责处理 task_queue_ 内的task(函数)
// task 通过 ThreadPool::Schedule  函数压入.

//总结:  ThreadPool初始化后,通过Schedule 压入内部任务队列,并安装队列顺序执行任务.

了解以上内容后,便可以进入pose_graph 看 optimization_problem 是怎样通过 ThreadPool 进行后台执行的.

// AddNode的主要工作是把一个TrajectoryNode的数据加入到PoseGraph维护的trajectory_nodes_这个容器中。并返回加入的节点的Node.  在global_trajectory_builder.AddSensorData()中调用.
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) {e
  // 生成的optimized_pose就是该节点的绝对位姿
  const transform::Rigid3d optimized_pose(
      GetLocalToGlobalTransform(trajectory_id) * constant_data->local_pose);
  const NodeId node_id = trajectory_nodes_.Append(
      trajectory_id, TrajectoryNode{constant_data, optimized_pose});
  // 节点数加1.
  ++num_trajectory_nodes_;

   const SubmapId submap_id =
        submap_data_.Append(trajectory_id, InternalSubmapData());
   submap_data_.at(submap_id).submap = insertion_submaps.back();
  
  // 检查insertion_submaps的第一个submap是否已经被finished了。
  // 如果被finished,那么我们需要计算一下约束并且进行一下全局优化了。
  // 这里是把这个工作交到了workItem中等待处理(通过线程池thread_pool处理)
  const bool newly_finished_submap = insertion_submaps.front()->finished();
  AddWorkItem([=]() REQUIRES(mutex_) {    //该部分用到lamda表达式结果作为addWorkItem()输入参数
    ComputeConstraintsForNode(node_id, insertion_submaps,
                              newly_finished_submap);
  });
  // 上述都做完了,返回node_id.
  return node_id;
}
// 该函数实现 node  和  submaps 插入 pose_graph, 并把优化工作交给线程池处理.
// 我们需要进一步查看  AddWorkItem  和 ComputeConstraintsForNode;

void PoseGraph2D::AddWorkItem(   //   std::function<outType(inType)>  f
    const std::function<WorkItem::Result()>& work_item) {
  absl::MutexLock locker(&work_queue_mutex_);
  if (work_queue_ == nullptr) {   //当work_queue_为空时... 由于work_queue_ 大部分时候非空,为了先看基本逻辑,该调节暂且认为不成立.所以,AddWorkItem函数主要执行 下面push_back部分
    work_queue_ = absl::make_unique<WorkQueue>();
    auto task = absl::make_unique<common::Task>();
    task->SetWorkItem([this]() { DrainWorkQueue(); });
    thread_pool_->Schedule(std::move(task));
  }
  const auto now = std::chrono::steady_clock::now();
  work_queue_->push_back({now, work_item}); // AddWorkItem  每次执行主要部分
}
// 通过以上两个函数,可知,每次addNode时,会把ComputeCocnstraintsForNode(计算约束)函数压入work_queue_.   到此,可以猜测work_queue_的处理函数一定被塞入线程执行. 因此,我们下一步寻找,被压入线程池执行的函数(task) 
//work_queue_ = {{time1,ComputeCocnstraintsForNode1},{time2,ComputeCocnstraintsForNode2},    ... }
// 通过搜索,发现,仅有一处thread_pool_->Schedule(task),压入的函数为: DrainWorkQueue()

// Process pending tasks in the work queue on the calling thread, until the
// queue is either empty or an optimization is required.
void PoseGraph2D::DrainWorkQueue() {
  bool process_work_queue = true;
  size_t work_queue_size;
  while (process_work_queue) {
    std::function<WorkItem::Result()> work_item;
    {
      absl::MutexLock locker(&work_queue_mutex_);
      if (work_queue_->empty()) {
        work_queue_.reset();
        return; // work_queue_  空的时候, thread_pool中DrainWorkQueue执行完,等待下一次addNode时候,再次执行thread_pool_->Schedule(task),再次执行.
      }
      work_item = work_queue_->front().task;
      work_queue_->pop_front();
      work_queue_size = work_queue_->size();
    }
    process_work_queue = work_item() == WorkItem::Result::kDoNotRunOptimization;
    //  执行work_item() 也就是ComputeCocnstraintsForNode 并判断是否需要运行优化
    //  如果需要需要优化,则work_item() = WorkItem::Result::NotRunOptimization, while退出,
    //  如果work_item()都不需要全局优化,则直到work_queue_为空,下面的优化函数不会执行.
  }
  LOG(INFO) << "Remaining work items in queue: " << work_queue_size;
  // We have to optimize again.  when  constraint_builder  done
  constraint_builder_.WhenDone(
      [this](const constraints::ConstraintBuilder2D::Result& result) {
        HandleWorkQueue(result);// 执行全局优化.
      });
}

总结:我们已经明白全局规划的程序思路. 从addNode开始.每次addNode后,会把ComputeCocnstraintsForNode以workItem的形式,压入work_queue_中,交给DrainWorkQueue依次执行.当没有任务时(比如初始化时),DrainWorkQueue作为task的形式被压入到thread_pool中进行处理.  最后,当需要执行全局优化时(这个调节可以由ComputeCocnstraintsForNode结果控制),进行全局优化(HandleWorkQueue).
原文地址:https://www.cnblogs.com/heimazaifei/p/12392231.html