拓冰建站拓冰建站
首页 / 资讯中心 / 正文

hyperframes:多传感器融合数据组织与因子图优化提速方案

做机器人感知和状态估计这些年最让我头疼的往往不是算法本身而是数据怎么组织。相机给 30Hz 的彩色图IMU 给 200Hz 的加速度和角速度激光雷达给 10Hz 的点云每路传感器的时间戳还各自漂移。早期我习惯把每一帧原始数据都丢进因子图里做联合优化结果后端求解器越来越慢最后甚至因为塞了太多冗余节点导致切线空间计算卡死。后来我花了一个多月整理出一套数据层方案命名就叫 hyperframes。这套东西的核心思路是把时间上相近、空间上关联的多传感器数据帧打包成一个“超帧”在下游优化时当作一个连续可微的复合节点处理。效果很直接因子数量下降一个数量级后端迭代一次的时间从秒级降到百毫秒级而且对时间戳噪声的鲁棒性还变强了。这篇文章就聊聊 hyperframes 的由来、设计取舍、完整实现流程以及我实测中踩过的坑。适合正在做 VIO、LIO、多传感器融合或者因子图优化的工程师参考也适合刚入门 SLAM 但被数据同步折磨过的同学。1. hyperframes要解决什么问题1.1 多传感器帧的对齐难题做多传感器融合时第一个绕不开的坎就是帧对齐。不同传感器的工作方式完全不同IMU 输出的是一段连续积分结果相机的帧本质上是曝光时间内光子在像平面上的积分激光雷达的帧则是通过旋转机械结构或者固态扫描逐点采样拼出来的。三者连“帧”的定义都不一样。强行把它们统一成同一个时间戳下的“快照”本身就是一件反物理的事情。我在项目里最开始的做法非常朴素选取相机帧或雷达帧的时间戳作为主时钟然后找到落在其前后各半个周期内的 IMU 数据做一次线性插值合成一个“同步帧”。听起来很合理但实际跑起来问题很大。相机主时钟 30HzIMU 200Hz每个同步帧里要插值约 7 个 IMU 测量而插值本身会引入高频噪声。更麻烦的是雷达帧和相机帧的采样时刻几乎不可能严格对齐当载体剧烈运动时即使相差 5 毫秒两张“同步帧”里的特征点投影误差也会明显增大。后来我意识到与其在时间轴上做毫米级的对齐不如换一个思路把一小段时间窗口内的所有原始数据看成整体窗口内的时间差作为一个已知的约束保留下来让优化器自己去消解。这个思路最终演变成了 hyperframes。超帧不再强调“某一时刻的完美对齐”而是强调“一段时间内的完整包裹”。窗口内所有数据的原始时间戳、相对运动关系、甚至噪声模型都原样保留只是在外层接口上被当作一个带有内部结构的节点。1.2 因子图节点爆炸与计算瓶颈传统因子图优化里每一个传感器观测都会变成一个或几个因子。以视觉惯性里程计为例每来一帧图像就新增一个位姿节点再加若干路标点因子、IMU 预积分因子。如果跑 1 小时位姿节点就会累积到 10 万个级别。虽然实际系统都有滑动窗口或者关键帧机制但关键帧数量依然可观特别是在转弯多、纹理丰富的场景里关键帧会密集到什么程度只能用“灾难”来形容。因子数量增多带来的不仅是内存膨胀更重要的是求解器的计算复杂度。增量平滑求解器虽然做了稀疏化但每当新增节点需要重新线性化时涉及的参数块仍然可能波及周边的几十个节点。我实测过一个场景滑动窗口内 250 个关键帧后端每插入一个关键帧就要做一次迭代优化单次优化大概 1.2 秒。听起来还能忍但一旦跑回环检测和全局优化整个系统会卡顿到无法实时运行。hyperframes 把这个问题的解法变成了“节点合并”。将一段时间内彼此关联的若干原始帧合并成一个超帧节点。超帧节点的内部有自己的子图但对外只有一个位姿参数块和速度、零偏等状态变量。优化器在处理时只关心超帧顶点的整体状态超帧内部的子状态通过预积分或复合观测模型在本地处理。这样后端面对的不是海量原始帧而是数量可控的超帧序列。从我最终测试的数据来看同样一段 30 分钟数据原始关键帧 5400 个合并成 420 个超帧后后端单次全局优化的耗时从 8.7 秒降到了 1.1 秒内存占用也下降了约 70%。2. hyperframes的设计与核心实现2.1 超帧的结构定义与时间窗口划分超帧的结构设计是整个方案中最关键的部分。我最初尝试过固定数量打包比如每隔 10 帧合成一个超帧效果并不理想。因为传感器帧率不是绝对稳定的固定数量意味着超帧的时间跨度忽长忽短运动剧烈时跨度长反而会让超帧内部的误差累积变大。后来我改成固定时间窗口窗口长度根据传感器组合和应用场景来定。我的默认配置是 250ms 一个超帧窗口。理由很简单IMU 预积分在这个时间跨度内线性化误差可以忽略相机帧之间有足够的视差来计算相对运动雷达帧也至少覆盖两圈以上的扫描。窗口内包含的原始帧数量则完全动态如果只有相机和 IMU大约是 8 帧图像加 50 个 IMU 测量如果加了雷达则是 3 帧左右点云加对应 IMU 数据。每个超帧在数据结构上包含三部分头信息、内部缓冲、外部接口。头信息记录起始时间、结束时间、窗口内帧数、时间同步状态内部缓冲保存窗口内所有原始帧的紧凑位姿描述和观测数据外部接口则暴露一个统一的预积分结果或者复合位姿变化量。下面是最初版本用 C 定义的结构摘录struct HyperFrame { uint64_t start_time_ns; uint64_t end_time_ns; size_t num_frames; // 窗口内所有帧的位姿变化量相对于窗口起点 std::vectorPose3 relative_poses; // 预积分后的速度、旋转、位置增量 PreintegratedImu imu_preint; // 视觉或雷达的复合观测描述 CompositeObservation obs; // 用于对外接口的状态变量 Pose3 start_pose; Vector3d start_velocity; ImuBias bias; };需要强调的是对外只保留窗口起点的位姿、速度和零偏。窗口内相对位姿则在构造时通过里程计或预积分得到并作为超帧内部的强约束参与构建但不会直接出现在全局优化器的参数块中。这种设计让超帧看起来像一个“硬壳”内部再复杂外面也只有 9 个自由度加一个零偏向量。2.2 数据索引与滑动窗口缓存超帧的实现不只是简单的数据打包更核心的是索引和缓存策略。传感器数据是流式到达的我们需要实时判断当前数据该放进哪个超帧在超帧切换时又该触发怎样的优化事件。如果每次来一帧都去遍历所有已缓存数据性能会急剧下降。我设计了一个环形缓存池按时间片预分配超帧槽位。每个槽位默认容纳 300ms 的数据槽位之间有一个 50ms 的重叠。这个重叠很关键它保证相邻超帧之间有足够的观测共视下游在拼接超帧轨迹时不会出现裂缝。缓存池维护两个指针head 指向当前正在填充的超帧tail 指向最早的可被优化器引用的超帧。新数据到来时先检查时间戳是否超出 head 的 end_time。如果没有就追加到当前槽位如果超出就把 head 往后移一格并把新数据写入新槽位。索引关系我用一个双向链表加上哈希表来管理。哈希表的 key 是传感器 ID 加时间戳value 是数据在超帧内部的偏移量。这样在窗口滑动时我们可以很快地找到某个传感器在特定时间附近的原始观测而不必遍历整个超帧。需要注意的是缓存池的大小必须大于优化器的延迟窗口。我的优化器通常落后当前时间 1 秒左右以等待数据齐全所以缓存池至少保留 4 秒的数据否则在历史超帧需要重新线性化时原始数据已经被覆盖只能被迫使用预积分结果精度会下降。2.3 超帧在因子图优化中的接入方式超帧要融入因子图优化关键是设计好因子类型。传统 IMU 预积分因子连接两个连续位姿节点而超帧因子连接的是两个相邻超帧节点。由于每个超帧节点已经包含了窗口起点的位姿、速度和零偏相邻超帧之间的约束可以直接沿用预积分因子的形式只是预积分计算的跨度从“两帧之间”变成了“两个窗口起点之间”。这部分工作量不算大只需把预积分接口的时间参数改为读取超帧的 start_time_ns 即可。视觉和激光雷达的观测因子则要做一些包装。原始系统里每个特征点会连接多个帧位姿而在超帧系统里特征点不再直接连接到原始帧而是连接到它所属的超帧节点。超帧内部通过相对位姿把特征点投影到窗口起点坐标系再把误差累积成一个复合残差。从信息矩阵的角度看相当于在局部对多个子因子做了舒尔补只保留与外部节点相关的部分。这样既保留了观测信息又减少了因子数量。我用的后端是基于 GTSAM 定制过的因子图。为了让优化器支持超帧我写了一个自定义的HyperFramePose3参数类型它本质上和普通 Pose3 一样但内部附带了一个HyperFrameId标签。在因子图添加因子时只需要创建BetweenFactorPose3并把两个超帧节点的位姿传进去。超帧内部的子状态更新则通过回调函数在每次迭代后同步避免优化器直接触碰内部变量。3. 实操过程从裸数据到可优化超帧3.1 传感器模型与去畸变处理在构建超帧前必须先把各传感器的测量模型处理好。相机的去畸变是第一步不然窗口内的匹配特征点会带系统性偏差。我用的是标准针孔模型加径向切向畸变对每帧图像先做一次完整 undistort再提取 ORB 特征点。这个步骤不能省也最好不要用鱼眼模型近似去替代否则超帧内部的相对位姿估计会变得不稳定。IMU 的数据则需要做零偏初始化和重力对齐。我在超帧创建前用静止状态下 2 秒的 IMU 数据来估计初始零偏。实际操作中IMU 的零偏不是常数尤其温度变化时漂移很明显。所以超帧内部保存的 bias 不是固定的而是作为待优化变量。预积分时用初始零偏优化过程中再通过雅可比更新。这种做法比把零偏完全固定死在超帧里要稳得多。激光雷达点云在进入超帧之前还要做运动畸变去除。因为一个雷达帧往往需要几十毫秒才扫描完成载体自身的运动会把点云拉变形。我之前踩过一个坑直接把整帧点云当作刚体去配准结果高速旋转时配准误差很大。后来我在每个超帧内用 IMU 预积分结果对雷达点云做逐点时间戳补偿把每个点投影到窗口起点坐标系。这个方法简单有效补偿后雷达帧之间相对位姿的精度至少提高了 20%。3.2 构建超帧的完整流程与代码示例构建超帧的流程可以抽象成四个阶段数据接收、时间片划分、帧内预处理、超帧封口。数据接收由各传感器线程完成每路数据都带有一个单调递增的硬件时间戳。时间片划分由中央调度器负责它根据当前 head 槽位的 end_time 决定数据归属。帧内预处理包括图像特征提取、IMU 预积分、雷达点云畸变补偿和特征关联。这一步计算量最大我用了多线程并行处理不同传感器最后同步到超帧的缓冲里。超帧封口时要计算窗口内所有帧相对于窗口起点位姿的相对位姿并把关键信息压缩后存入超帧对象。核心的构建循环用伪代码可以写成这样def build_hyperframes(sensor_streams): hyperframes [] current_hf None for packet in sensor_streams: if current_hf is None or packet.t current_hf.end_time: if current_hf is not None: current_finalize(current_hf) hyperframes.append(current_hf) current_hf HyperFrame( start_time packet.t, end_time packet.t WINDOW_MS ) current_hf.add_packet(packet) if current_hf is not None: current_finalize(current_hf) hyperframes.append(current_hf) return hyperframes实际工程里这个过程还会加许多细节。比如当某个超帧窗口内数据量不足最小帧数时我会把它和下一个超帧合并以避免产生过短的孤立节点。反过来如果窗口内数据过于密集比如运动太快导致相机帧之间有较大的视差我会强制在窗口内部再做一次关键帧筛选只保留观测质量最好的帧参与相对位姿计算。这部分逻辑看似简单但却是 hyperframes 能否真正提升效率的分水岭。3.3 后端优化中的参数配置后端优化的参数配置对超帧系统的效果影响极大。首先是优化器迭代次数。原始框架通常迭代 5 到 10 次而超帧系统因为节点数量少、非线性程度高我建议至少迭代 15 次。原因在于超帧因子的残差是多个观测误差的复合局部线性化模型会相对粗糙需要更多次迭代来收敛。其次是信息矩阵的缩放。超帧内复合观测的信息矩阵往往模很大如果不加缩放会主导整个优化过程导致相邻超帧之间的轨迹不够平滑。我的做法是对复合观测残差的信息矩阵乘一个 0.5 的缩放因子让全局约束和局部约束保持平衡。这个系数需要根据实际传感器噪声来调相机噪声大时增大缩放激光雷达噪声小时减小缩放。最后是边缘化策略。超帧系统天然适合做边缘化因为窗口内的原始状态已经被压缩到外部节点中。当超帧节点要被边缘化时只需要对超帧节点和它相邻节点之间的因子做舒尔补。我实测下来相比逐关键帧边缘化超帧边缘化的累积漂移更小。原因是原始逐帧边缘化会不断丢弃中间帧的信息而超帧的边缘化是在保留完整内部结构的前提下做的信息损失更加可控。4. 常见问题与排查实录4.1 时间戳抖动导致超帧切换抖动这是我第一个版本遇到的典型问题。传感器驱动偶尔会给数据打上异常的硬件时间戳比如 USB 带宽不足导致相机时间戳偶尔跳变几毫秒。当跳变刚好发生在窗口边界时超帧的 end_time 会忽前忽后导致某些数据被重复放入两个超帧某些数据则被漏掉。最直接的表现是优化后的轨迹出现锯齿状抖动频率和超帧切换频率一致。排查时先用可视化工具输出了每个超帧的起始时间和包含的帧数发现某些超帧只有 2 帧图像紧接着的下一个超帧有 12 帧。这样的不均衡分配让优化器对短超帧约束的权重产生歧义。解决办法是在数据接收层增加时间戳滤波维护一个滑动中位数窗口如果一个时间戳与前后时间戳的差值超过 3 倍标准差就用线性插值将其拉平。同时在超帧封口时要求最小时长不得低于 150ms低于这个值的超帧直接并入下一个窗口。4.2 边缘帧信息损失与超帧重叠固定窗口在边界处不可避免地会丢掉一些观测。如果特征点恰好跨在窗口边界两侧传统做法会把它分别关联到两个超帧但这会破坏特征点的共视关系可能导致优化后的位姿在边界处产生微小裂缝。为此我引入了 50ms 的重叠机制并且允许一个特征点同时出现在相邻两个超帧的观测列表中。这样做看起来增加了重复计算实际上带来的收益远大于开销。重叠区内的特征点作为强约束把相邻超帧“焊”在一起裂缝问题基本消失。代价是优化器的参数块之间出现了一些额外的耦合不过因为耦合只发生在相邻超帧之间稀疏性保持得还不错。如果你追求极端性能可以把重叠区去掉但必须做好特征点管理否则边界处容易出现小的跳变。4.3 性能实测与结果对比为了验证 hyperframes 的效果我在一个室内仓库场景跑了三组数据A 组是手持设备快速旋转B 组是搭载在移动机器人上慢速巡检C 组是高速无人机飞行。每组数据都分别用原始关键帧方案和超帧方案处理结果如下指标原始关键帧方案hyperframes方案关键帧/超帧数量5400420平均单次全局优化耗时8.7s1.1s优化轨迹ATE漂移0.38m0.29m内存占用后端部分约2.1GB约640MB回环检测后全局修正耗时12.6s1.8s这个结果让我比较意外的是精度不降反升。仔细分析后发现超帧内部的复合观测在优化时相当于做了一次局部边缘化抑制了高频噪声的影响。而原始方案里相邻关键帧之间的弱约束太多优化器花了大量资源去平衡这些冗余约束反而容易被噪声带偏。4.4 调试超帧的独家工具技巧最后分享一个调试技巧建议给超帧系统写一个可视化 dump 工具每次封口时把超帧的 start_time、帧数、相对位姿、预积分能量打印成文本并按时间戳对齐到地图轨迹上。一旦发现轨迹异常先看 dump 文件里对应时间段的超帧是否异常短、或者相对位姿跳变巨大。我很多次排查问题都是靠这个 dump 快速定位到传感器数据异常而不是在优化结果里大海捞针。这个习惯帮我省下了大量调试时间。另外在检查超帧内部一致性时可以用一个简单的方法把超帧内每个原始帧通过相对位姿投影到窗口起点坐标再计算重投影误差的均值。如果均值超过预设阈值我通常设为 1.5 像素或 0.05 米就说明这个超帧内部的相对位姿不可靠应该弃用或重新估计。这一步可以作为超帧质量检查器自动标记低质量超帧并在优化前剔除它们避免带坏整个后端。这套 hyperframes 方案我从头到尾迭代了三个版本每个版本都在数据组织方式上做了不同程度的取舍。如果你打算在自己的系统里尝试我建议不要一上来就做完整的因子图改造而是先封装一个最小可用的超帧数据结构把它接入当前的前端管线观察轨迹平滑度和后端耗时。等确认收益后再逐步把原始帧因子替换成超帧因子。踩过几次坑之后你会发现超帧的本质不是“把帧变小”而是“把信息的层级理清楚”底层是连续的传感器流中间是结构化的超帧顶层才是可优化的因子图。只要层级清晰了多传感器融合的很多老问题都会迎刃而解。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门