cartographer代码学习-扫描匹配(暴力搜索)

在学习栅格地图的时候,我们知道在栅格更新前会先进行扫描匹配获取当前机器人最有可能所在的位姿:

  // local map frame <- gravity-aligned frame// 扫描匹配, 进行点云与submap的匹配std::unique_ptr<transform::Rigid2d> pose_estimate_2d =ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);if (pose_estimate_2d == nullptr) {LOG(WARNING) << "Scan matching failed.";return nullptr;}

这个函数同样在LocalTrajectoryBuilder2D里面,它主要执行了两个操作:包里匹配以及基于ceres的LM匹配。这里我们首先看一下暴力匹配部分。

参数配置

在扫描匹配中,是否启用相关性扫描匹配(暴力匹配)根据参数use_online_correlative_scan_matching决定:

  // 根据参数决定是否 使用correlative_scan_matching对先验位姿进行校准if (options_.use_online_correlative_scan_matching()) {const double score = real_time_correlative_scan_matcher_.Match(pose_prediction, filtered_gravity_aligned_point_cloud,*matching_submap->grid(), &initial_ceres_pose);kRealTimeCorrelativeScanMatcherScoreMetric->Observe(score);}

其决定了是否启动该扫描搜索器。同时,如果要使用的话,还需要配置下述一些参数:

real_time_correlative_scan_matcher = {linear_search_window = 0.1,             -- 线性搜索窗口的大小angular_search_window = math.rad(20.),  -- 角度搜索窗口的大小translation_delta_cost_weight = 1e-1,   -- 用于计算各部分score的权重rotation_delta_cost_weight = 1e-1,},

进一步,我们看一下match这个函数的具体实现。

函数实现

1、函数输入与输出

对于相关性扫描匹配函数而言,其输入输出主要包含了以下几个:

/*** @param[in] initial_pose_estimate 预测出来的先验位姿* @param[in] point_cloud 用于匹配的点云 点云的原点位于local坐标系原点* @param[in] grid 用于匹配的栅格地图* @param[out] pose_estimate 校正后的位姿* @return double 匹配得分*/

在调用相关性扫描匹配函数时,前面我们看到它传递了一些参数:

const double score = real_time_correlative_scan_matcher_.Match(pose_prediction, filtered_gravity_aligned_point_cloud,*matching_submap->grid(), &initial_ceres_pose);

上述参数中,第一项pose_prediction为位姿推测器计算出来的预测位姿,作为当前先验的存在;filtered_gravity_aligned_point_cloud是用于扫描匹配的当前点云。注意到这里的点云是位于local坐标系原点的点云。在cartographer中,似乎它的位姿跟随不是跟随的全局坐标系而是一直跟随的local坐标系,所以前期处理点云的时候都是将点云转换到local坐标系原点然后用于后期匹配以及插入;*matching_submap->grid()则是一个指针,我们知道在cartographer中地图都是采用智能指针的形式处理的,所以这里也就是相当于使用了第一张激活的地图进行匹配(cartographer中正常运行时存在两张激活的子图,它们之间差90帧)。而最后面的initial_ceres_pose则是一个根据相关性扫描匹配函数得到的新的位姿。

2、函数具体流程

看完输入输出,接下来展开看看这个函数的具体内容,它主要包含了以下几个部分:

2.1、激光数据的位姿旋转

前面我们知道,输入的点云是位于local坐标系原点的,但是传入的初始姿态一版都不是原点,我们的搜索是基于给定姿态进行前后左右的搜索的,所以需要先进行位姿旋转:

// Step: 1 将点云旋转到预测的方向上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())));

注意到这里进行的是位姿的旋转而不是直接投影到给定坐标系下的。然后我们往下看第二步:

2.2、生成按照不同角度旋转后的点云集合

这一步的操作是根据前面我们初始化时给定次参数生成一系列点云:

// 根据配置参数初始化 SearchParametersconst SearchParameters search_parameters(options_.linear_search_window(), options_.angular_search_window(),rotated_point_cloud, grid.limits().resolution());// Step: 2 生成按照不同角度旋转后的点云集合const std::vector<sensor::PointCloud> rotated_scans =GenerateRotatedScans(rotated_point_cloud, search_parameters);

注意到上面部分的search_parameters里面的就是我们开始时传递的lua参数:

linear_search_window = 0.1,             -- 线性搜索窗口的大小
angular_search_window = math.rad(20.),  -- 角度搜索窗口的大小

这个地方的作用是根据传递给过来的参数生成具体的线性搜索框数量以及角度搜索框数量。其准循以下原则:首先对于旋转而言,传入的角度搜索窗为上下限(-20-20度)然后根据获取的点云的最远距离与栅格分辨率决定每次角度旋转的具体大小。即一次旋转时最远处点云不要平移超过一个分辨率的距离。其次对于平移而言,这个就比较简单了,直接一次平移一个分辨率的距离。
然后下面的GenerateRotatedScans负责按照计算出来的分辨率生成不同角度下的点云。

2.3、获取平移后的点在地图中的索引

到这里我们已经得到了一系列旋转后的点云的集合,既然有旋转自然也要有平移,所以下一步自然要进行平移操作:

  // Step: 3 将旋转后的点云集合按照预测出的平移量进行平移, 获取平移后的点在地图中的索引const std::vector<DiscreteScan2D> discrete_scans = DiscretizeScans(grid.limits(), rotated_scans,Eigen::Translation2f(initial_pose_estimate.translation().x(),initial_pose_estimate.translation().y()));

DiscretizeScans函数本身传参为之前旋转后的所有点云,然后在这里对每一组点云按照之前设置的平移阈值以及分辨率生成一系列平移后的点云,并返回这些点云的索引。

2.4、生成候选解的数量

它的作用是计算有多少个候选解得数量。总数量为:角度步长总数乘以X方向平移步长总数乘以Y方向平移步长总数。然后再生成一系列的候选解,以结构体Candidate2D的形式存在。

  // Step: 4 生成所有的候选解std::vector<Candidate2D> candidates =GenerateExhaustiveSearchCandidates(search_parameters);

注意到这个函数中生成的两个参数x_index_offset与y_index_offset,它们是原始点云在XY方向的平移量,这个后面会用到。

2.5、计算每个候选解的得分

在前面的4个步骤中既然我们已经获取到了所有可能的解,那么下一步自然是将这些可能的解具体分数求出来:

  // Step: 5 计算所有候选解的加权得分ScoreCandidates(grid, discrete_scans, search_parameters, &candidates);

这里是直接调用了一个ScoreCandidates函数进行的计算,我们直接看一下这个函数:

// 计算所有候选解的加权得分
void RealTimeCorrelativeScanMatcher2D::ScoreCandidates(const Grid2D& grid, const std::vector<DiscreteScan2D>& discrete_scans,const SearchParameters& search_parameters,std::vector<Candidate2D>* const candidates) const {for (Candidate2D& candidate : *candidates) {switch (grid.GetGridType()) {case GridType::PROBABILITY_GRID:candidate.score = ComputeCandidateScore(static_cast<const ProbabilityGrid&>(grid),discrete_scans[candidate.scan_index], candidate.x_index_offset,candidate.y_index_offset);break;case GridType::TSDF:candidate.score = ComputeCandidateScore(static_cast<const TSDF2D&>(grid),discrete_scans[candidate.scan_index], candidate.x_index_offset,candidate.y_index_offset);break;}// 对得分进行加权candidate.score *=std::exp(-common::Pow2(std::hypot(candidate.x, candidate.y) *options_.translation_delta_cost_weight() +std::abs(candidate.orientation) *options_.rotation_delta_cost_weight()));}
}

这个函数本身分为两个部分,第一部分根据地图选项调用ComputeCandidateScore函数计算每一个候选项的得分,第二部分根据初始化时给定的圈中参数对结果分数进行加权。
首先看一下第一部分:ComputeCandidateScore函数本身也很简单:

// 计算点云在指定像素坐标位置下与ProbabilityGrid地图匹配的得分
float ComputeCandidateScore(const ProbabilityGrid& probability_grid,const DiscreteScan2D& discrete_scan,int x_index_offset, int y_index_offset) {float candidate_score = 0.f;for (const Eigen::Array2i& xy_index : discrete_scan) {// 对每个点都加上像素坐标的offset, 相当于对点云进行平移const Eigen::Array2i proposed_xy_index(xy_index.x() + x_index_offset,xy_index.y() + y_index_offset);// 获取占用的概率const float probability =probability_grid.GetProbability(proposed_xy_index);// 以概率为得分candidate_score += probability;}// 计算平均得分candidate_score /= static_cast<float>(discrete_scan.size());CHECK_GT(candidate_score, 0.f);return candidate_score;
}

首先我们看到这个函数的传参包含了四个:probability_grid为一张用于匹配的概率栅格地图;discrete_scan为一帧候选点云。这两个比较明确,但是后续两个x_index_offset与y_index_offset不是很好理解。但是我们看调用函数的传参可以比较清晰的知道,它来自于结构体Candidate2D,而这个东西是在2.4中生成的,所以回看2.4我们知道它代表的是原始点云在XY方向需要的平移量,而实际函数中的使用也体现了这一点:

// 对每个点都加上像素坐标的offset, 相当于对点云进行平移const Eigen::Array2i proposed_xy_index(xy_index.x() + x_index_offset,xy_index.y() + y_index_offset);

根据平移量,我们可以获取到该点平移后的栅格,因此也就可以获得它的概率值。然后,我们将每个点的概率值全部加起来:

// 获取占用的概率const float probability =probability_grid.GetProbability(proposed_xy_index);// 以概率为得分candidate_score += probability;

没错,就是直接加起来,不用任何优化的思想就加起来就好了。这就是暴力搜索的暴力所在!那么它的依据在哪里呢?想像两种极端:所有点都与障碍物完全匹配以及一个点都没匹配到障碍物。我们知道,在概率栅格中,1代表完全占用0代表完全空闲。因此对于前面一种,其暴力匹配后的总分就会是点云数量,而对于第二者,约等于0分。因此,暴力匹配的分数越高,也就说明该位置点云与栅格中障碍物所在位置越接近。这也就是暴力匹配的原理。当然这里为了防止点云数量的变化导致有的地方虽然单点概率低但是总分高的情况还做了下均值,但是不影响原理。
然后是第二部分:

candidate.score *=std::exp(-common::Pow2(std::hypot(candidate.x, candidate.y) *options_.translation_delta_cost_weight() +std::abs(candidate.orientation) *options_.rotation_delta_cost_weight()));

该函数对每个候选项的得分乘以了一个系数,这个系数为它的XY偏移量乘以权重与角度偏移量乘以旋转权重两者和的倒数。也就是说,这个数据离初始姿态的XY平移量越大或者角度分量越大,它的系数也就会越小,所以说在算法中对于位姿推测器的结果的置信度还是很高的。

2.6、获取最高得分

这个函数只用于获取所有解中得分最大值的解:

// Step: 6 获取最优解const Candidate2D& best_candidate =*std::max_element(candidates.begin(), candidates.end());

这里的获取方式主要依靠max_element,可以单独学习一下这个函数的用法。

2.7、更新最优位姿

既然已经得到了分数最高的解,下一步自然是将其作为最优先验位姿,以便后续程序的使用了:

  // Step: 7 将计算出的偏差量加上原始位姿获得校正后的位姿*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 best_candidate.score;
总结

暴力搜索在cartographer中主要用于根据给定的预测位姿在一定范围内寻找最有可能为最优解的大体位置,但是由于它不涉及到优化内容,所以得到的结果是有一定误差的(一个分辨率栅格内),所以根据这个结果还需要调用ceres匹配方式进一步获取最优解。同时,由于它需要不断进行栅格的遍历与计算,所以它的计算量也是想对较高的,如果搜索框给的比较大但是栅格分辨率又给的比较小的话就会需要进行非常久的搜索才能得出结果。

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

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

相关文章

[4]python+selenium - UI自动框架之封装基类BasePage页面

这部分内容是页面上的一些基本操作 from selenium.common.exceptions import TimeoutException, NoSuchElementException, WebDriverException, \StaleElementReferenceException from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support im…

某安全公司DDoS攻击防御2024年6月报告

引言&#xff1a; 在2024年6月&#xff0c;网络空间的安全挑战汹涌澎湃。分布式拒绝服务&#xff08;DDoS&#xff09;攻击频发&#xff0c;针对云服务、金融科技及在线教育平台的精密打击凸显出当前网络威胁环境的严峻性。 某安全公司作为网络安全防护的中坚力量&#xff0c…

mybatis之动态标签

有些时候&#xff0c;sql语句where条件中&#xff0c;需要一些安全判断&#xff0c;例如按性别检索&#xff0c;如果传入的参数是空的&#xff0c;此时查询出的结果很可能是空的&#xff0c;也许我们需要参数为空时&#xff0c;是查出全部的信息。这是我们可以使用动态sql&…

代码随想录算法训练营Day55|42.接雨水、84.柱状图中最大的矩形

接雨水 42. 接雨水 - 力扣&#xff08;LeetCode&#xff09; 暴力解法 对计算接到的雨水&#xff0c;有两种方式&#xff0c;一是按照行来计算。 另一种是按列计算 按列计算容易不乱。基本思路如下&#xff1a; 对每列i进行循环&#xff0c;在循环中&#xff0c;找到该列左…

HarmonyOS Next开发学习手册——视频播放 (Video)

Video组件用于播放视频文件并控制其播放状态&#xff0c;常用于为短视频和应用内部视频的列表页面。当视频完整出现时会自动播放&#xff0c;用户点击视频区域则会暂停播放&#xff0c;同时显示播放进度条&#xff0c;通过拖动播放进度条指定视频播放到具体位置。具体用法请参考…

【JVM-02】垃圾收集(回收)算法

【JVM-02】垃圾收集/回收算法 1. 分代收集算法2. 标记-清除算法3. 标记-复制算法4. 标记-整理算法 1. 分代收集算法 分代收集(回收)算法根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代&#xff0c;这样我们就可以根据各个年代的特点选择合适的垃圾收集…

Kotlin中object关键字的作用

1、对象声明&#xff0c;通过这种方式创建一个单例对象。 object MySingleton{ fun function{ //方法代码块 } } 调用方式&#xff1a;MySingleton.function(),类似像Java的静态方法 2、在类内部声明伴生对象 class OutClass { companion object{ val value 1 fun method(…

【算法题】爬楼梯 (经典递归)

题 爬楼梯&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 输入&#xff1a; n 2 输出&#xff1a; 2 解释&#xff1a; 有两种方法可以爬到楼顶。 1 阶 1 阶 2 阶 示例 2&#x…

寒武纪实现高维向量的softmax进阶优化和库函数对比

关于寒武纪编程可以参考本人之前的文章添加链接描述,添加链接描述,添加链接描述 实验证明,axis=0和axis=-1的时候,手写softmax速度可以和库函数媲美,甚至于更甚一筹。 src/softmax.mlu #include <bang.h> #include

Nik Collection by DxO:摄影师的创意利器与调色宝典

在数码摄影的世界里&#xff0c;后期处理是摄影师们展现创意、调整细节、提升作品质量的重要步骤。而Nik Collection by DxO作为一款由DxO公司开发的强大照片编辑插件套件&#xff0c;为摄影师们提供了一套全面的、功能丰富的工具集&#xff0c;让他们的创意得以充分发挥。 Ni…

遇到多语言跨境电商系统源码问题?这里有解决方案!

从手机到电脑&#xff0c;从线下到线上&#xff0c;如今&#xff0c;跨境电商正在打破地域界限&#xff0c;成为全球贸易的新引擎。在这个全球化的背景下&#xff0c;跨境电商平台的运营也面临着一系列的挑战&#xff0c;其中之一就是多语言问题。如果你遇到了多语言跨境电商系…

2065. 最大化一张图中的路径价值 Hard

给你一张 无向 图&#xff0c;图中有 n 个节点&#xff0c;节点编号从 0 到 n - 1 &#xff08;都包括&#xff09;。同时给你一个下标从 0 开始的整数数组 values &#xff0c;其中 values[i] 是第 i 个节点的 价值 。同时给你一个下标从 0 开始的二维整数数组 edges &#xf…

小抄 20240629

1 很多人当下焦虑的原因&#xff0c;是短期内无法实现别人长期才能做到的目标&#xff0c;总想几天就能追赶别人几年的努力&#xff0c;只看到了别人的结果&#xff0c;没有思考过别人的过程。 2 把时间线拉长看&#xff0c;人和人都只是阶段性在一起&#xff0c;只要人还在成…

vivado FIFO IP核 中的rd_valid信号

在Vivado中使用FIFO IP核时&#xff0c;valid信号&#xff08;通常称为rd_valid或dout_valid&#xff09;是一个重要的控制信号。让我详细解释它的作用和使用场景。 valid信号的作用&#xff1a; 功能&#xff1a; valid信号是一个输出信号&#xff0c;用于指示FIFO输出数据的…

7基于SpringBoot的SSMP整合案例-表现层开发

目录 1.基于Restfu1进行表现层接口开发 1.1创建功能类 1.2基于Restful制作表现层接口 2.接收参数 2使用Apifox测试表现层接口功能 保存接口&#xff1a; 分页接口&#xff1a; 3.表现层一致性处理 3.1先创建一个工具类&#xff0c;用作后端返回格式统一类&#xff1a;…

springboot校园购物网站APP-计算机毕业设计源码041037

摘 要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存…

笔记-Python文件: .py、.ipynb、.pyi、.pyc、​.pyd

.py 最常见的Python代码文件后缀名&#xff0c;官方称Python源代码文件。 不用过多解释了~ .ipynb 这个还是比较常见的&#xff0c;.ipynb是Jupyter Notebook文件的扩展名&#xff0c;它代表"IPython Notebook"。 学过数据分析&#xff0c;机器学习&#xff0c;深度…

内卷情况下,工程师也应该了解的项目管理

简介&#xff1a;大家好&#xff0c;我是程序员枫哥&#xff0c;&#x1f31f;一线互联网的IT民工、&#x1f4dd;资深面试官、&#x1f339;Java跳槽网创始人。拥有多年一线研发经验&#xff0c;曾就职过科大讯飞、美团网、平安等公司。在上海有自己小伙伴组建的副业团队&…

算法笔记:模拟过程(螺旋遍历矩阵)

1 模拟过程 “模拟过程题”通常指的是那些要求编程者通过编写代码来“模拟”或重现某个过程、系统或规则的题目。这类题目往往不涉及复杂的数据结构或高级算法&#xff0c;而是侧重于对给定规则的精确执行和逻辑的清晰表达。 其中螺旋遍历矩阵的题目就是一类典型的模拟过程题…

如何在开发过程中减少 Bug?

爱因斯坦曾说过&#xff1a;“如果我有一个小时来解决一个关系到我生死的问题&#xff0c;我会花55分钟弄清楚问题是什么。一旦我知道了问题是什么&#xff0c;我可以在五分钟内解决它。” 虽然我们的软件开发过程并不涉及生死抉择&#xff0c;但它直接影响用户体验&#xff0c…