历史快照
本专题页写于包拆分与 FAST-LIVO2 / CustomMsg / 图像输入落地之前,反映当时的单包结构与
五前端状态。已知过时点:目录布局(apps/launch 现属 asuka_ros 包)、共享的
config_sensor.json(外参已改为各前端 odometry 配置的 sensor 块)、ROS 层 acc_scale
等描述、前端数量(现六种,新增 fastlivo)、CustomMsg 与图像输入链路。2026-09 下旬以来:lightning / superodom 前端与 optimization_miao 后端已移除
(现五前端:fastlio / batchlio / superlio / fastlivo / srlivo;smallpointlio 已于 2026-10-23 移除,odometry/10-11 为其历史快照);
freedom 扩展已整体迁出 asuka(移入 nagisa 包);KeyFrame::stamp 统一为扫描
起始时刻;执行器新增连续失败熔断与 wait_for() 排空检测、扩展与执行器有
ok() 健康查询。
现行事实以主文档 (index / architecture / parameters / odometry 等)与源码为准。
title: "BatchLIO 里程计主流程" description: >- 五种实现中最庞大的文件(43.8KB cpp),承担批量式 LIO 的状态估计主循环与地图维护,是阅读该项目计算主链路时投入产出比最高的一页。
BatchLIO 里程计主流程(odometry_estimation)
定位与文件构成
src/asuka/src/asuka/odometry/batchlio/odometry_estimation.cpp(43.8KB)是五种里程计实现中最大的单文件,与 6.6KB 的同名头文件共同承载批量式 LIO 的状态估计主循环。子模块内的其余文件围绕它服务:
| 文件 | 体量 | 角色 |
|---|---|---|
odometry_estimation.cpp/.hpp |
43.8KB / 6.6KB | 主估计循环、双 ESKF、缓冲与地图维护 |
common.hpp/.cpp |
4.9KB / 4.2KB | batch::state_input/state_output、MeasureGroup、IVoxType、time_compressing_batch/time_list、reset_cov、get_f_input/df_dx_* 等批处理公共件 |
so3.hpp |
4.0KB | SO(3) 流形运算(状态中 rot 为 batch::SO3) |
cloud_preprocess.cpp / imu_preprocess.cpp |
5.3KB / 3.2KB | 数据入口的算法私有预处理 |
create.cpp |
183B | extern C 插件导出 |
类声明 class OdometryEstimation : public asuka::OdometryEstimation(且显式 delete 拷贝),完整实现了基类契约:insert_imu / insert_frame / process_once / workload / stop / save_map / cloud_preprocess,其中 cloud_preprocess() 返回内部 cloud_preprocess_impl 供 ROS 层在入队前调用。
构造装配:三份配置 + 双滤波器
构造函数一次读齐三份配置:
- config_odometry:
batch(dt=0.001、deskew、enable_openmp)、mapping(约 20 个键:imu_en、各类协方差、plane_thr、match_s、init_map_size、use_imu_as_input、gravity、enable_map_saving 等)、odometry(四个 circular_buffer 容量)、filter(surf/map 体素尺寸); - config_sensor:经
active_lidar_key()按config_ros.lidar_type选择livox/robosense块,读取外参extrinsic_T/extrinsic_R;无对应雷达块直接 throw,这是构造期唯一硬失败点。
滤波器装配的核心是双 ESKF 并存:
kf_input.init_dyn_share_modified_2h(get_f_input, df_dx_input, &h_model_input_trampoline);
kf_output.init_dyn_share_modified_3h(get_f_output, df_dx_output,
&h_model_output_trampoline, &h_model_imu_output_trampoline);
两套滤波器对应同一状态的不同参数化:匿名命名空间的 reset_state_input/output 展示了字段——state_input 含 pos/rot/offset_R_L_I/offset_T_L_I/vel/bg/ba/gravity 共 8×3 = 24 维;state_output 额外携带 omg/acc,共 30 维。即 input 模式把 IMU 当控制输入(状态里只需 bias),output 模式把 IMU 当量测(状态里必须显式携带角速度/比力)。use_imu_as_input(默认 false)决定运行路径。随后 reset_cov → change_P 初始化协方差,process_noise_cov_input/output 生成 24×24 / 30×30 过程噪声。
局部地图用 batch::IVoxType:分辨率来自 ivox_grid_resolution(默认 2.0m),ivox_nearby_type 把 0/6/18/26 映射到 CENTER/NEARBY6/18/26,非法值回退 NEARBY18。OpenMP 线程数在 batch_omp 开启时取 min(omp_get_max_threads(), 16)。
关键状态分组
| 分组 | 成员 | 说明 |
|---|---|---|
| 输入缓冲 | lidar_buffer/time_buffer/imu_buffer(环形,容量可配)+ last_imu_stamp |
全部由 buffer_mutex 保护;环形写满覆盖最旧。last_imu_stamp 是纯水位(回环丢弃已移除,检测统一在 asuka_ros),供 synchronize 的数据饥饿判定使用 |
| IMU 初始化 | initializing_imu、imu_initialization、last_synchronized_imu |
初始化期样本额外转交 imu_preprocess |
| 双 ESKF | kf_input(24)/kf_output(30)、p_input/p_output/q_input/q_output |
use_imu_as_input 二选一 |
| 批处理工作区 | feats_down_body/world(预分配 10000)、normvec(100000)、point_selected_surf[100000]、time_seq、nearest_points、crossmat_list |
大容量预分配,避免迭代中反复申请 |
| 局部地图 | ivox、init_feats_world、init_map、init_map_size(默认100) |
冷启动累积 → 注入 IVox |
| 帧内时序 | imu_deque、imu_last/imu_next、time_current、time_update_last、time_predict_last_const |
输出循环里在点时刻间插值推进 IMU |
| 输出 | map_cloud、pcd_save_en |
save_map() 直接返回 map_cloud(mapping.enable_map_saving 开启时逐帧累积) |
调用链:从 insert 到 process_once
上层 AsyncOdometryEstimation 按到达序逐条喂 insert_imu/insert_frame,每条之后循环调 process_once() 排空计算。输入槽 on_insert_imu / on_insert_frame 由执行器在喂算法之前统一发射,算法内不做回环丢弃(检测统一在 asuka_ros 告警)。主循环结构如下:
flowchart TB
subgraph BUF["入口缓冲(buffer_mutex 保护)"]
A["insert_imu:更新水位 → imu_buffer"]
B["insert_frame:空帧检查 → lidar_buffer + time_buffer"]
end
A --> E
B --> E
E["synchronize<br/>end_time = begin + max(offset)·1e-3<br/>IMU 未覆盖到 end_time → 返回 false(数据饥饿保护)"]
E -->|"MeasureGroup"| F["process(measurements)"]
F --> G["handle_first_scan<br/>写入重力、计算 g_m_s2、推进 imu_deque"]
G --> H["downsample_and_window<br/>VoxelGrid + 按 time_list 排序"]
H --> I["time_compressing_batch → time_seq<br/>窗口粒度 batch_dt·1000"]
I --> J{"init_map?"}
J -->|"否(冷启动)"| K["initialize_map<br/>点变换世界系累积进 init_feats_world<br/>≥ init_map_size 后 ivox->AddPoints"]
K -.->|"init_map=true 后"| L{"use_imu_as_input?"}
J -->|"是"| L
L -->|"false(默认)"| M["run_output_loop<br/>kf_output.predict + update_iterated_dyn_share_IMU"]
L -->|"true"| N["run_input_loop(kf_input 路径)"]
M --> O["map_incremental → emit_outputs(头文件声明的收尾段)"]
N --> O
O --> P["map_cloud → save_map()"]
图中 synchronize → process 为源码直接证据(L214-218);process 内部的阶段顺序按私有方法族与文件内定义顺序重构(process 主体位于证据片段之外),map_incremental/emit_outputs 为头文件声明的管线收尾。
synchronize 的三个细节
- 扫描结束时刻由点云内
offset最大值(每点相对时刻,毫秒)推得:end_time = begin_time + max_offset * 1e-3; - 数据饥饿保护:
imu_enabled && last_imu_stamp < end_time时返回 false,本帧留待下一轮——保证任何窗口都有完整 IMU 覆盖; - 初始化期把窗口内 IMU 复制进
imu_initialization并挂到measurements.imu_initialization;正常运行后若窗口无 IMU,则复用last_synchronized_imu兜底;窗口后第一条 IMU 单独放入imu_after_end。
process_once 在 process 后检查 imu_preprocess->imu_need_init,一旦初始化完成即清空 imu_initialization 并置 initializing_imu=false,完成初始化状态的交接闭环。workload() 只返回 lidar_buffer.size(),与异步执行器页所述"workload 只反映输入缓冲"一致。
run_output_loop:传播与更新的节奏(证据至 L429)
按 time_seq 划分的每个窗口,循环做两件事:
- IMU 推进:维护
imu_last/imu_next双指针扫过imu_deque,当time_current > imu_next.stamp时取新 IMU 值写入angvel_avr/acc_avr; - 双段 predict:
kf_output.predict(dt, q_output, input_in, true, false)在两个 IMU 时刻间推进状态;随后以predict(dt_cov, …, false, true)传播协方差并调kf_output.update_iterated_dyn_share_IMU()——即把 IMU 本身作为量测做迭代更新(这正是 output 模式的语义)。
无 IMU 模式下(imu_en=false)handle_first_scan 直接把 acc 置为 -gravity 并强制 imu_need_init=false,整条 IMU 链路被旁路。
双 ESKF 与 trampoline 量测回调
esekfom 的量测回调是 C 风格静态函数,而残差计算需要访问实例状态。本实现用 static thread_local OdometryEstimation* active 解决:process_once 在进入 process 前执行 active = this,三个 h_model_*_trampoline 借此转发到实例的 h_model_input(点面残差)、h_model_output(点面 + IMU 量测残差)、h_model_imu_output。这三个函数是整个配准数学的定制点,也是阅读本文件后半部分(L750 之后)的主线索。
标志位与生命周期边界
| 标志 | 初值 | 翻转时机 |
|---|---|---|
first_scan_flag |
true | handle_first_scan 首次执行(记录首帧/首 IMU 时刻、设重力) |
is_first_frame |
true | run_output_loop 处理第一个窗口时 |
initializing_imu |
true | process_once 检测到 !imu_need_init 后 |
init_map |
false | init_feats_world ≥ init_map_size,注入 IVox 后 |
reset_flag |
true→false | reset() 执行期间(整体复位含新建 IVox、重置双滤波器) |
防御性边界汇总:空点云直接丢弃;IMU 未覆盖窗口尾则整帧等待;config_sensor 无 lidar_type 对应的雷达块抛异常;ivox_nearby_type 非法值回退;析构函数调 stop() 清空三个缓冲。时间回环不在算法内处理——AsukaROS::handle_loop_back 是唯一检测点(仅告警),last_imu_stamp 仅作同步水位使用。
扩展点
use_imu_as_input:24 维/30 维双模式切换,是理解本文件分支的第一开关;batch_dt / batch_deskew / batch_omp:批窗口粒度、去畸变开关与并行度(线程上限 16);h_model_*三量测模型:替换点面/IMU 残差即改变配准行为;- 被注释的
extrinsic_est_en块(构造与 reset 各一处):雷达-IMU 外参在线估计是预留未启用的扩展位; - IVox 分辨率与近邻类型:局部地图密度/查询范围的直接旋钮。
阅读建议
建议顺序:构造函数(参数全景)→ synchronize(数据契约)→ run_output_loop 证据段(传播-更新节奏)→ 其余 h_model_* 与 run_input_loop/map_incremental/emit_outputs 段落;同时对照 common.hpp 中 state_input/state_output 字段与 MeasureGroup 定义,可覆盖本页未能展开的后半程。
Sources: src/asuka/include/asuka/odometry/batchlio/odometry_estimation.hpp#L1-L188, src/asuka/src/asuka/odometry/batchlio/odometry_estimation.cpp#L1-L220, src/asuka/src/asuka/odometry/batchlio/odometry_estimation.cpp#L155-L470, src/asuka/src/asuka/odometry/batchlio/odometry_estimation.cpp#L352-L750
关联源码
src/asuka/src/asuka/odometry/batchlio/odometry_estimation.cppsrc/asuka/include/asuka/odometry/batchlio/odometry_estimation.hpp