引言
之前博客Paper Survey之——Deep IMU-Bias Inference对基于learning的IMU odometry进行了调研及学习,并且也复现了AirIMU: Learning uncertainty propagation for inertial odometry工作对应IMU的补偿,发现效果非常惊艳(虽然泛化能力还有待提升) 最近AirIMU作者新出了工作《AirIO: Learning Inertial Odometry with Enhanced IMU Feature Observability》并且开源了,为此写下本博客记录学习与复现过程~
@article{qiu2025airio,
title={AirIO: Learning Inertial Odometry with Enhanced IMU Feature Observability},
author={Qiu, Yuheng and Xu, Can and Chen, Yutian and Zhao, Shibo and Geng, Junyi and Scherer, Sebastian},
journal={arXiv preprint arXiv:2501.15659},
year={2025}
}
论文学习
该工作是围绕着基于无人机的IO(inertial odometry),作者发现将原始IMU数据转换为全局坐标会破坏无人机关键运动学信息的可观察性。因此直击在body frame下进行IMU数据的特征学习(preserving the IMU feature representation in the body frame)。 通过结合他们的前作AirIMU(IMU correction model)和EKF,提出AirIO来实现无人机在剧烈飞行状态下的位姿估计,无需额外的sensor或者控制输入(比如thrust commands). EKF是用于整合IMU预积分以及所学习的IMU correction model的
从下面效果来看还是比较impressive的,仅仅依靠IMU即可实现稳定的状态估计
- IMO:《Learned Inertial Odometry for Autonomous Drone Racing》RAL2023
- RONIN:《Ronin: Robust neural inertial navigation in the wild: Benchmark, evaluations, & new methods》ICRA2020
论文的贡献点如下:
- 对于learning-based IO而言,bodyframe representation is more expressive and observable(一般的做法则是转换到global world frame或者从原始的IMU数据中去掉重力)
- AirIO显式编码姿态(pose)信息,并且将其与body-frame IMU data融合来估算body frame的速度以及对应的uncertainty。(这部分就是所谓的AirIO motion network)
- 整合uncertainty-aware IMU preintegration mode(应该就是前作AirIMU,可以估算IMU预积分及其uncertainty)与learned motion network(上面第二点)到EKF中来实现odometry estimation
而我个人认为更重要的poit是Our model also demonstrates generalizability to the unseen datasets that are not included in the training sets.
具备较好的泛化能力是很重要的,当然这里所谓的model应该不是指uncertainty-aware IMU preintegration mode或者learned motion network,而是指整个AirIO。

但是从开源的代码中可以发现,对于测试的三个数据集,都需要分别训练AirIO network model(估算速度用的)以及AirIMU(对IMU进行滤波,且需要用其输出作为EKF的输入,因此整个模型看似应该是offline的)
![]() |
![]() |
IMU Coordinate Frame
对于IMU实际测量量,加速度部分包括了物体自身运动带来的(usually aligned with object body frame)以及地球的重力,因此在timestamp i下所测量的加速度应该为:
![]() |
![]() |
接下来作者通过Principal Component Analysis和t-SNE一顿分析,证明了上述两个表达中,在body frame下的表达更好(过程就跳过了😀)
AirIO Motion Network & Training
在encoders,用CNN来提取body-frame IMU data (wB, aB)的特征以及飞机的orientation $\xi \in so(3)$. 然后将feature concatenate到一起,再用GRU layer来提取latent feature; 而在decoder,采用两个线性层来输出body-frame velocity and its uncertainty。整个网络的结构可以表达如下:

在训练中飞机的orientation有GT pose提供,而测试的时候则有EKF估算的
但从开源的代码中可以发现,对于测试的三个数据集,都需要分别训练AirIO network model(估算速度用的)以及AirIMU(对IMU进行滤波,且需要用其输出作为EKF的输入,因此整个模型看似应该是offline的)
PS:将飞机的orientation转换到so(3) Lie algebra space(更有利于网络的smoother gradient)
Extended Kalman Filter
上述的motion network仅仅是估算body frame的速度,而pose等其他状态则有EKF估算。 状态空间$X$定义如下

而error-state representation of the current filter state为:

采用AirIMU来滤掉IMU的噪声以及估算IMU sensor model的uncertainty

而EKF的传播模型则如下:

更新模型则是由 AirIO Motion Network估算的速度带来的,如下:

代码复现
- My AirIO_Comment
- 复现过程采用Euroc数据,作者在原文也提到用其中的5个序列来进行训练,而测试过程采用的序列为
MH_02_easy
,MH_04_difficult
,V1_03_difficult
,V2_02_medium
, 和V1_01_easy

配置安装
conda create -n airio python=3.10.11
# conda remove --name airio --all
conda activate airio
#同样依赖于pypose
# 安装系列依赖
pip install matplotlib==3.8.4
pip install numpy==2.2.2
pip install pyhocon==0.3.61
pip install pypose==0.6.8
pip install pyproj==3.7.0
pip install PyYAML==6.0.2
pip install scipy==1.15.1
pip install simplekml==1.3.6
# pip install torch==2.5.1 #重复安装了~
pip install tqdm==4.66.5
pip install wandb==0.19.4
下载数据集
- Euroc,路径在
/home/gwp/AirIMU/Euroc_dataset
- Blackbird dataset,路径在
/home/gwp/Air-IO/Blackbird_dataset
获取预训练模型
- 作者提到要运行EKF模式,是需要下载 AirIMU results的,它提供了IMU预积分以及uncertainty(这样来说相当于得先用AirIMU来处理数据,因此应该属于是offline的~)
- 而如果要用其他数据集,可能就是要重新训练AirIMU了~
Datasets | AirIO Pre-trained Models & Results | AirIMU Pre-trained Models & Results |
---|---|---|
EuRoC | AirIO Model | AirIMU |
Blackbird | AirIO Model | AirIMU |
Pegasus | AirIO Model | AirIMU |
NOTE: Each AirIMU Results pickle file contains raw IMU correction; Each Orientations pickle file contains two critical keys: airimu_rot for AirIMU-corrected orientation, inte_rot for raw IMU integrated orientation.
测试Euroc
- 网络输出会保存为
net_output.pickle
文件(路径在AirIO_EuRoC/AirIO_checkpoint/net_output.pickle
) - 记得修改
configs/datasets/EuRoC/Euroc_body.conf
中关于数据的路径~ configs/EuRoC/motion_body_rot.conf
中的[‘general’][‘exp_dir’]也要修改(这是模型的路径)- AirIO network supports three orientation input modes:
- Default: using Ground-truth orientation (no setup required)
- Switch modes: modify the rot_type and rot_path in the dataset config file configs/datasets/EuRoC/Euroc_body.conf. You can use AirIMU-corrected Orientation (rot_type: airimu) or raw IMU preintegration orientation (rot_type: integration). 这部分的操作则是需要AirIMU的输出结果
- Download precomputed rotation files (e.g. orientation_output.pickle) from the Download Datasets section and update the rot_path.
python inference_motion.py --config configs/EuRoC/motion_body_rot.conf
Evaluate & Visualize Network Predictions
- 通过下面命令来验证网络对于motion的估算效果并且画出轨迹
# 注意:删除反斜杠后的空格,确保每行以 \ 结尾且没有其他字符:
python evaluation/evaluate_motion.py \
--dataconf configs/datasets/EuRoC/Euroc_body.conf \
--exp AirIO_EuRoC/AirIO_checkpoint \
--savedir ./result/loss_result \
--seqlen 1000
# --seq is the segment length (in frames) for RTE calculation
# --exp is the Path for AirIO netoutput
# 下面两种都会出现报错的情况,还未fix
# `AirIO_EuRoC/network_output_using_airimuRot/net_output.pickle`中有用airimu作为rotation的网络输出
python evaluation/evaluate_motion.py \
--dataconf configs/datasets/EuRoC/Euroc_body.conf \
--exp AirIO_EuRoC/network_output_using_airimuRot \
--savedir ./result/loss_result_airimuRot \
--seqlen 1000
# `AirIO_EuRoC/network_output_using_gtRot/net_output.pickle`中有用GT作为rotation的网络输出
python evaluation/evaluate_motion.py \
--dataconf configs/datasets/EuRoC/Euroc_body.conf \
--exp AirIO_EuRoC/network_output_using_gtRot \
--savedir ./result/loss_result_gtRot \
--seqlen 1000
输出的结果如下:
[
{
"name": "MH_02_easy",
"ATE": 2.530659436584817,
"AVE": 0.18877704889692698,
"RP_RMSE": 0.972156050539075,
"Integration Method": "==============Integration==============",
"Integration pos_err": 35431.88813105013,
"Integration vel_err": 9.474668063515825,
"AirIO Method": "==============AirIO==============",
"AirIO pos_err": 2.2516072971978067,
"AirIO vel_err": 0.807469868411387
},
{
"name": "MH_04_difficult",
"ATE": 2.2503146961385756,
"AVE": 0.22445296159494285,
"RP_RMSE": 1.0092401639709976,
"Integration Method": "==============Integration==============",
"Integration pos_err": 17034.383004241186,
"Integration vel_err": 9.248072741739545,
"AirIO Method": "==============AirIO==============",
"AirIO pos_err": 2.111607675401745,
"AirIO vel_err": 0.8878740819530951
},
{
"name": "V1_03_difficult",
"ATE": 3.1073632144485637,
"AVE": 0.38263154057907856,
"RP_RMSE": 1.511648049992939,
"Integration Method": "==============Integration==============",
"Integration pos_err": 16898.100169538437,
"Integration vel_err": 8.73100262917092,
"AirIO Method": "==============AirIO==============",
"AirIO pos_err": 2.672330765766064,
"AirIO vel_err": 1.3228342084701386
},
{
"name": "V2_02_medium",
"ATE": 4.309661365752476,
"AVE": 0.33851355967935326,
"RP_RMSE": 1.263009365746855,
"Integration Method": "==============Integration==============",
"Integration pos_err": 11761.063571576806,
"Integration vel_err": 8.452118708802605,
"AirIO Method": "==============AirIO==============",
"AirIO pos_err": 3.5996171944071746,
"AirIO vel_err": 1.1390835097147682
},
{
"name": "V1_01_easy",
"ATE": 6.52871041920713,
"AVE": 0.2774450150679226,
"RP_RMSE": 1.2017184870731785,
"Integration Method": "==============Integration==============",
"Integration pos_err": 24396.605673957074,
"Integration vel_err": 10.97922060906811,
"AirIO Method": "==============AirIO==============",
"AirIO pos_err": 5.919706406806912,
"AirIO vel_err": 1.0943105537291968
}
]
![]() |
![]() |
![]() |
![]() |

- 虽然看似有不少的drift,但是跟IMU预积分比起来少很多,且此处仅仅是用了AirIO的motion network而已(应该是用速度积分计算轨迹的)
- 上图应该主要对比的是估算的速度的情况
运行EKF
- EKF的结果会保存为
${SEQUENCE_NAME}_ekf_poses.npy
and${SEQUENCE_NAME}_ekf_results.npy
python EKF/IMUofflinerunner.py \
--dataconf configs/datasets/EuRoC/Euroc_body.conf \
--exp AirIO_EuRoC/network_output_using_gtRot \
--airimu_exp AirIMU_EuRoC \
--savedir ./EKFresult/loss_result
# --airimu_exp ${AirIMU_RESULT_PATH}
- 可视化结果如下
- EKF估算的轨迹 vs 真值轨迹
- EKF的bias
- 估算角度 vs 真值
- 估算位置(蓝色) vs 速度积分估算的位置(红色) vs 真值(绿色)
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
- 最后的V1_01_easy报错如下:
odict_keys(['mode', 'coordinate', 'rot_type', 'rot_path', 'data_list', 'gravity'])
ConfigTree([('name', 'Euroc'), ('window_size', 1000), ('step_size', 1000), ('data_root', '/home/gwp/AirIMU/Euroc_dataset'), ('data_drive', ['V1_01_easy'])])
/home/gwp/AirIMU/Euroc_dataset V1_01_easy
loaded: /home/gwp/AirIMU/Euroc_dataset, interpolate: True, gravity: 9.81007
Traceback (most recent call last):
File "/home/gwp/Air-IO/EKF/IMUofflinerunner.py", line 174, in <module>
dataset_inf = SeqInfDataset(data_conf.data_root, data_name, inference_state, device = args.device, name = data_conf.name,duration=1, step_size=1, drop_last=False, conf = dataset_conf)
File "/home/gwp/Air-IO/datasets/dataset.py", line 130, in __init__
self.data["acc"][:-1] += inference_state["correction_acc"][:, time_cut:].cpu()[0]
RuntimeError: The size of tensor a (28711) must match the size of tensor b (28940) at non-singleton dimension 0
向作者提问,得到的答复是说他们采用的序列是openvins重新校正的V101数据,而我测试的是原版的Euroc数据,数据有差异~~~Github Issue
Evaluate & Visualize EKF Results
python evaluation/evaluate_ekf.py \
--dataconf configs/datasets/EuRoC/Euroc_body.conf \
--exp EKFresult/loss_result \
--savedir ./result/loss_result_ekf \
--seqlen 1000
[
{
"name": "MH_02_easy",
"ATE(EKF)": 2.4776350567640453,
"RTE(EKF)": 0.8392048606301833,
"RP_RMSE(EKF)": 0.986514639276038
},
{
"name": "MH_04_difficult",
"ATE(EKF)": 2.3080264050205286,
"RTE(EKF)": 0.8697517581648648,
"RP_RMSE(EKF)": 1.0046212473704987
},
{
"name": "V1_03_difficult",
"ATE(EKF)": 3.050263789492288,
"RTE(EKF)": 1.3646776743637254,
"RP_RMSE(EKF)": 1.5521645175050485
},
{
"name": "V2_02_medium",
"ATE(EKF)": 4.20582509824693,
"RTE(EKF)": 1.1778671729026637,
"RP_RMSE(EKF)": 1.3132037593617787
}
]