cartographer代码学习-概率栅格地图(ActiveSubmaps2D与Submaps2D)

概率栅格地图是二维激光SLAM的特点,能够将环境通过地图的形式表达出来。

ActiveSubmaps2D作为概率栅格地图中的重要成分,这个对象主要在LocalTrajectoryBuilder2D这里被使用
第一次调用:

active_submaps_(options.submaps_options())

传入一个submap的配置参数。具体的参数在trajectory_builder_2d.lua内:

submaps = {
    num_range_data = 90,          -- 一个子图里插入雷达数据的个数的一半
    grid_options_2d = {
      grid_type = "PROBABILITY_GRID", -- 地图的种类, 还可以是tsdf格式
      resolution = 0.05,
    },
    range_data_inserter = {
      range_data_inserter_type = "PROBABILITY_GRID_INSERTER_2D",
      -- 概率占用栅格地图的一些配置
      probability_grid_range_data_inserter = {
        insert_free_space = true,
        hit_probability = 0.55,
        miss_probability = 0.49,
      },
      -- tsdf地图的一些配置
      tsdf_range_data_inserter = {
        truncation_distance = 0.3,
        maximum_weight = 10.,
        update_free_space = false,
        normal_estimation_options = {
          num_normal_samples = 4,
          sample_radius = 0.5,
        },
        project_sdf_distance_to_scan_normal = true,
        update_weight_range_exponent = 0,
        update_weight_angle_scan_normal_to_ray_kernel_bandwidth = 0.5,
        update_weight_distance_cell_to_hit_kernel_bandwidth = 0.5,
      },
    },
  },

最上面几个是外部参数,其中range_data_inserter_type参数决定了在建立地图写入器的时候选择使用哪个地图写入器:

// 创建地图数据写入器
std::unique_ptr<RangeDataInserterInterface>
ActiveSubmaps2D::CreateRangeDataInserter() {
  switch (options_.range_data_inserter_options().range_data_inserter_type()) {
    // 概率栅格地图的写入器
    case proto::RangeDataInserterOptions::PROBABILITY_GRID_INSERTER_2D:
      return absl::make_unique<ProbabilityGridRangeDataInserter2D>(
          options_.range_data_inserter_options()
              .probability_grid_range_data_inserter_options_2d());
    // tsdf地图的写入器
    case proto::RangeDataInserterOptions::TSDF_INSERTER_2D:
      return absl::make_unique<TSDFRangeDataInserter2D>(
          options_.range_data_inserter_options()
              .tsdf_range_data_inserter_options_2d());
    default:
      LOG(FATAL) << "Unknown RangeDataInserterType.";
  }
}

下面细分两块概率栅格地图参数以及tsdf地图参数。
第二次调用:

if (active_submaps_.submaps().empty())

判断地图是否为空。
第三次调用:

// 使用active_submaps_的第一个子图进行匹配
  std::shared_ptr<const Submap2D> matching_submap =
      active_submaps_.submaps().front();

取出一个子地图进行扫面匹配。
第四次调用:

// 将点云数据写入到submap中
  std::vector<std::shared_ptr<const Submap2D>> insertion_submaps =
      active_submaps_.InsertRangeData(range_data_in_local);

将点云写入栅格地图中。

ActiveSubmaps2D的具体实现是在submap2D.cc文件中:

// ActiveSubmaps2D构造函数
ActiveSubmaps2D::ActiveSubmaps2D(const proto::SubmapsOptions2D& options)
    : options_(options), range_data_inserter_(CreateRangeDataInserter()) {}

这里实际上是使用了一个CreateRangeDataInserter()函数,这个函数创建了一个地图写入器。

ActiveSubmaps2D类中这里面还有一个函数:InsertRangeData

// 将点云数据写入到submap中
std::vector<std::shared_ptr<const Submap2D>> ActiveSubmaps2D::InsertRangeData(
    const sensor::RangeData& range_data) {
  // 如果第二个子图插入节点的数据等于num_range_data时,就新建个子图
  // 因为这时第一个子图应该已经处于完成状态了
  if (submaps_.empty() ||
      submaps_.back()->num_range_data() == options_.num_range_data()) {
    AddSubmap(range_data.origin.head<2>());
  }
  // 将一帧雷达数据同时写入两个子图中
  for (auto& submap : submaps_) {
    submap->InsertRangeData(range_data, range_data_inserter_.get());
  }
  // 第一个子图的节点数量等于2倍的num_range_data时,第二个子图节点数量应该等于num_range_data
  if (submaps_.front()->num_range_data() == 2 * options_.num_range_data()) {
    submaps_.front()->Finish();
  }
  return submaps();
}

这个函数就是用来处理点云到子图的函数。cartographer会同时维护两个子图。当激光雷达的插入次数达到一定的次数时,会停止该子图的插入,然后新建一个新的子图。原来的一个子图会通过Addsubmap函数插入到地图中。
submap子图的思路如下:
算法配置文件默认参数为90。
初始时只有一个子图,当第一个地图达到90帧时,建立第二个子图;
当第一个地图到达180帧时,第二个子图到达90帧,此时将第一个子图的指针删除,同时建立第三个子图,将指针指向第二个与第三个子图。注意这里第一个子图本身是没有被删除的。删除的是指向它的一个指针。
当第二个子图到达180帧时,重复第二步

可以看到子图是以每180次激光数据的处理为一个新的更新的。

新建子图的代码如下:

// 以当前雷达原点为地图原件创建地图
std::unique_ptr<GridInterface> ActiveSubmaps2D::CreateGrid(
    const Eigen::Vector2f& origin) {
  // 地图初始大小,100个栅格
  constexpr int kInitialSubmapSize = 100;
  float resolution = options_.grid_options_2d().resolution(); // param: grid_options_2d.resolution
  switch (options_.grid_options_2d().grid_type()) {
    // 概率栅格地图
    case proto::GridOptions2D::PROBABILITY_GRID:
      return absl::make_unique<ProbabilityGrid>(
          MapLimits(resolution,
                    // 左上角坐标为坐标系的最大值, origin位于地图的中间
                    origin.cast<double>() + 0.5 * kInitialSubmapSize *
                                                resolution *
                                                Eigen::Vector2d::Ones(),
                    CellLimits(kInitialSubmapSize, kInitialSubmapSize)),
          &conversion_tables_);
    // tsdf地图
    case proto::GridOptions2D::TSDF:
      return absl::make_unique<TSDF2D>(
          MapLimits(resolution,
                    origin.cast<double>() + 0.5 * kInitialSubmapSize *
                                                resolution *
                                                Eigen::Vector2d::Ones(),
                    CellLimits(kInitialSubmapSize, kInitialSubmapSize)),
          options_.range_data_inserter_options()
              .tsdf_range_data_inserter_options_2d()
              .truncation_distance(),               // 0.3
          options_.range_data_inserter_options()
              .tsdf_range_data_inserter_options_2d()
              .maximum_weight(),                    // 10.0
          &conversion_tables_);
    default:
      LOG(FATAL) << "Unknown GridType.";
  }
}

可以看出来它支持两种格式的地图:概率栅格地图以及TSDF格式地图。
对于概率地图,它以机器人当前位姿为原点建立一个一定大小的栅格地图。默认是100x100个栅格。当前原点的世界坐标会存入submap类中。对于概率地图,其需要两个参数:MapLimits以及conversion_tables_,这里的conversion_tables_是指转换表,其将0.1-0.9的概率值转换成0-65530的整数形式进行存储,方便进行运算。

总体来说,ActiveSubmaps2D函数执行了三个事情:
1、调用构造函数 CreateRangeDataInserter()新建ProbabilityGridRangeDataInserter2D类的对象
2、调用submaps()函数获取指向submap2D的shared_ptr指针的vector
3、调用雷达写入数据的InsertRangeData函数,同时该函数会调用addsubmap()函数以及submap2D::InsertRangeData函数。addsubmap()调用CreateGrid()函数创建概率地图。同时该函数调用地图相关的几个类函数:MapList、Grid2D以及ProbabilityGrid。

上述操作第三步调用了submap2D的操作,其继承了Submap,对于submap,其有三个私有变量:

const transform::Rigid3d local_pose_; // 子图原点在local坐标系下的坐标
int num_range_data_ = 0;
bool insertion_finished_ = false;

local_pose_代表子图原点在local坐标系下的坐标,num_range_data_为插入激光数据的数量,insertion_finished_代表这张子图是否执行完成。
对于submap的初始化而言,它需要传入一个local_submap_pose作为子图的原点:

Submap(const transform::Rigid3d& local_submap_pose)
      : local_pose_(local_submap_pose) {}

除此之外就是几个简单的赋值以及返回参数的函数,所以submap这块的内容相对而言内容不多,还是比较简单的。

而submap2D主要继承了submap,同时将其没有具体实现的函数进行了实现。此外,对于submap2D的初始化需要的参数主要包含以下部分:


/**
 * @brief 构造函数
 * 
 * @param[in] origin Submap2D的原点,保存在Submap类里
 * @param[in] grid 地图数据的指针
 * @param[in] conversion_tables 地图数据的转换表
 */
Submap2D::Submap2D(const Eigen::Vector2f& origin, std::unique_ptr<Grid2D> grid,
                   ValueConversionTables* conversion_tables)
    : Submap(transform::Rigid3d::Translation(
          Eigen::Vector3d(origin.x(), origin.y(), 0.))),
      conversion_tables_(conversion_tables) {
  grid_ = std::move(grid);
}

可以看到这里的参数其实最终都是传递到submap的。此外,这个类主要也是包含了几个简单的函数调用以及实现,不作具体展开,到这里基本ActiveSubmaps2D与Submaps2D的内容告一段落。

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

余额充值