ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

激光SLAM核心机制Hyperframes:多帧点云融合原理与工程实践

激光SLAM核心机制Hyperframes:多帧点云融合原理与工程实践 最近在折腾激光雷达 SLAM尤其是翻 hdl_graph_slam、LIO-SAM 或者 Fast-LIO 这类开源项目源码的时候你大概率会在论文、issue 和代码注释里反复撞见一个词hyperframes。我第一次看到它时心里想的是这不就是把几帧点云摞在一起吗至于单独造一个名字出来直到后来自己在隧道、地下车库和高架桥底这类场景里把建图跑崩才明白 hyperframes 这套设计到底在替我们解决什么麻烦。简单说hyperframes 就是把一段时间窗口内的多帧激光点云按照相对位姿关系叠成一个“超帧”再拿这个超帧去做配准、建图或优化节点。这个机制看着不起眼但它直接关系到系统在稀疏、退化、动态遮挡环境下的稳定性和精度也是很多新入坑的同学最容易忽略的一环。这篇文章我不会只堆概念而是想把 hyperframes 从术语来源、在前后端中的作用、到自己动手构建一份可跑的流程再到实际踩过的坑完整拆一遍。你不需要有非常深的 SLAM 基础只要知道点云、位姿、ICP/NDT 这几个词大概是什么就能跟着理解清楚。1. hyperframes 到底是什么从一个被忽略的数据结构说起1.1 从 keyframe 到 hyperframe术语背后的思路转变在激光 SLAM 里大家最熟悉的概念是 keyframe关键帧。传统图优化 SLAM 的做法是从连续帧里挑出一些不冗余、信息量大的帧作为关键帧把关键帧之间的相对位姿作为约束丢进位姿图里优化。关键帧的核心逻辑是“抽样”目的是减少计算量同时保留足够的几何信息。submap子图是另一种常见数据结构它把一段轨迹上所有帧拼成一个局部地图通常用于扫描匹配的前端或者重定位。submap 关心空间覆盖时间概念比较弱。hyperframes 更像是一种介于 keyframe 和 submap 之间的组织方式。它按时间窗口或空间距离取出一组点云帧先通过传感器内外参和已有的位姿估计把这几帧投影到同一个坐标系下再合并成一个密度更高、覆盖范围更广的点云集合。这个集合就叫 hyperframe。每个 hyperframe 在后续流程里可以当作一个大号的“虚拟关键帧”来用既保留了多帧信息又没有 submap 那么大的体积。三者之间的区别我用一张表格总结数据结构定义时间跨度主要用途keyframe从原始帧中抽取的单帧点云单帧时刻位姿图节点、局部配准参考submap一段轨迹内所有帧拼接的局部地图数秒到数十秒扫描匹配、定位、回环候选hyperframe按滑窗选取的多帧点云融合结果几十毫秒到几秒前端配准、子图构建、图优化节点需要强调的是hyperframe 不只是简单的“点云拼接”。拼接只解决了空间叠加问题在这里我们还要处理点的置信度、动态物体过滤、边界权值衰减甚至特征强度的重新计算。所以 hyperframe 是一套数据结构加处理流程的组合而不是一个单一的容器。1.2 为什么单帧点云不够用要理解 hyperframe 存在的必要得先直面单帧点云的几个痛点。第一是稀疏性。机械式激光雷达一帧能出几十万点听起来很多但平均到空间里依然稀疏。特别是 16 线或者 32 线的雷达放到开阔区域远处一个行人可能只有几个点墙面的点间距能拉到几十厘米。这种密度下做配准、做平面拟合、做特征提取误差会被敏感放大。第二是畸变。旋转式雷达扫描一圈需要时间例如 10Hz 的机械雷达单帧扫描耗时 100ms。如果机器人在运动这一帧里的点并不是同一时刻采集的直接拿原始点云去做匹配相当于带着运动模糊干活。低速还好高速或剧烈颠簸时点云会拉出“拖影”精度掉得很快。第三是退化场景。走廊、隧道、长直墙这类环境沿某个方向缺少足够几何约束。单帧点云往往只在局部几个方向上提供充分信息发生退化的概率很高。把多帧拼在一起后不同视角和不同位置的观测叠加在一起能让之前“看不到”的方向重新获得约束。第四是噪声和动态遮挡。单帧中的噪声点、飞点、动态物体的点云往往没有足够统计样本去识别。多帧聚合之后同一空间的多次观测可以互相验证离群点和瞬态障碍物更容易被剔除。这些痛点叠加起来你会发现在真实环境中单帧点云“又稀又脏还带畸变”。hyperframes 本质上是一种以计算换质量的思路通过多帧信息互补把单帧的限制给摊薄。2. hyperframes 在 SLAM 里的三种角色前端、中端、后端2.1 前端配准把超帧当“大号关键帧”来用前端扫描匹配最常用的方法是 ICP、NDT 以及基于特征的点云配准。这些算法对点云的质量非常敏感。单帧点云如果太稀疏NDT 的体素格子里面没有足够多的点来估计均值和协方差配准结果就会左右飘。当点云数量增加hyperframe 的价值就体现出来了。我们可以维护一个最近一小段时间窗口比如 0.5 秒内的多帧点云把它们按预测位姿融合成一个 hyperframe。当前帧到达时用这个 hyperframe 来代替单一关键帧作为 target当前帧作为 source之后跑 ICP/NDT。因为 target 的点云密度显著提高平面结构更完整配准结果更稳定。有一点需要提醒这种前端用法要求输入 hyperframe 的位姿估计不能太差。如果你在加速度很大的情况下直接把多帧硬拼等于把误差也叠进了 target配准反而会崩。所以前端用 hyperframe 时通常需要先做一轮粗略的运动补偿或惯性积分保证融合前各帧之间基本对齐。2.2 子图与滑窗hyperframe 作为局部地图管理器除了配准之外hyperframe 另一个高频用途是构建局部地图。具体做法是维护一个滑窗每来一帧点云就进窗口窗口长度可以是固定帧数也可以是固定时间或固定移动距离。当窗口内的帧满足一定条件就融合生成一个新的 hyperframe窗口向前滑动旧的帧被淘汰。这样的好处是局部地图不存在“无限膨胀”的问题。每一次新建 hyperframe 时只在一个有界的时空窗口内取数计算量是可控的。同时hyperframe 之间还可以设计一定的重叠度保证相邻子图之间有足够的公共区域便于后续做约束闭环或者子图配准。在建图系统里hyperframe 可以直接作为“子图单元”。比如每隔 2 米生成一个 hyperframe后续把多个 hyperframe 再通过全局位姿拼成大图。这种分层结构在工程中很常见原始帧 - hyperframe - 全局地图。每一层都在降低数据规模同时保留关键信息。2.3 因子图后端hyperframe 作为优化节点如果你关注过 LIO-SAM 或者 hdl_graph_slam 这类系统会发现它们在后端图中用的节点并不全是原始帧。很多系统会选 keyframe 作为因子图的节点而 hyperframe 的概念则经常被弱化成“该 keyframe 附带的一堆点云”。但在更完整的实现里hyperframe 可以作为因子图中的一个节点身份出现。每个 hyperframe 对应一个局部点云集合维护一个六自由度位姿。相邻 hyperframe 之间的相对位姿由配准得到作为两帧之间的因子约束。全局优化时调整的是这些 hyperframe 节点的位姿而不是每一帧的位姿。这么做的好处很明显图节点的数量大大减少优化速度更快同时由于每个节点携带了多帧信息帧间约束的信噪比更高不容易被单帧噪声带偏。当然这也会带来一个问题如果某个 hyperframe 由于运动过大导致融合质量差它的误差会直接进入后端图。所以后端节点的生成必须经过严格的质量检查比如点云数量阈值、覆盖度、配准得分等。这三种角色并不互斥很多时候一个系统里同时存在几种用法。前端用 hyperframe 做配准后端用 hyperframe 做节点中端用 hyperframe 管理子图层层递进构成了一个比较健壮的建图链路。3. 手把手构建一套可用的 hyperframes 流程3.1 整体流程设计与输入输出我自己在工程里实现 hyperframes通常把它设计成一个独立模块输入有三样原始激光点云帧包含时间戳最好有每个点的相对扫描时间当前帧的粗略位姿估计可以来自 IMU 递推、轮速计或上一帧配准结果传感器内外参数雷达相对本体的外参雷达内参必要时还有运动补偿参数。输出是一个融合后的点云以及该 hyperframe 对应的位姿。这个位姿可以是窗口内首帧的位姿也可以是窗口中间参考帧的位姿具体取决于你的坐标系约定。我习惯用窗口内第一个有效帧的位姿作为 hyperframe 位姿这样后面做时间戳推断比较直观。整体流程可以拆成四步选帧、去畸变、变换融合、后处理。3.2 滑窗选取与体素滤波参数怎么定选帧是 hyperframe 构建的第一个关键环节。窗口不能太大否则计算量失控而且融合时会引入较大的累计误差窗口也不能太小否则多帧优势体现不出来。我常用的窗口判定条件有两个二者满足其一即可时间条件当前帧时间戳与窗口第一帧时间戳之差超过阈值比如 0.5s空间条件当前帧相对窗口第一帧的平移距离超过阈值比如 1.0m或旋转超过 15 度。为什么要双条件因为机器人可能长时间静止也可能高速运动。只按时间选静止时会堆一堆位置几乎相同的帧信息冗余严重只按距离选高速运动时窗口内帧数太少密度提升不够。双条件可以保证在静止期间不会过度建帧在运动期间不会让窗口信息稀疏。体素滤波参数的选择也很关键。一般我会根据雷达线数和场景规模来定16 线雷达室内环境体素尺寸 0.2m ~ 0.3m32 线雷达室外园区体素尺寸 0.2m ~ 0.5m64 线雷达开放道路体素尺寸 0.5m ~ 1.0m。体素越小分辨率越高但点数也会更多。实际调参时先按单帧原始点数估算假设单帧有效点 10 万窗口取 5 帧总共 50 万体素滤波后目标控制在 20 万左右这样既保留细节又不会让后端被数据淹没。3.3 位姿对齐与点云融合的关键步骤选完帧以后不能直接把点云丢到一个坐标系里。每一帧点云都带着自身的运动畸变需要先做去畸变。去畸变通常有两条路如果位姿估计频率较低采用线性插值根据每个点相对扫描起始时刻的时间比例在起始位姿和结束位姿之间插值出一个“扫描内位姿”再反向补偿到起始时刻坐标系如果 IMU 可用用 IMU 预积分在时间轴上插值出更准确的扫描内位姿。去畸变之后再把每一帧点云转换到窗口第一帧的坐标系下。假设第 i 帧相对第一帧的位姿是 T_1i原始点在雷达坐标系下的坐标是 p那融合后的坐标为 T_1i * p。如果有点已经做了去畸变那么直接乘 T_1i 即可。全部变换完成后把所有点放到一起做一次体素滤波目的是去掉重叠点、统一密度。之后再根据需要提取平面/边缘特征。很多人会在这里犯一个错误先用体素滤波再提取特征。但如果你先提取特征再把特征点合在一起做降采样特征强度往往会更稳定。我先说一个朴素的流程后面再讲进一步的处理。融合后的点云我还会做一次统计离群点剔除用每个点的近邻距离分布来判断是噪声还是有效点。动态物体会在多个帧之间留下“重影”这种重影在融合阶段会表现为局部密度异常统计滤波可以处理一部分。3.4 一个简化的实现框架下面是一个 Python 风格的伪代码方便理解整体流程class HyperframeBuilder: def __init__(self, time_window0.5, dist_window1.0, voxel_size0.2): self.time_window time_window self.dist_window dist_window self.voxel_size voxel_size self.frames [] # 每帧点云位姿时间戳 def add_frame(self, points, pose, timestamp, scan_time): self.frames.append({ points: points, pose: pose, timestamp: timestamp, scan_time: scan_time }) if self._window_satisfied(): hf_pose self.frames[0][pose] hf_points self._fuse_points() self.frames.clear() return hf_points, hf_pose return None def _window_satisfied(self): first self.frames[0] last self.frames[-1] dt last[timestamp] - first[timestamp] dx translation_diff(first[pose], last[pose]) return dt self.time_window or dx self.dist_window def _fuse_points(self): accumulated [] for i, frame in enumerate(self.frames): # 1. 用扫描内插值去除运动畸变 undistorted self._undistort(frame) # 2. 转换到第一帧坐标系 T_ref relative_pose(self.frames[0][pose], frame[pose]) transformed transform_points(undistorted, T_ref) accumulated.append(transformed) # 3. 合并 体素滤波 combined voxel_downsample(np.vstack(accumulated), self.voxel_size) # 4. 统计滤波去离群点 filtered statistical_removal(combined) return filtered这段伪代码里最需要关注的是_window_satisfied和_fuse_points两个方法。前者决定何时触发融合后者决定如何融合。在实际 C 工程里我建议把点云容器改成预先分配的数组避免反复申请内存去畸变用 Eigen 的齐次变换矩阵组装成复合变换不要逐点计算矩阵乘否则效率会非常拉胯。3.5 进一步处理让 hyperframe 更“高级”上面只是基础版本。在真实项目中我还会增加几个环节第一对每一帧点云的边界区域做权重缩减。机械式激光雷达的每一帧扫描在水平和垂直方向的边缘处点密度很低畸变也严重。融合时如果给边界点同等权重会把很多坏点带进来。做法是预先对每帧生成一个点权重字段边界点权重降低再在体素滤波时按权重做保留下采样。第二对不同时间戳的点设定不同强度权重。离中心参考时刻越远的帧位姿不确定性越高。我一般会做一个高斯衰减权重距离参考位姿最近的帧权重最高离得远的帧权重低。这样即使某些帧的位姿估计存在小误差也不会把误差全面扩散到整个 hyperframe 里。第三做特征级 hyperframe。正常的 hyperframe 是点云级融合但在特征提取阶段我们可以分别对每帧提取平面/角点特征再把特征融合成 hyperframe 的特征集合。这样做的好处是特征点在融合时可以做强度累加比如同一平面被多帧观测到融合后的平面系数置信度会更高。坏处是特征数量有限在退化场景里反而丢了原始点云的冗余性。两种方案没有绝对好坏取决于你后面配准用的什么算法。4. 实操中一定会遇到的坑与排查手册4.1 计算量爆炸hyperframe 不是越密越好很多人一上来就把窗口拉得很长参数也调得很小结果系统 fps 直接从 20 掉到 5后端优化半天不收敛。原因就是 hyperframe 包含的数据量太大了每来一帧都要重新融合、重新提取特征计算开销被无限放大。我自己的经验是先想清楚这个 hyperframe 服务的对象。如果只用于前端扫描匹配窗口内帧数控制在 36 帧就够如果用于构建子图和后端节点窗口长度可以放宽到 10 帧但必须配合体素滤波强降采样。另外不要每次都从头融合所有帧。可以用增量式体素地图维护一个体素哈希表新帧来了只更新新增点和删除过期点而不是把所有点重新放到一起滤波。这样计算量能下降一两个数量级。4.2 动态物体和重影如何减少鬼影在室外道路上跑旁边有车辆经过、人在走动多帧融合之后会出现明显的“鬼影”。因为同一物体在每一帧里的位置不一样直接叠加出来的结果是物体轮廓被拉长或者出现多层影子。这不仅影响建图美观还会让配准和特征提取产生错误。处理动态物体通常有三个层次显式剔除用语义分割网络或者帧间差分把动态物体的点云识别出来在融合前就删掉。精度高但需要额外算力统计降权在融合后如果一个体素单元中的点在邻域内的分布方差特别大就认为该处可能是动态区域降低这些点的权重时间一致性检查同一个空间位置如果被多次观测计算这些观测点之间的距离一致性。距离超过阈值保留中值附近的点删除离群点。我在实际工程中常用第二种和第三种组合因为不需要训练模型鲁棒性也不差。具体做法是在体素滤波时不仅统计点云数量还统计每个体素内点的最大距离差。如果某个体素内在同一方向上点云离散程度特别高就把这个体素内的点舍弃或者标记为不确定性栅格。4.3 退化环境的补救结构化 sparse 特征处理在长直隧道、室内走廊这种退化环境里hyperframe 虽然能增加点云密度但提升是有限的。因为退化不是“点少”造成的而是几何结构在某个方向上没有变化。比如一条笔直的走廊沿着走廊方向无论激光怎么看你得到的墙壁点云都是平行的点云再密也没有办法在机器人前进方向上提供约束。这时如果仍然把 hyperframe 直接拿去做配准ICP/NDT 会给出一个“看起来收敛但实际上在这个方向上随便飘”的结果。实操上要做的不是加帧数而是必须引入外部传感器约束用 IMU 的加速度和角速度提供重力方向约束以及短时间内的位移先验用轮速计提供里程计约束防止沿走廊方向的漂移在建图完成后通过回环检测把退化的子图纠正回来。同时在构建 hyperframe 时我会特意统计当前窗口内几何约束的主方向。具体做法对融合后的点云做 PCA计算三个方向的特征值分布。如果最大特征值远大于其他两个说明环境沿某个方向退化这时就降低 hyperframe 在配准过程中的置信度并在后端给它加一个较大的噪声协方差。这个处理在代码上并不复杂但在系统层面非常关键。很多开源系统没有显式暴露这个信息导致在退化环境中漂移得一塌糊涂。自己做 hyperframe最好把这一项加进去。4.4 hyperframes 参数速查表下面这张表是我在调试过程中沉淀下来的一套常用参数范围具体值还要看你用的传感器、运行平台和场景参数常见取值范围设置原则时间窗口0.3s ~ 1.0s运动越快窗口越短避免累计误差空间距离窗口0.5m ~ 2.0m与场景尺度相关室内取小值室外取大值旋转窗口5° ~ 20°防止旋转过大导致融合点云扭曲体素尺寸0.1m ~ 0.5m根据雷达线数和作用距离选择宁大勿小配准 max_correspondence_distance0.5m ~ 2.0m与 hyperframe 规模成正比每次融合最大帧数5 ~ 15超出了用增量式更新别硬怼实际调试时我建议先固定时间窗口只调节体素尺寸跑通一版再动其他参数。一次只改一个变量不然出问题都不知道是谁引起的。4.5 常见问题速查表再整理一个更偏故障排查的表方便你踩坑时快速定位现象可能原因排查与解决hyperframe 点云出现明显重影位姿估计不准或去畸变没做检查输入位姿来源增加 IMU 约束重新标定内外参配准得分波动大hyperframe 密度不均匀检查体素滤波是否生效动态物体是否剔除边界权重是否处理建图轨迹在直线场景漂移环境本身退化加入 IMU/轮速约束检查 PCA 特征值比降低退化方向权重内存占用持续上涨没有及时清理过期帧确认滑窗释放逻辑使用增量式体素地图hyperframe 生成耗时过长窗口帧数太多或逐点坐标变换限制帧数改用 Eigen 批量变换或 GPU 加速后端优化不收敛hyperframe 节点太多约束冗余增加节点间距阈值对质量差的 hyperframe 不建节点这些坑我都实实在在踩过。特别是去畸变和位姿估计不准导致的点云重影一度让我以为是雷达坏了后来才发现是窗口内位姿插值方法写错了。现在做 hyperframe我总是先把输入位姿的质量监控放在最前面如果高频位姿的置信度很低我宁可推迟 hyperframe 的生成也不盲目融合。5. 另一个容易被忽视的细节hyperframe 的时间戳与坐标系约定5.1 时间戳到底用哪个时刻hyperframe 是一个多帧融合的产物它不像单帧点云那样自带一个明确的“采集时刻”。生成 hyperframe 后它的时间戳该取窗口内第一帧、最后一帧还是中间帧这个看起来很琐碎的问题实际会影响后续的数据关联。我通常取窗口内第一个有效帧的时间戳作为 hyperframe 的时间戳。理由有两个第一后续如果要把 hyperframe 和相机图像对齐第一帧时间戳与原始数据流更接近便于查询最近邻第二如果和多传感器系统做时间同步第一帧时间戳往前推一个窗口就能判断出这个 hyperframe 覆盖的时间范围逻辑清晰。如果你用的是“帧间位姿增量建图”的方式也可以取窗口内位姿估计最准的那一帧作为参考帧。但这会增加额外的逻辑分支一般没必要。保持统一标准最重要。5.2 融合坐标系放在哪里融合时的目标坐标系通常选窗口第一帧的雷达坐标系或者当前滑动窗口起始时刻的本体坐标系。如果后续要和 IMU 做紧耦合我会把融合结果变换到 IMU 坐标系下。这里要强调的是一定要记录 hyperframe 的位姿是在哪个坐标系下的表达并在融合过程中使用统一的外参树。否则多传感器融合的时候坐标串了就是灾难。我见过一个项目因为 hyperframe 的点云停留在“雷达起始坐标系”但 hyperframe 位姿却是相对世界系的结果配准时把雷达外参重复叠加了一次整个地图都偏出去了。最后排查花了两天。所以我的建议是在 hyperframe 结构体里显式保存坐标系标识以及外参版本号尽量不要在后续代码里隐式假设坐标系。5.3 数据流设计上的建议如果要把 hyperframe 集成进一个实时系统我建议把它设计成异步模块。激光雷达帧率通常 10Hz而 hyperframe 生成的频率可能只有 25Hz。如果同步处理前端匹配会被拖慢。更合理的做法是主线程接收原始帧做轻量级的帧间匹配得到高频位姿hyperframe 模块异步订阅原始帧和位姿流在后台生成 hyperframe前端扫描匹配在 hyperframe 频繁更新时用最近生成的结果做 target不要求每帧都重新构建。这样的异步架构会增加一些代码复杂度但能保证系统的实时性。在嵌入式平台上你用双线程加互斥锁或者无锁队列就能实现。6. 从 hyperframe 到更完整的建图系统扩展思路hyperframe 并不是终点。当你掌握了 hyperframe 的构建方法可以继续往两个方向扩展。一个是回环检测。传统回环检测是用全局描述子对 scan context、PointNetVLAD 这类特征做检索。如果把 hyperframe 作为回环检测的基本单元而不是单帧点云检索的输入会更稳定因为你喂进去的是一段局部子图而不是随机的一帧。配合全局描述子的降采样和体素化检索准确率通常比单帧更高尤其是激光点云稀疏的场合。另一个是多传感器融合。hyperframe 与 IMU 预积分结合可以把扫描间的运动先验做进 hyperframe 的协方差里与视觉里程计结合可以把多帧图像观测投影到 hyperframe 的空间中补充激光观测不到的结构。这些扩展本质上都在做同一件事让“几何信息单元”更大、更完整减少系统对单帧观测的依赖。如果你实际动手实现了 hyperframe你会发现很多 SLAM 的问题不再是“算法适用性”的问题而是“数据结构是否合理”的问题。就像盖房子一砖一瓦当然重要但砌成预制板再盖楼效率和质量会完全不一样。hyperframe 就相当于把砖块预制成板材的那一步简单却很有用。我个人在实际操作中的体会是hyperframe 的价值不是靠堆帧数实现的而是靠一套严密的时间窗口、空间阈值和退化检测机制。最开始我追求的是“尽量多融合几帧”后来才发现正确选择哪些帧、过滤哪些帧、给不同观测多少权重才是这项技术真正的门槛。希望这篇文章能让你避开我走过的弯路更快地把 hyperframes 落到自己的系统里。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进