历史快照
本专题页写于包拆分与 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: "IMU 接入与标定补偿" description: >- insert_imu 将 sensor_msgs::Imu 转为 ImuData,先做时间回环检查,再叠加 imu_time_offset 与 acc_scale 标定系数后交给异步队列;传感器标定类修正被集中收敛在 ROS 封装层,算法层无需感知。
IMU 接入与标定补偿
模块职责
本页覆盖 IMU 数据从 ROS 消息进入 asuka 框架的第一段旅程:AsukaROS::insert_imu(src/asuka/src/asuka/asuka_ros.cpp#L59-L70)。它承担三类职责,且只承担这三类:
- 类型转换:把
sensor_msgs::Imu摊平为框架内部契约ImuData(src/asuka/include/asuka/core/types.hpp#L20-L27),即stamp(double 秒)+linear_acc/angular_vel(Eigen::Vector3d)。 - 时间回环检查:调用
handle_loop_back(stamp, FrameId::IMU),对 rosbag 循环播放等时间回退场景发出告警。 - 标定补偿:叠加
imu_time_offset(时间偏移)与acc_scale(加计尺度)两个系数。
关键设计取向是:所有传感器标定类修正被收敛在 ROS 封装层。五种里程计插件通过 OdometryEstimation::insert_imu(ImuData::ConstPtr) 接口收到的数据已经是补偿完毕的干净样本,切换 so_name 更换算法时无需重复实现任何标定逻辑;算法私有的进一步 IMU 处理(滤波、前向传播、时间同步)下沉在各算法自己的 imu_preprocess 中,与本层无关。
调用链
ros::Subscriber (imu_topic, queue=1000) asuka_rosnode.cpp L19-21 / nodelet L61
│ sensor_msgs::Imu::ConstPtr
▼
AsukaROS::insert_imu(msg) asuka_ros.cpp L59
│ 1. make_shared<ImuData>,stamp = header.stamp.toSec()
│ 2. handle_loop_back(stamp, FrameId::IMU) ← 用补偿前的原始时间戳
│ 3. 填充 linear_acc / angular_vel
│ 4. stamp += imu_time_offset ← 标定补偿 ①
│ 5. linear_acc *= acc_scale ← 标定补偿 ②
▼
AsyncOdometryEstimation::insert_imu(ConstPtr) async_odometry_estimation.cpp
│ 锁内检查 running / end_of_sequence,入 imu_queue,cv.notify_one
▼
worker 线程逐条取出(先发射 on_insert_imu)→ odometry->insert_imu(imu)
(IMU/帧按到达序混排投递,复刻上游 ROS 回调执行器语义)
两个 ROS 入口(独立节点 asuka_rosnode.cpp 与 nodelet asuka_nodelet.cpp)都以 lambda 或成员函数直接转发到同一个 insert_imu,IMU 订阅队列长度为 1000(远高于点云的 50,对应 IMU 的高频特性)。
关键状态
| 状态 | 位置 | 作用 |
|---|---|---|
acc_scale{1.0} |
asuka_ros.hpp L52 | 加速度计尺度因子,构造时从 config_ros.json 的 "ros" 组读取(asuka_ros.cpp L37),默认 1.0 即直通 |
imu_time_offset{0.0} |
asuka_ros.hpp L53 | IMU 时间偏移(秒),同处读取(L38),正值把时间戳向后推 |
last_timestamp_imu{-1.0} |
asuka_ros.hpp L58 | 回环检测的游标,记录最近一次 IMU 原始时间戳,-1.0 保证首条消息必通过 |
| imu_queue / frame_queue(环形缓冲 1000/50) | AsyncOdometryEstimation | 互斥锁 + 条件变量保护,worker 按队首传感器时间交错逐条消费 |
值得注意的状态语义细节:handle_loop_back 在 imu->stamp += imu_time_offset 之前调用(L62 对比 L67),因此回环检测比较的是补偿前的原始传感器时间线,检测语义与消息流严格一致,不受补偿参数影响。
另一个易被忽略的点:acc_scale 只乘在 linear_acc 上(L68),angular_vel 原样传递——陀螺仪没有尺度修正是当前实现的有意取舍。
数据流图
flowchart TD
A["sensor_msgs::Imu<br/>(imu_topic, queue=1000)"] --> B["AsukaROS::insert_imu<br/>asuka_ros.cpp L59"]
B --> C["构造 ImuData::Ptr<br/>stamp = header.stamp.toSec()"]
C --> D["handle_loop_back(stamp, FrameId::IMU)<br/>与 last_timestamp_imu 比较"]
D --> E{"stamp < last_timestamp_imu ?"}
E --> "是" --> F["logger->warn 'imu loop back detected'<br/>仅告警,数据不丢弃"]
E --> "否" --> G["继续"]
F --> G
G --> H["填充 linear_acc / angular_vel"]
H --> I["stamp += imu_time_offset<br/>标定补偿①: 时间偏移"]
I --> J["linear_acc *= acc_scale<br/>标定补偿②: 加计尺度(陀螺不修正)"]
J --> K["async->insert_imu(ConstPtr)"]
K --> L{"running 且<br/>非 end_of_sequence ?"}
L --> "否" --> M["静默丢弃<br/>async_odometry_estimation.cpp"]
L --> "是" --> N["imu_queue.push_back<br/>cv.notify_one"]
N --> O["worker 逐条取出<br/>先发射 on_insert_imu 再喂 odometry->insert_imu"]
图中关键节点:
- D/E/F(回环闸口):这是一道“哨兵”而非“闸门”——检测到回退只输出
imu loop back detected: {旧} -> {新}告警(asuka_ros.cpp L177-181),不丢帧、不重排、不重置队列,真正的乱序容错交给下游算法层。 - I/J(标定补偿):紧邻数据从 ROS 类型转为内部类型的边界,在数据进入共享契约
ImuData后、进入异步队列前完成,是全链路中唯一叠加这两个系数的位置。 - L/M(停机保护):补偿后的数据交给异步层时才做生命周期检查;
save()/析构触发stop()后到达的消息被静默丢弃,保证不与退出时序竞争。
主要文件
src/asuka/src/asuka/asuka_ros.cpp:insert_imu(L59-70)、handle_loop_back(L171-183)、参数读取(L37-38)全部在此,是本页的唯一核心实现文件。src/asuka/include/asuka/asuka_ros.hpp:声明acc_scale/imu_time_offset/last_timestamp_imu三个成员状态(L52-58),类型即文档。src/asuka/include/asuka/core/types.hpp:ImuData结构(L20-27)是 ROS 层与算法插件之间的数据契约,只有时间戳加两个三维向量,刻意保持精简。src/asuka/src/asuka/core/async_odometry_estimation.cpp:队列接收端与消费循环(逐条取出、先发射输入槽再喂算法)。src/asuka/apps/asuka_rosnode.cpp、src/asuka/apps/asuka_nodelet.cpp:两种运行形态的订阅转发端。
边界条件
- 参数缺省即直通:
acc_scale默认 1.0、imu_time_offset默认 0.0,配置缺失时本层退化为纯类型转换,不会引入意外畸变。 - 回环检测不拦截:告警后数据照常入队。若下游算法对时间戳单调性有硬假设,需在算法自身的
imu_preprocess中处理,本层不兜底。 - 检测游标用原始时间戳:如上所述,
last_timestamp_imu记录的是补偿前的值;若把补偿挪到检测之前,rosbag 循环场景下的告警阈值语义会随参数漂移。 - 停机后消息静默丢弃:
running=false或end_of_sequence=true时队列端直接 return,这是退出时序安全的一部分。 - 无滤波、无外点剔除:本层不做任何信号质量判断,那些职责全部归属各算法目录下体量差异巨大的
imu_preprocess实现(FastLIO 版 8.7KB 最重,SmallPointLIO 版仅 0.7KB),ROS 层保持薄封装。 - 共享所有权交接:
ImuData::Ptr以ConstPtr形式进入队列与算法,算法层只能读不能改,补偿结果不可被下游篡改。
扩展点
- 新增标定项:若要引入陀螺尺度、轴间非正交、温度漂移等修正,按现有模式应在
insert_imu中handle_loop_back之后、async->insert_imu之前(即 L64-68 区间)插入,参数经config_ros.json的"ros"组传入并在构造函数(L34-40 附近)读取。 - 上游改造不影响下游:由于算法只依赖
ImuData::ConstPtr契约,未来把 IMU 接入改成 nodelet 内聚合、或多 IMU 融合,只要最终产出补偿后的ImuData,五种算法插件零改动。 - 反向扩展需动契约:若有算法需要未补偿的原始数据(例如自管时间同步),当前“ROS 层交付已补偿数据”的约定不满足,需要扩展
ImuData字段或调整OdometryEstimation接口,属于跨层契约变更而非局部修改。
Sources:
- src/asuka/src/asuka/asuka_ros.cpp#L59-L70
- src/asuka/include/asuka/asuka_ros.hpp#L29-L64
- src/asuka/include/asuka/core/types.hpp#L18-L57
- src/asuka/src/asuka/core/async_odometry_estimation.cpp#L17-L83
- src/asuka/apps/asuka_rosnode.cpp#L14-L28
- src/asuka/apps/asuka_nodelet.cpp#L61-L76
关联源码
src/asuka/src/asuka/asuka_ros.cpp