Cartographer学习——地图概率更新过程
前言:
最近一直在研究建图,对google的开源SLAM框架 Cartographer 进行了源码梳理,发现很多巧妙的算法设计,结合原论文 《Real-time Loop Closure in 2D LIDAR SLAM》 的理论简介,才恍然理解其核心目的就是实现实时回环;为了达到实时的效果,其引入了一些其他方法来进行计算加速。理解其中的思想方法,整个过程有点漫长但很有意思。Cartographer 中工程设计中可参考的点很多,本篇先把其概率地图更新部分的流程进行说明,用图示意的方式对其思路进行展示,强化自我理解。
地图更新的目的: 根据最新采集到的雷达点,在与地图匹配后,把雷达点插入到当前地图中,其本质是对栅格地图概率的更新。
地图更新的方式:
- 逐一对地图中栅格进行概率更新,一般用对数 Odd(x) 进行相乘或者 )Log(Odd(x)) 相加减,因此当地图较大时,大量的乘法计算会影响地图更新的效率;所以为了提升计算效率,其他大部分SLAM实现中地图更新采用Log(Odd(x)) 相加减的方式。
- Cartographer针对地图更新过程,采用以空间换时间的策略,主要是建立地图更新对照表,然后更新过程就直接变成了查表操作,连加减法都省去了,牺牲内存空间,节约时间。
其具体操作流程如下,地图要更新的点主要是激光所扫到的范围:

如上图所示,当第1个栅格中的值是700时,通过查表可立即获得更新后的值892,然后直接填入第1个栅格中,即可完成栅格概率更新。那本质上说,更新表是存放的有序更新后的值,而表的序号就是更新前的值,因此该问题的重点是如何构建这张表。
这里也许注意,Cartographer子图中存放的既不是概率也不是赔率odd,而是概率的映射[0~65536]。
注: 上图的值只做流程展示参考,不与真实计算挂钩。
在代码中构建这两张表的入口在代码在类LaserFanInserter的构造函数中:
// laser_fan_inserter.cc
LaserFanInserter::LaserFanInserter(
const proto::LaserFanInserterOptions& options)
: options_(options),
hit_table_(mapping::ComputeLookupTableToApplyOdds(
mapping::Odds(options.hit_probability()))),
miss_table_(mapping::ComputeLookupTableToApplyOdds(
mapping::Odds(options.miss_probability()))) {}
其调用probability_values.cc中ComputeLookupTableToApplyOdds函数,可以看到,这里分别构造了两张表hit_table_和miss_table_,这就是前面提到的地图概率更新表。地图中点的更新状态有三种:hit、miss、free(不更新),options.hit_probability()和options.miss_probability()为激光雷达点击中(hit)和穿过(miss)的概率,是Lua配置数据输入:
options.hit_probability() = 0.55;
options.miss_probability() = 0.49;
我们可以看到在probability_values.h和probability_values.cc中实现了整个表的构建:
// probability_values.h
// 概率值转Odds
inline float Odds(float probability) {
return probability / (1.f - probability);
}
// Odds转概率值
inline float ProbabilityFromOdds(const float odds) {
return odds / (odds + 1.f);
} // 概率值的范围为 [0.1, 0.9]
constexpr float kMinProbability = 0.1f;
constexpr float kMaxProbability = 1.f - kMinProbability; // Clamps probability to be in the range [kMinProbability, kMaxProbability].
inline float ClampProbability(const float probability) {
return common::Clamp(probability, kMinProbability, kMaxProbability);
} constexpr uint16 kUnknownProbabilityValue = 0;
constexpr uint16 kUpdateMarker = 1u << 15; // = 32768 // Converts a probability to a uint16 in the [1, 32767] range.
inline uint16 ProbabilityToValue(const float probability) {
const int value =
common::RoundToInt((ClampProbability(probability) - kMinProbability) *
(32766.f / (kMaxProbability - kMinProbability))) +
1;
// DCHECK for performance.
DCHECK_GE(value, 1);
DCHECK_LE(value, 32767);
return value;
} extern const std::vector<float>* const kValueToProbability; // Converts a uint16 (which may or may not have the update marker set) to a
// probability in the range [kMinProbability, kMaxProbability].
inline float ValueToProbability(const uint16 value) {
return (*kValueToProbability)[value];
}
头文件中主要实现了基本概率、赔率、对应整数的转换函数,概率的范围为[0.1, 0.9],对应value的范围为[1, 32767],若value为0表示地图为unknow状态,也就是初始灰色状态。kValueToProbability为从Value对应的概率值的表,为计算地图更新hit和miss表做准备。
这里kUpdateMarker 为已更新后的标志量,是为了防止miss和hit重复对地图更新,其值为32768,为uint16 的最高位,其操作的方式是,更新时加上,更新完了后减去。
// probability_values.cc
// 0 is unknown, [1, 32767] maps to [kMinProbability, kMaxProbability].
float SlowValueToProbability(const uint16 value) {
CHECK_GE(value, 0);
CHECK_LE(value, 32767);
if (value == kUnknownProbabilityValue) {
// Unknown cells have kMinProbability.
return kMinProbability;
}
const float kScale = (kMaxProbability - kMinProbability) / 32766.f;
return value * kScale + (kMinProbability - kScale);
} // 直接巧妙计算Value转概率对照表,重复2次,区别了带marker和不带marker
const std::vector<float>* PrecomputeValueToProbability() {
std::vector<float>* result = new std::vector<float>;
// Repeat two times, so that both values with and without the update marker
// can be converted to a probability.
for (int repeat = 0; repeat != 2; ++repeat) {
for (int value = 0; value != 32768; ++value) {
result->push_back(SlowValueToProbability(value));
}
}
return result;
} } // namespace const std::vector<float>* const kValueToProbability = PrecomputeValueToProbability();
在计算ComputeLookupTableToApplyOdds更新表之前,这里先利用PrecomputeValueToProbability计算了一个kValueToProbability 表,也是为了加速计算更新表:
注意:
- 这里直接巧妙计算Value转概率对照表,重复2次,区别了带marker和不带marker,但都可以同时获得相同的概率值。
std::vector<uint16> ComputeLookupTableToApplyOdds(const float odds) {
std::vector<uint16> result;
// 计算Value=0时更新后的Value,可直接更新,因为更新前=0为Unknown
result.push_back(ProbabilityToValue(ProbabilityFromOdds(odds)) + kUpdateMarker);
// 从第2个开始,到32767
for (int cell = 1; cell != 32768; ++cell) {
result.push_back(ProbabilityToValue(ProbabilityFromOdds(
odds * Odds((*kValueToProbability)[cell]))) +
kUpdateMarker);
}
return result;
}


注意:
- 这里的更新表第一个数是直接存入的,直接用传入的赔率值计算,这其实是在计算未知区域的更新Value,也是地图中当前值为0的区域;
- 更新后的Value都加上了kUpdateMarker标志,该标志将在下次地图更新之前被清除。清除操作在probability_grid.h文件中的StartUpdate()函数中。
到此,我们已经成功建立了更新查询的hit和miss表,如下开始介绍其地图更新的核心操作,如下laser_fan_inserter.cc文件中:
// laser_fan_inserter.cc
void LaserFanInserter::Insert(const sensor::LaserFan& laser_fan,
ProbabilityGrid* const probability_grid) const {
// 去除上一次更新后留下的kUpdateMarker标志
CHECK_NOTNULL(probability_grid)->StartUpdate(); // By not starting a new update after hits are inserted, we give hits priority
// (i.e. no hits will be ignored because of a miss in the same cell).
// 利用激光的CastRays模型获得his和miss栅格在grid_map上的索引表,然后根据前面构建的更新表进行更新
// hit和miss一次调用ApplyLookupTable()进行更新
CastRays(laser_fan, probability_grid->limits(),
[this, &probability_grid](const Eigen::Array2i& hit) {
probability_grid->ApplyLookupTable(hit, hit_table_);
},
[this, &probability_grid](const Eigen::Array2i& miss) {
// 可根据外部参数关闭miss区域更新
if (options_.insert_free_space()) {
probability_grid->ApplyLookupTable(miss, miss_table_);
}
});
}
注意:
- 这里的CastRays()传入的是一个函数指针;
- 这块后面可以单独拎出来讲解。
// probability_grid.h
bool ApplyLookupTable(const Eigen::Array2i& xy_index,
const std::vector<uint16>& table) {
DCHECK_EQ(table.size(), mapping::kUpdateMarker);
const int cell_index = GetIndexOfCell(xy_index);
uint16& cell = cells_[cell_index];
if (cell >= mapping::kUpdateMarker) {
return false;
}
update_indices_.push_back(cell_index);
cell = table[cell]; // 真正的更新操作
DCHECK_GE(cell, mapping::kUpdateMarker);
UpdateBounds(xy_index);
return true;
}
到这里,地图概率更新的过程已经梳理完成,这其中还有很多具体的细分点,比如:MapLimit的计算、CastRays模型的介绍、边界的更新,等等。
参考:【图解 cartographer】 之地图概率更新过程_栅格地图更新-CSDN博客
Cartographer学习——地图概率更新过程的更多相关文章
- 谷歌Cartographer学习(1)-快速安装测试(转载)
转载自谷歌Cartographer学习(1)-快速安装测试 代码放到个人github上,https://github.com/hitcm/ 如下,需要安装3个软件包,ceres solver.cart ...
- 学习《概率机器人》中英文PDF+Probabilistic Robotics
研究机器人时,使机器人能够应对环境.传感器.执行机构.内部模型.近似算法等所带来的不确定性是必须面对的问题. <概率机器人>对概率机器人学这一新兴领域进行了全面的介绍.概率机器人学依赖统计 ...
- cartographer保存地图
手持激光,并用cartographer建图,保存的地图是.pbstream格式 ht@ht:~$ rosservice call /write_state /home/ht/Desktop/carto ...
- 谷歌Cartographer学习(1)-快速安装测试
谷歌自己提供了安装方法,但是安装比较繁琐,我做了一定的修改,代码放到个人github上,https://github.com/hitcm/. ros下面的安装非常快捷,只需要catkin_make即可 ...
- [学习笔记]概率&期望
概率的性质 非负性:对于每一个事件$A,0\;\leq\;P(A)\;\leq\;1$. 规范性:对于必然事件$S,P(S)=1$;对于不可能事件$A,P(A)=0$. 容斥性:对于任意两个事件$A, ...
- 谷歌Cartographer学习(2)-原理阐述与源码解析
最近终于写完了毕业论文.想仔细研究下Cartographer.无奈自己学识有限,先看下网上大牛的解析,作一个汇总. 一.泡泡机器人原创专栏-cartographer理论及实践浅析 http://mp. ...
- 谷歌Cartographer学习 -快速安装测试
参考资料:https://www.cnblogs.com/hitcm/p/5939507.html PC下面进行安装: 遇到的问题如下 1.首先安装ceres solver 在编译的时候,如果是低配的 ...
- slam cartographer 学习
https://github.com/slam4code 感谢大牛的分享
- zz“深度高斯模型”可能为深度学习的可解释性提供概率形式的理论指导
[NIPS2017]“深度高斯模型”可能为深度学习的可解释性提供概率形式的理论指导?亚马逊机器学习专家最新报告 专知 [导读]在NIPS 2017上,亚马逊机器学习专家Neil Lawrence在12 ...
- 学习移动机器人SLAM、路径规划必看的几本书
作者:小白学移动机器人链接:https://zhuanlan.zhihu.com/p/168027225来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 声明:推荐正版图 ...
随机推荐
- 【转载】 tensorflow变量默认是如何进行初始化的?
版权声明:本文为CSDN博主「TahoeWang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/sinat_3 ...
- python多进程multiprocessing模块的变量传递问题:父进程中的numpy.array对象隐式序列化到子进程后的inplace操作的问题
参考: https://docs.python.org/zh-cn/3/library/multiprocessing.html cloudpickle -- Python分布式序列化的专用模块 ...
- C语言编写Linux终端环境下无缓冲键盘输入 ,并识别上下左右光标键
自己重构了游戏<2048>并且重构了它的最好启发式AI解法,并上传到了Gitee中的 鬼&泣 / 2048-heuristic的devilmaycry分支,在这个过程中编写了一个C ...
- C# 命名空间和程序集 小记
前言 本文只是为了复习,巩固,和方便查阅,一些知识点的详细知识会通过相关链接和文献标记出来. 命名空间 1.1 概念 可以把命名空间看做字符串,他加在类名或类型名前面并且通过点进行分割 既然看做是字符 ...
- Windows使用命令行终止任务
在Windows操作系统中,可以使用命令提示符(cmd)或Windows PowerShell来查看运行的任务并终止指定的任务.以下是一些常用的命令: 使用命令提示符(cmd) 查看运行的任务: 打开 ...
- 数据结构 分块 & 莫队
分块 一种优化暴力的思想. 通常是将原数据划分成适当块(一般为 \(\sqrt{n}\)),对每块数据进行预处理,进而达到比暴力更优的时间复杂度. 划分 确定块长后,一般需要开两个数组存储每一块的右边 ...
- logback日志级别动态切换的四种方案
荐
生产环境中经常有需要动态修改日志级别. 现在就介绍几种方案 方案一:开启logback的自动扫描更新 配置如下 <?xml version="1.0" encoding=&q ...
- CentOS7 压缩及打包的常用命令
gzip gzip 文件名 压缩文件 gzip -d 文件名 解压文件 gunzip 文件名 解压文件 gzip -1 #压缩级别 最高到9 默认是6级别 gzip -f # 强制覆盖同名压缩包 gz ...
- WinForm DevExpress 添加行内按钮
1.在设计器里面添加一列,设置单元格不可编辑.只读属性 2. 在所在GridView属性里面添加CustomDrawCell事件与RowCellClick事件 private void gvMain_ ...
- Mono 现状与未来:从Wine-mono 到.NET 9
Mono 官网主页[1]和 Mono GitHub 页面今日发布公告[2],微软宣布将 Mono 项目移交给 WineHQ 组织,也就是 Linux 兼容 Windows 应用框架 Wine 的开发团 ...