☰
地图总闭不上环、位姿莫名跳变?我把 Cartographer 源码拆开,跟着一帧激光 走了一遍
2026/10/24 4:06:46 网站建设 项目流程

摘要:从扫描匹配、概率栅格、分支定界到全局优化,跟着一帧激光把 Cartographer 内部算法串成一条线。


写在前面

说实话,Cartographer 我用了有一阵了,但一直是"会用、不懂"的状态:参数照着网上抄,跑起来没问题就谢天谢地,一出问题就抓瞎。最让我头疼的两个毛病——地图闭不上环、位姿莫名其妙跳一下——我压根不知道去哪查。

前阵子狠下心把源码啃了一遍,才发现这东西没那么玄。它没有魔法,拆开看就是一条流水线:一帧激光进来,先算我在哪,再把点记到地图上,跑远了回头认认路,最后把大家的说法统一一下。

这篇就记一下我啃出来的东西。我不堆公式,就跟着一帧激光,从它进门到变成地图,一步一步走一遍。你跟着走下来,再回头看那些参数,就知道每个数字在干嘛了。

(先交代下背景:我这边是 AGV 小车,前端源码被我们改过,跑的是 Kalman 版前端,不是官方原版。但主线逻辑是通的,不影响理解。)


一、先别急着看细节:Cartographer 是"两个工种"在接力

刚打开源码那会儿,我最晕的是文件一大堆,不知道从哪看起。后来才搞明白,得先站高一点:它里面其实是两拨人在干活。

  • 前端:盯着"现在"。每一帧激光来了,它立刻算"我这会儿在哪",然后把激光点记到一张局部的小地图(子图)上。它追求快,活在当下。
  • 后端:管"过去"。它不参与实时,而是隔一段时间把前端攒下的地图和轨迹拿出来,做一次全局的"对账",把累积的漂移消掉。

这两拨人之间有个很重要的规矩,我一开始没意识到,吃了不少亏:

前端把活交给后端,是单向的。后端算完之后,不会回头去改前端正在用的那张小地图。

这个规矩能解释一个我困惑很久的现象:为什么小车在 TF 里的位姿会突然"跳"一下?

因为那不是前端算错了,而是后端对完账,把全局位姿给改了。前端那张小地图一直没动,动的是后端手里的"全局账本"。记住这一点,后面看到跳变就不会瞎猜了。

架构清楚了,接下来就真的跟着一帧激光走。它进门第一件事,是搞清楚自己在哪。


二、跟着激光走第一步:它得先搞清楚"我在哪"

激光一进来,第一件事是定位——这一帧的每个点,得先知道是站在哪个位置打出去的。这一步叫扫描匹配,分三步走,我一个个说。

第一步,先猜个大概(先验外推)。小车有里程计(odom),它根据上一帧的位置和轮子的转动,先估一个"我这会儿大概在哪"。这个估计不准,但能给个起点。

第二步,在小范围里粗搜(粗配)。拿着这个估计值,在周围±5cm、±5°的小窗口里挨个位置试一遍,看哪个位置让激光点和已有地图对得最齐。注意这个窗口很小——它只负责"微调",不负责"救场"。

第三步,精细打磨(ceres 精修)。粗配给了个还不错的初值后,交给一个叫 ceres 的优化库做梯度下降,把位置一点点挪到最优。这一步没有窗口限制,是真正能把位置"拉到位"的主力。

这里有个我踩过的坑,得专门说说。ceres 精修的时候,里面有两股力在较劲:

  • 一股是激光点的拉力(点弹簧):每个激光点都想往"它该落的那面墙"上靠,几百个点一起使劲,力量不小;
  • 另一股是里程计的拉力(平移弹簧锚):它把位置往回拽,不让偏离 odom 的估计太远。

正常情况下这俩配合挺好。但如果 odom 自己滑了(比如轮子打滑了 30cm),麻烦就来了:粗配那个 5cm 的小窗口够不着 30cm 的偏差,帮不上忙;真正在救场的,是那几百个激光点的拉力。而里程计那股拉力还在往回拽(它锚定的就是个错的估计),等于帮倒忙。所以这一帧只能纠回一部分,剩下的误差带到下一帧,一帧一帧慢慢追。

这也是我后来才想通的:"信 odom 信到什么程度"是个要命的选择。极端信它,激光被污染时确实稳;可一旦 odom 自己错了,就没人能把它拉回来了。

位置算出来了,那这些激光点是怎么变成地图的?接着往下走。


三、位置定了,激光点往地图上"记账"

搞清楚"我在哪"之后,这些激光点就要往地图上记了。这一步是概率栅格。

地图其实就是一大堆小方格,每个格子存一个数,意思是"这块地是不是墙"。但这个数不是简单的"是/否",而是一个0.1到0.9之间的概率——0.9 是"我很确定这是墙",0.1 是"我不确定/没见过"。

激光每碰一个格子,就给这个格子"投一票":

  • 激光穿过一个格子(说明这儿是空的),就投一张miss 票,概率往"空"那边降一点;
  • 激光打在一个格子上(说明有东西挡住了),就投一张hit 票,概率往"墙"那边升一点。

听着简单,但有个细节我卡了半天:这些票不是在概率上直接加减的,而是在一个叫"赔率"的对数空间里加减的。

为什么要绕这么一下?因为概率是 0 到 1 的数,直接加减容易越界、还不对称;换成对数之后,加减就干净了。你就记住一句:在 carto 里算这些票的权重,永远先取对数再算,别在概率上直接加。

这里还有个特别容易混的地方,我当时绕了好久——地图里有两种"空",长得像但完全是两码事:

  • 一种是自由空间,值大概0.495:激光穿过过、吃过 miss 票的格子,是"见过的空地";
  • 另一种是未知区,值是0.1:激光压根没碰过的格子,是"没见过的黑域"。

这俩差了快 5 倍。后面回环检测打分的时候,踩到这两种"空"分数完全不一样,所以必须分清。

(插一句:跑完那个pbstream文件,存的就是这张格子地图。格式是 8 字节文件头 + gzip 压缩的记录流,格子是一串打包好的数字。知道这个,哪天你想扒开地图文件看看,就有门了。)

地图一边记,小车一边往前跑。可问题来了——跑得越远,偏得越多。怎么办?


四、跑远了会飘,得靠"回头认路"

里程计和激光都有误差,跑得越远,累积偏差越大,这就是漂移。怎么消?靠回环检测:小车绕一圈回到老地方,得能认出"咦,这地方我来过",然后把这个信息告诉后端去纠正。

但"认出老地方"这事,计算量大得吓人。一帧激光要在一个很大范围里找"哪个位置最贴合",可能的位置有上亿个,一个个试根本不现实。carto 用了两招解决,我分开讲。

第一招,分辨率金字塔。既然原图上找太慢,那就把地图缩小几档,做成一摞"缩略图"——就像找照片,先看缩略图锁定大概位置,再点开原图看细节。具体做法:每往上一层,就把width × width个格子合并成 1 个格子。

这里我又踩了个坑,说出来你别再踩:width是边长,不是格子数。比如第 1 层是2×2=4个格子并 1 个,第 6 层是64×64=4096个格子并 1 个。我一开始总以为第 6 层是 64 个格子,其实是 4096 个——窗口是个方块,不是一排。

而且合并时有个讲究:取这一片格子里的最大值,不是平均值。为什么非得取最大?这是整个算法敢"偷懒"的根基。取最大值能保证一件事:合并出来的"读数"只会高估、不会低估。有了这个保证,才敢放心做下面的剪枝。

第二招,分支定界(B&B)。有了金字塔,搜索就变成:先在粗糙的上层快速框范围,值得细看的才往精细的下层钻。判断"值不值得细看"的逻辑是这样:

如果某个区域的"读数"已经比我当前找到的最好结果还差,那这个区域里不可能藏着更好的(因为读数只会高估),直接整个扔掉,不用看了。

就这一条,砍掉绝大部分不用看的地方,速度就上来了。注意它是精确的——扔掉的都是"数学上证明不可能更好"的,不是瞎猜。

找到候选位置后,还得打分(ComputeCandidateScore):把这一帧激光点按这个候选位置摆到地图上,每个点读出它落进格子的值,然后取平均。这里记住一条铁律,能帮你检查有没有算错:

一帧点云,只出一个分数;而且这个平均分,永远落在 0.1 到 0.9 之间。你要是算出 0.98 这种数,不用想,肯定加错了或忘了除。

打完分,还有个门槛要过,叫min_score。carto 里有两把"刀",架在不同高度:

  • 建图时门槛高,0.65——逼着候选位置必须结结实实踩在墙上,不然不要;
  • 定位时门槛低,0.2——先让小车跑起来再说,别一上来就卡死。

最后说个真实教训,是我们项目里实际遇到的:156° 假解。小车在一条左右对称的长走廊里重定位,结果一个"转了 156°"的错误位置,激光点居然也能和墙严丝合缝——因为走廊是直的、对称的,转个大角度,墙还是那些墙。这个假解分数还不低,有 0.66。

有人第一反应是把门槛提到 0.70 砍掉它。但问题来了:门槛一提,真解也被一起砍了(真解分数可能也没那么高)。所以正确解法不是调门槛,而是缩小搜索范围——把原来±18m / ±180°的大窗口砍成±1m / ±20°,让算法根本没机会去试那么离谱的角度。一句话:这种对称环境的假解,治"范围",不治"阈值"。

认路认出来的修正意见,接下来要交给后端。可后端手里,里程计也有一套说法。两边打架,听谁的?


五、认到的路和里程计打架,听谁的?

回环检测认了路,会产生一个"修正意见"(行话叫回环约束,我喜欢叫它"皮筋"——它想把跑偏的轨迹拉回老地方)。但里程计那边也有一串自己的说法(odom 链)。这俩要是打架,听谁的?这就是全局优化要干的事。

你可以把全局优化想象成一个天平,两边都在使劲:

  • 一边是odom 边:相邻位置之间的约束,一根接一根,数量成千上万,单根权重由参数odometry_weight决定;
  • 另一边是回环皮筋:连接"现在"和"很久以前",跨得远,但就那么几条,单根权重相对小。

优化的底层逻辑就一句话:让所有约束的"总痛苦"最小。每根约束的痛苦 =权重 × 被拉长的程度²。所以天平往哪边倒,看的是权重 × 数量的总分量,谁大听谁的。

这么一算就清楚了:

  • 如果把odometry_weight调到1e5(很大):odom 那边权重又大、数量又多,皮筋彻底没话语权。结果就是漂移没人纠正——小车绕一圈回到起点,本该重合的那面墙,在地图上被画成两截,闭不上环、出现重影。
  • 如果odometry_weight比较小:皮筋能掰动 odom,全局位姿被大改——表现就是小车的位姿突然跳一下。

看到没,开头说的那两个毛病,根子都在这儿:位姿跳变,是皮筋"赢了"的副作用;地图重影,是皮筋"输了"的结局。这两个现象是相反的,千万别记反。

所以1e2还是1e5怎么选?没有标准答案,是个权衡:极端信 odom,抗干扰强、但回环救不了场;信得轻一点,回环能拉正、但容易被激光噪声带偏。得看你现场的传感器哪个更靠谱。


六、串起来:它其实一直在回答同一个问题

到这儿,一帧激光的旅程走完了。回头一看,前面这四个模块,表面是四回事,底下其实一直在回答同一个问题——“这条证据,有多可信?”

  • 扫描匹配里,σ(观测噪声)决定"多信激光",平移弹簧锚决定"多信 odom";
  • 概率栅格里,hit/miss 的汇率决定"多信这是墙";
  • 回环检测里,那个分数决定"这个位置多可信";
  • 全局优化里,odometry_weight决定"多信里程计"。

Cartographer 这台机器,说白了就是一台不停权衡"各种证据可信度"的机器。你调的每一个参数,都是在给某一种证据"加大音量"或"调小音量"。想通这一点,再看那些参数,就都不是玄学了。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询