(02)Cartographer源码无死角解析-(67) 2D后端优化→FastCorrelativeScanMatcher2D -分支定界算法(BranchAndBound)1

news2024/11/24 1:27:37

讲解关于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官方认证

一、前言

在上一篇博客中对 PrecomputationGrid 与 PrecomputationGridStack2D进行了纤细分析,其主要作用是用于构建多分辨率低,且在最后我们提到,存储在 PrecomputationGridStack2D 中层级越高的,则表示其分辨率约粗。该篇博客主要的目的就是实现如下:
在这里插入图片描述由图1、图2、图3 估算出机器人(节点在) 图gt 的位姿。这里假设 .lua 文件中的参数 branch_and_bound_depth=3为例进行讲解。其上的 图1、图2、图3 看作上篇博客求解出来的多分辨率地图。

如图3所示,首先使用最最粗的分辨率,其中机器人的位姿认为是一个初始位姿,这里记为 R o b o t 3 t r a c k i n g l o c a l \mathbf {Robot3}^{local}_{tracking} Robot3trackinglocal,很明显,基于这种比较粗的分辨率地图,是很容易匹配上的,但是很不精准。因为其对应的 width=4,如下图所示,4x4个像素会作为1个像素来对待,如下图所示,本人只绘画了一部分,也就是蓝色与黄色区域:
在这里插入图片描述
接下来就要 注意: \color{red}注意: 注意:可以看到,机器人位姿落在 local 系下黄色区域,在[(0,1),(0,2),(1,1),(1,2)] 这4点处(从0开始),进行暴力相关性扫描匹配,都能得到不错的优解,我们记为最优候选界,接下来,那么我们找到 图2中的对应区域,需要注意,图2的 width=2,所以其会把 2x2=4 个像素作为1个像素来对待,如下:
在这里插入图片描述
首先对上诉中的[2:5,0:3],也就是绿色矩形框内区域进行暴力相关性扫描匹配,最终找到四个候选解,也就是黄色区域,4点坐标分别为[(2,2),(2,3),(3,2),(3,3)] 。同理,再在图一中找到这四个点对应的区域,再进行暴力相关性扫描匹配,如下:
在这里插入图片描述
通过绿色矩形框内区域的暴力相关性扫描匹配,最终又可以求得四个不错的解,不过这次我们可以直接选取最高的那一个。也就是最终解。上图中的最优解与最前面的 图gt 的解是十分接近的。

这里使用图示的方式进行讲解,虽然比较容易理解,但是不太好与树、枝、树深度、分支等名词联系起来。等分析完源码之后,我们再以树状的形式比体现一下,大家就明白怎么回事,为什么这个算法叫分支定界算法了。

二、::FastCorrelativeScanMatcher2D()

文件 fast_correlative_scan_matcher_2d.h 中可以看到 FastCorrelativeScanMatcher2D 类的声名,其成员变量十分的的简单,所以跳过了,直接从 FastCorrelativeScanMatcher2D 的构造函数讲起,不过其构造函数也是十分的简单,如下所示:

// 构造函数
FastCorrelativeScanMatcher2D::FastCorrelativeScanMatcher2D(
    const Grid2D& grid,
    const proto::FastCorrelativeScanMatcherOptions2D& options)
    : options_(options),
      limits_(grid.limits()),
      // 多分辨率地图的构建
      precomputation_grid_stack_(
          absl::make_unique<PrecomputationGridStack2D>(grid, options)) {}

其就是根据参数配置以及 grid 构建了一个多分辨率地图,即 PrecomputationGridStack2D 实例对象赋值给 precomputation_grid_stack_。

三、 ::Match() 与 SearchParameters

该函数主要调用了 MatchWithSearchParameters() 这个函数,如下:

/**
 * @brief 进行局部搜索窗口的约束计算(对局部子图进行回环检测)
 * 
 * @param[in] initial_pose_estimate 先验位姿
 * @param[in] point_cloud 原点位于local坐标系原点处的点云
 * @param[in] min_score 最小阈值, 低于这个分数会返回失败
 * @param[out] score 匹配后的得分
 * @param[out] pose_estimate 匹配后得到的位姿
 * @return true 匹配成功, 反之匹配失败
 */
bool FastCorrelativeScanMatcher2D::Match(
    const transform::Rigid2d& initial_pose_estimate,
    const sensor::PointCloud& point_cloud, const float min_score, float* score,
    transform::Rigid2d* pose_estimate) const {
  // param: linear_search_window angular_search_window 
  const SearchParameters search_parameters(options_.linear_search_window(),
                                           options_.angular_search_window(),
                                           point_cloud, limits_.resolution());
  return MatchWithSearchParameters(search_parameters, initial_pose_estimate,
                                   point_cloud, min_score, score,
                                   pose_estimate);
}

首先 initial_pose_estimate 表示机器人(节点的)初始位姿,或者说是待优化位姿。point_cloud 应该是 local 系下的点云数据。min_score 是一个阈值,后续计算出来的位姿解,只要得分大于该阈值,都看作是一个最有候选解。关于 SearchParameters 这个类,再前面有十分纤细的介绍,如下链接。

(02)Cartographer源码无死角解析-(49) 2D点云扫描匹配→相关性暴力匹配1:SearchParameters

该类中有几个函数是比较重要的,分别就是 SearchParameters ::GenerateRotatedScans() 与 DiscretizeScans(),作用分别是对地图上角度与位置上的遍历。另外还有 SearchParameters::ShrinkToFit() 这个函数。其再前面的博客种由于没有使用到,并没有进行分析,现在来看一下:

// 计算每一帧点云 在保证最后一个点能在地图范围内时 的最大移动范围
void SearchParameters::ShrinkToFit(const std::vector<DiscreteScan2D>& scans,
                                   const CellLimits& cell_limits) {
  CHECK_EQ(scans.size(), num_scans);
  CHECK_EQ(linear_bounds.size(), num_scans);

  // 遍历生成的旋转后的很多scan
  for (int i = 0; i != num_scans; ++i) {
    Eigen::Array2i min_bound = Eigen::Array2i::Zero();
    Eigen::Array2i max_bound = Eigen::Array2i::Zero();

    // 对点云的每一个点进行遍历, 确定这帧点云的最大最小的坐标索引
    for (const Eigen::Array2i& xy_index : scans[i]) {
      // Array2i.min的作用是 获取对应元素的最小值组成新的Array2i
      min_bound = min_bound.min(-xy_index);
      max_bound = max_bound.max(Eigen::Array2i(cell_limits.num_x_cells - 1,
                                               cell_limits.num_y_cells - 1) -
                                xy_index);
    }

    // 计算每一帧点云 在保证最后一个点能在地图范围内时 的最大移动范围
    linear_bounds[i].min_x = std::max(linear_bounds[i].min_x, min_bound.x());
    linear_bounds[i].max_x = std::min(linear_bounds[i].max_x, max_bound.x());
    linear_bounds[i].min_y = std::max(linear_bounds[i].min_y, min_bound.y());
    linear_bounds[i].max_y = std::min(linear_bounds[i].max_y, max_bound.y());
  }
}

scans 中包含了多帧点云数据,该函数会遍历每一帧点云,然后判断一帧点云在地图中能够朝4个方向移动的最大距离,按照该距离移动点云数据之后,保证至少有一个点云数据位于地图之后,否则就没有意义了,遍历的时候点云都跑到地图外面去了。

::Match() 函数在构建完 SearchParameters 实例 search_parameters之后,接着调用了 ::MatchWithSearchParameters() 函数,且把 search_parameters 作为实参传入。

四、::MatchWithSearchParameters()

( 1 ): \color{blue}(1): 1):从名字不难看出,该函数就是根据 SearchParameters 对象进行扫描匹配。首先来看该函数的输入:

// 进行基于分支定界算法的粗匹配
bool FastCorrelativeScanMatcher2D::MatchWithSearchParameters(
    SearchParameters search_parameters,
    const transform::Rigid2d& initial_pose_estimate,
    const sensor::PointCloud& point_cloud, float min_score, float* score,
    transform::Rigid2d* pose_estimate) const {
  CHECK(score != nullptr);
  CHECK(pose_estimate != nullptr);

该函数接受一个 SearchParameters 实例对象,以及一个初始位姿 initial_pose_estimate,同时还有进行暴力搜索的点云数据,这里的点云数据应该还是居于 local 系的,另外 min_score 用于过滤一些比好的位姿解。score 与 pose_estimate 分别用于存储匹配之后的分值与位姿。

( 2 ): \color{blue}(2): 2):接着就是对点云数据进行了一个旋转,点云的角度是相对于机器人(节点)坐标系了。然后在调用 GenerateRotatedScans 函数生成各个需要遍历角度的点云数据,本质上也是对点云数据进行旋转。

  // Step: 将原点处的点云先旋转到预测的方向上
  const Eigen::Rotation2Dd initial_rotation = initial_pose_estimate.rotation();
  const sensor::PointCloud rotated_point_cloud = sensor::TransformPointCloud(
      point_cloud,
      transform::Rigid3f::Rotation(Eigen::AngleAxisf(
          initial_rotation.cast<float>().angle(), Eigen::Vector3f::UnitZ())));

  // Step: 生成按照不同角度旋转后的点云集合
  const std::vector<sensor::PointCloud> rotated_scans =
      GenerateRotatedScans(rotated_point_cloud, search_parameters);

( 3 ): \color{blue}(3): 3): 将旋转后的点云帧集合进行平移,平移之后的点云数据此时无论旋转还是平移都是相对于机器人(节点)坐标系了。然后或者这些点云数据在地图上的索引,如下

  // Step: 将旋转后的点云集合按照预测出的平移量进行平移, 获取平移后的点在地图中的索引
  // 这里的离散激光点是在最细的分辨率的地图上面
  const std::vector<DiscreteScan2D> discrete_scans = DiscretizeScans(
      limits_, rotated_scans,
      Eigen::Translation2f(initial_pose_estimate.translation().x(),
                           initial_pose_estimate.translation().y()));
  
  // 缩小搜索窗口的大小, 计算每一帧点云在保证最后一个点能在地图范围内时的最大移动范围
  search_parameters.ShrinkToFit(discrete_scans, limits_.cell_limits());

接着调用 search_parameters.ShrinkToFit() 函数,计算每一帧点云在保证最后一个点能在地图范围内时的最大移动范围,search_parameters.linear_bounds变量中。

( 4 ): \color{blue}(4): 4): 记者就是调用 ComputeLowestResolutionCandidates() 函数获得分辨率最低(障碍物最粗)地图的所有合格的候选解,具体的细节稍后再讨论。

  // 计算最低分辨率中的所有的候选解 最低分辨率是通过搜索树的层数、地图的分辨率计算出来的.
  // 对于地图坐标系来说 最低分辨率=1<<h, h表示搜索树的总的层数
  // 这里不但对最低分辨率的所有候选解的得分进行了计算, 同时还按照从大到小排列
  const std::vector<Candidate2D> lowest_resolution_candidates =
      ComputeLowestResolutionCandidates(discrete_scans, search_parameters);

( 5 ): \color{blue}(5): 5): 由了最初的一批候选解之后,就可以调用分支定界算法了,也就是BranchAndBound() 函数,代码如下所示:

  // Step: 进行基于分支定界算法的搜索, 获取最优解
  const Candidate2D best_candidate = BranchAndBound(
      discrete_scans, search_parameters, lowest_resolution_candidates,
      precomputation_grid_stack_->max_depth(), min_score); // param: max_depth
  
  // 检查最优解的值, 如果大于指定阈值min_score就认为匹配成功,否则认为不匹配返回失败
  if (best_candidate.score > min_score) {
    *score = best_candidate.score;
    // Step: 根据计算出的偏移量对位姿进行校准
    *pose_estimate = transform::Rigid2d(
        {initial_pose_estimate.translation().x() + best_candidate.x,
         initial_pose_estimate.translation().y() + best_candidate.y},
        initial_rotation * Eigen::Rotation2Dd(best_candidate.orientation));
    return true;
  }

对于 BranchAndBound() 暂时也不做详细分析,后续单独讲解。最后再判断一下最优的解是否超过指定阈值 min_score,如果超过,则对 initial_pose_estimate 进行调整,获得最有位姿。

五、::ComputeLowestResolutionCandidates()

现在回过头来分析代码 ComputeLowestResolutionCandidates。该代码其实也比较好理解,前面提到,源码中最低分辨率的地图:

precomputation_grids_[6]       width=64      可以简单理解,比原始地图构建的分辨率地图
增加x,y增加64-1=1个像素,且每 64x64=4096 个像素的像素值都用他们中最大的来代替(略有区别)

把 4096 个像素当作一个新像素来对待,且是对一个子图进行处理,那即使对子图的所有新像素方式进行遍历,也不会消耗多少资源,来看源码如下:

// 生成最低分辨率层(栅格最粗)上的所有候选解, 并进行打分与排序
std::vector<Candidate2D>
FastCorrelativeScanMatcher2D::ComputeLowestResolutionCandidates(
    const std::vector<DiscreteScan2D>& discrete_scans,
    const SearchParameters& search_parameters) const {

  // 生成最低分辨率层(栅格最粗)上的所有候选解
  std::vector<Candidate2D> lowest_resolution_candidates =
      GenerateLowestResolutionCandidates(search_parameters);

  // 计算每个候选解的得分, 按照匹配得分从大到小排序, 返回排列好的candidates 
  ScoreCandidates(
      precomputation_grid_stack_->Get(precomputation_grid_stack_->max_depth()),
      discrete_scans, search_parameters, &lowest_resolution_candidates);
  return lowest_resolution_candidates;
}

看起来逻辑是十分简单的,先调用 GenerateLowestResolutionCandidates() 函数根据 search_parameters 在前面调用 search_parameters.ShrinkToFit() 计算处出来,点云数据在在地图中移动的最大距离求得所有相关的候选解。然后通过 ScoreCandidates() 函数按降序方式排列。关于 GenerateLowestResolutionCandidates() 函数的代码不是很复杂,如下:

// 生成最低分辨率层(栅格最粗)上的所有候选解
std::vector<Candidate2D>
FastCorrelativeScanMatcher2D::GenerateLowestResolutionCandidates(
    const SearchParameters& search_parameters) const {
  const int linear_step_size = 1 << precomputation_grid_stack_->max_depth();
  int num_candidates = 0;
  // 遍历旋转后的每个点云
  for (int scan_index = 0; scan_index != search_parameters.num_scans;
       ++scan_index) {

    // X方向候选解的个数
    const int num_lowest_resolution_linear_x_candidates =
        (search_parameters.linear_bounds[scan_index].max_x -
         search_parameters.linear_bounds[scan_index].min_x + linear_step_size) /
        linear_step_size;

    // Y方向候选解的个数
    const int num_lowest_resolution_linear_y_candidates =
        (search_parameters.linear_bounds[scan_index].max_y -
         search_parameters.linear_bounds[scan_index].min_y + linear_step_size) /
        linear_step_size;

    // num_candidates 为最低分辨率这一层中所有候选解的总个数
    num_candidates += num_lowest_resolution_linear_x_candidates *
                      num_lowest_resolution_linear_y_candidates;
  }

  // 将所有候选解保存起来, 候选解的结构为(角度的索引, x偏移量, y偏移量, 搜索参数)
  std::vector<Candidate2D> candidates;
  candidates.reserve(num_candidates);

  for (int scan_index = 0; scan_index != search_parameters.num_scans;
       ++scan_index) {
    for (int x_index_offset = search_parameters.linear_bounds[scan_index].min_x;
         x_index_offset <= search_parameters.linear_bounds[scan_index].max_x;
         x_index_offset += linear_step_size) {
      for (int y_index_offset =
               search_parameters.linear_bounds[scan_index].min_y;
           y_index_offset <= search_parameters.linear_bounds[scan_index].max_y;
           y_index_offset += linear_step_size) {
        // 生成候选解, 存的是候选解与原始点云原点坐标间的偏移量
        candidates.emplace_back(scan_index, x_index_offset, y_index_offset,
                                search_parameters);
      }
    }
  }
  CHECK_EQ(candidates.size(), num_candidates);
  return candidates;
}

其首先循环遍历点云,这里简单理解为角度的遍历。循环过程中,有一个参数,那么就是 linear_step_size,这里就是等价于前面提到的 width=64,也就是 64x64=4096 作为一个像素来对待。遍历的过程中,使用到了 search_parameters.linear_bounds,遍历的每帧点云都要属于自己的 linear_bound,这样能保证所有的候选解点云帧至少有一个点云在地图中。

随后的操作需要注意一下,候选解存储的是相对于初始点云(机器人系)原点坐标间的偏移量,并不是一个绝对坐标。

六、结语

该篇博客通过首先通过图示,形象且直观的展示分支定界算法核心原理,且在后续 过程中讲解了最低分辨率求得候选解的过程,且对候选者解进行降序排列。但是关于分支定界最核心的算法部分,即 FastCorrelativeScanMatcher2D::BranchAndBound() 暂未讲解,这就是下篇博客的主要内容了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/709493.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java——包(package)

一、Java中的包(package) 在Java中&#xff0c;包&#xff08;package&#xff09;是一种用于组织类和接口的命名空间的机制。它用于将相关的类和接口组织在一起&#xff0c;以便更好地管理和维护代码。 其实就是一个个文件夹 二、包(package)的作用 防止类和接口命名冲突&…

时间序列分解 | Matlab变分模态分解(VMD)的信号分解

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 时间序列分解 | Matlab变分模态分解(VMD)的信号分解 部分源码 %--------------------

尚硅谷Docker2022版学习笔记(基础篇 下)

目录 五、本地镜像发布到阿里云 5.1、本地镜像发布到阿里云流程 5.2、镜像的生成方法 基于当前容器创建一个新的镜像&#xff08;第一种方法&#xff09; DockerFile&#xff08;第二种方法&#xff09; 5.3、将本地镜像推送到阿里云 本地镜像素材原型 创建仓库镜像 选…

hive row_number()对单列进行去重

学到一个高级用法 row_number() select * ,row_number() over (partition by c_id order by s_score) from score; https://www.jb51.net/article/254952.htm

【Kafka】第一章

【Kafka】第一章 1.课程内容 1.课程内容 >课程学习

Python———PyCharm下载和安装

&#xff08;一&#xff09;开发环境介绍 开发环境&#xff0c;英文是 IDE &#xff08; Integrated Development Environment 集成开发环境&#xff09;。 不要纠结于使用哪个开发环境。开发环境本质上就是对Python 解释器python.exe 的封装&#xff0c;核心都一样。可以说&…

web前端求职面试题参考精选(合集)

web前端求职面试题之选择题 1. CSS样式表根据所在网页的位置&#xff0c;可分为?(B ) A.行内样式表、内嵌样式表、混合样式表 B.行内样式表、内嵌样式表、外部样式表 C.外部样式表、内嵌样式表、导入样式表 D.外部样式表、混合样式表、导入样式表 2. 对于标签&#xff0…

IDEA使用技巧之——自动导入包,自动优化未使用的import

1 进入设置 2 进入编辑器 3 选择自动导入&#xff0c;中的方框内容 4 最后点击应用&#xff0c;点击确定即可了。 END

使用docker搭建frp在云服务器上调用本地服务器的代码

前言:最近生成图片的ai很火&#xff0c;国内也有不少这样的网站&#xff0c;但是一般都需要钱&#xff0c;这时候自己想用&#xff0c;又不想花钱&#xff0c;就只能在本地服务器搭建一个ai生成图片的服务&#xff0c;再通过内网穿透&#xff0c;从公网访问到这个本地搭建的服务…

AI 绘画 - 建筑绘图辅助设计之 Controlnet SEG Depth

前情提要 2023-06-17 周六 杭州 雨转阴 小记: 周末的午休感觉还是没有尽兴&#xff0c;说是要乖乖休息却只是躺下闭目养神。 任务清单: a. Sketchup 安装和学习; b. Sketchup 建模学习; c. Controlnet SEG 语义分割基础&#xff1b; d. Controlnet Depth; 简介 应用实践 …

理解mysql数据库

1.MySQL 在 Centos 7环境安装 1.1 卸载不要的环境 ps ajx |grep mariadb # 先检查是否有 mariadb 存在 systemctl stop mariadb.service # 停⽌ mariadb 服务 ps ajx |grep mariadb # 再 检查是否有 mariadb 存在 1.2 删除多余的安装包 rpm -qa | grep mysql #查看默认安装…

#systemverilog# 关于关键字 之 unique 随机约束

前言 在随机约束中,我们可以使用关键字 unique 。 使用关键字unique定义的SystemVerilog约束称为唯一约束。在随机化中,使用唯一约束可以生成变量集的唯一值或数组的唯一元素。这里着重解释一下变量集:是同一类型随机变量的集合。 通过unique约束我们可以完成以下任务: …

免费搭建网站(手把手教学) — freehost免费主机体验

相信不少人都想制作一个属于自己的网站&#xff0c;其实并不是很难&#xff0c;即使没有基础也是能够完成的&#xff0c;而这些免费的主机就是小白练手的最佳途径了。免费主机有点就是免费&#xff0c;缺点也是有一堆&#xff0c;如果自己玩玩小范围提供网页服务足够&#xff0…

Virtualbox下CentOS安装k8s

1.系统配置 硬件配置基本要求 资源大小硬盘> 20Gcpu> 2核内存> 2G 本教程配置 主机名IP配置master192.168.10.1553核2G20Gworker1192.168.10.2343核2G20Gworker2192.168.10.1473核2G20G 2.安装必要软件 所有机器都要执行 yum 更新 sudo yum update -y bash-completion…

android复制与粘贴

官网关于复制的教程&#xff1a;https://developer.android.google.cn/guide/topics/text/copy-paste?hlzh-cn 关于Android 13模拟器无法运行可查看这篇文章&#xff1a;https://blog.csdn.net/android_cai_niao/article/details/131422541 界面UI如下&#xff1a; 界面中有…

事务的传播

七种传播机制支持当前事务不支持当前事务嵌套事务 七种传播机制 事务传播机制&#xff1a;解决一个事务在多个方法传递的问题 传播机制有以下7种 REQUIRED (默认)&#xff1a;如果当前存在事务&#xff0c;则加入该事务&#xff0c;如果不存在事务&#xff0c;则创建一个新事务…

DEBUG 系列二:Configure Debugger Layer

还拿之前的例子&#xff1a; https://mp.weixin.qq.com/s/a0Ia2wzFPGWjN988uutg7Q 前台操作dump了 然后debug分析原因的时候发现是这里的问题 进这标准函数看看 里面用到的是全局变量 go_hugm 说明这个函数使用时还有一系列配套的其他函数&#xff0c;都应该在它所对应的函数组…

[RSIC-V]Milk-V开发板 i2c测试oled及波形输出

I2C3 引脚图 修改i2c3复用功能 build\boards\cv180x\cv1800b_sophpi_duo_sd\u-boot\cvi_board_init.c //I2C3 pin6 7 PINMUX_CONFIG(SD1_CMD, IIC3_SCL); PINMUX_CONFIG(SD1_CLK, IIC3_SDA);扫描I2C3上的设备 [rootcvitek]~# i2cdetect -y -r 3 0 1 2 3 4 5 6 7 8 9 a b c …

wpf 3d立方体上的按钮

新建一个WPF工程&#xff1b; 引入3D Tools&#xff1b;前面已经说了&#xff0c;引入之后&#xff0c;不用写代码即可实现3D物体的鼠标操作&#xff1b; xaml代码如下&#xff1b; <Window x:Class"thrbtn.MainWindow"xmlns"http://schemas.microsoft.com…