第259篇 大规模地图管理——子地图、分层地图和地图压缩
SLAM系统跑几分钟建一张小地图没什么问题,但如果让机器人在一个大型工厂里跑一整天,或者让自动驾驶车在城市里跑几个小时,地图的规模就会变得很大。点云数据动辄几个GB,位姿图有几十万个节点,内存和计算量都会成为瓶颈。大规模地图管理是SLAM从"玩具项目"到"工程项目"的关键一步。
地图规模大带来的问题有几个层面。内存方面,所有点云都加载在内存中,很快就会超出机器人的RAM限制。计算方面,后端优化需要对整个位姿图做优化,节点数太多计算时间就扛不住。回环检测方面,当前帧要和所有历史帧比较,历史帧太多比较时间就线性增长。这些问题不解决,SLAM系统只能做"短途旅行"。
子地图(Submap)策略
子地图是处理大规模地图最常用的方法。核心思想是把整个地图分成若干个子地图,每个子地图独立维护,子地图之间通过变换关系连接。
Cartographer是子地图策略的典型代表。它把连续的几十帧点云组成一个子地图,子地图内部做精细匹配,子地图之间做约束优化。每个子地图"完成"后就冻结,不再修改内部结构,只更新子地图之间的相对位姿。
# 子地图的管理策略
class SubmapManager:
def __init__(self, max_frames_per_submap=90):
self.submaps = []
self.current_submap = Submap()
self.max_frames = max_frames_per_submap
def add_frame(self, frame):
self.current_submap.add(frame)
if self.current_submap.num_frames >= self.max_frames:
self.current_submap.freeze()
self.submaps.append(self.current_submap)
self.current_submap = Submap()
子地图的好处很明显。内存方面,只需要把当前子地图和附近的子地图加载在内存中,远处的子地图可以存到磁盘。计算方面,子地图内部的优化范围很小,速度快。回环检测方面,当前帧只需要和每个子地图的代表帧比较,而不是和每一帧比较。
子地图的大小需要调好。太小了(比如10帧一个),子地图数量太多,子地图间的约束太多,后端优化反而变慢。太大了(比如1000帧一个),子地图内部优化慢,内存占用也大。经验值是60-120帧一个子地图,或者覆盖面积20-50平方米一个子地图。
分层地图(Hierarchical Map)
分层地图在子地图的基础上再加一层抽象。底层是原始点云数据,中间层是子地图,顶层是全局拓扑图。不同层次解决不同的问题。
全局导航用顶层就够了——知道"我在哪个子地图,相邻子地图在哪里",不需要加载所有点云。局部避障用底层数据——需要精确的点云来检测障碍物。回环检测在中间层——和子地图级别做匹配,平衡精度和速度。
# 分层地图的结构
class HierarchicalMap:
def __init__(self):
self.topology = Graph() # 顶层:子地图间的拓扑
self.submaps = {} # 中间层:子地图集合
self.raw_data = DiskStore() # 底层:原始数据存磁盘
def query_for_navigation(self, position):
submap_id = self.topology.find_nearest(position)
return self.topology.get_neighbors(submap_id)
def query_for_perception(self, position, radius):
submap_ids = self.topology.find_within(position, radius)
return [self.submaps[sid].get_points() for sid in submap_ids]
分层地图的好处是按需加载。机器人在某个区域工作时,只需要加载附近的几个子地图,远处的子地图不用管。如果机器人移动到新区域,就加载新区域的子地图,卸载远处的子地图。这样内存占用始终控制在一个合理范围内。
地图压缩和稀疏化
点云数据是地图中最大的存储消耗。一帧Velodyne 64线激光扫描有大约12万个点,每秒10帧,跑一个小时就是43亿个点,原始数据约50GB。压缩地图数据是大规模SLAM的刚需。
体素下采样是最简单的压缩方法。把空间分成固定大小的体素格子,每个体素中只保留一个代表点(通常是质心)。分辨率设成0.1米,可以把点数减少一到两个数量级,同时保留大部分几何结构信息。
# 体素下采样压缩地图
def compress_map(point_cloud, voxel_size=0.1):
voxel_grid = VoxelGrid(voxel_size)
voxel_grid.set_input(point_cloud)
compressed = voxel_grid.downsample()
return compressed
更高级的压缩方法会考虑点的"重要性"。边缘点、角点保留,平坦区域的点丢弃。还有方法用八叉树组织点云,自适应地在不同区域使用不同分辨率。
地图的序列化和增量更新
大规模地图不可能全部存在内存中,必须有磁盘存储和增量更新的机制。
序列化格式的选择很重要。PCD是PCL的标准格式,支持二进制模式读写速度快。LAS/LAZ是地理信息领域的格式,LAZ支持压缩文件小。自定义二进制格式灵活性最高。
增量更新指的是机器人一边建图一边保存,而不是等全部建完再保存。每次子地图"冻结"后就写入磁盘。如果系统意外崩溃,已经保存的子地图不会丢失,重启后可以从上次保存的位置继续。
# 增量保存策略
def on_submap_complete(submap):
# 序列化到磁盘
filename = f"submap_{submap.id}.bin"
submap.save(filename)
# 更新索引
index.add_entry(submap.id, submap.bounds, filename)
# 内存中只保留元数据
submap.unload_points()
面试追问环节
面试官:Cartographer的子地图策略具体怎么实现的?
Cartographer把连续的激光扫描帧组成子地图,每个子地图大约90帧。新帧加入当前子地图时,用Ceres做局部优化(扫描匹配)。子地图满了就冻结,变成"完成的子地图"。之后新帧只和完成的子地图做回环检测(用分支定界加速),检测到回环后在全局优化中更新子地图间的相对位姿。这种设计让前端保持实时,后端在后台异步优化。
面试官:子地图之间怎么保证全局一致性?
子地图之间的约束来自两部分:相邻子地图的帧间匹配约束,和回环检测给出的远距离约束。所有子地图的位姿放在一个全局位姿图中优化(Cartographer用Ceres Solver做这个优化)。优化的结果是所有子地图的全局位姿。子地图内部的点云不动,只更新子地图的位姿,内部点云跟着做刚体变换。这样既保证了全局一致性,又不需要重新优化子地图内部。
面试官:八叉树在地图管理中怎么用的?
OctoMap用八叉树存储三维占据概率地图。每个节点表示一个立方体区域,包含被占据的概率。八叉树的好处是自适应分辨率——空旷区域用大节点,细节区域用小节点。同样的场景,八叉树比体素格子节省很多内存,而且支持概率更新。
面试官:地图压缩会丢失多少信息?
体素下采样的信息丢失和分辨率直接相关。0.1米分辨率在室内场景几乎不丢信息(室内物体尺寸通常大于0.1米)。0.5米分辨率会丢失小物体的细节,但大结构还在。2米分辨率只保留大轮廓。实际项目中,压缩率通常在5到50倍之间。对于导航用途,0.1-0.2米的分辨率够了,压缩10-20倍。对于可视化用途,0.5米也够了,压缩50倍以上。
面试官:多机器人建图时地图怎么合并?
多机器人各自建子地图,合并时需要解决坐标系对齐和地图融合两个问题。坐标系对齐可以用共享区域的点云配准找变换关系。地图融合需要处理重叠区域的冲突,用加权平均处理,不确定度小的地图权重更大。Kimera-Multi是做多机器人SLAM的代表工作。
大规模地图管理是SLAM工程化的核心挑战之一。子地图策略把大问题拆成小问题,分层地图按需加载控制内存,地图压缩减少存储开销。这些技术的组合让SLAM系统可以处理城市级别的大规模场景。
从学术角度看,大规模SLAM的研究重点是算法效率和一致性保证。从工程角度看,重点是系统稳定性和资源管理。两者都很重要,面试中被问到的概率也很高。
上一篇:第258篇 动态环境SLAM——处理运动物体的挑战与方案
下一篇我们聊重定位技术,看看机器人在已知地图中如何确定自己的位置。
如果这篇文章对你有帮助,欢迎点赞支持一下,你的鼓励是我持续更新的动力!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)