论文复现之——《AirIO: Learning Inertial Odometry with Enhanced IMU Feature Observability》

2025-02-13

引言

之前博客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即可实现稳定的状态估计

论文的贡献点如下:

  1. 对于learning-based IO而言,bodyframe representation is more expressive and observable(一般的做法则是转换到global world frame或者从原始的IMU数据中去掉重力)
  2. AirIO显式编码姿态(pose)信息,并且将其与body-frame IMU data融合来估算body frame的速度以及对应的uncertainty。(这部分就是所谓的AirIO motion network)
  3. 整合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。

但从实验结果中可以看到,所谓的generalizability只是只没有用于训练的而已hhh

但是从开源的代码中可以发现,对于测试的三个数据集,都需要分别训练AirIO network model(估算速度用的)以及AirIMU(对IMU进行滤波,且需要用其输出作为EKF的输入,因此整个模型看似应该是offline的)

左图为motion network;右图为full system

IMU Coordinate Frame

对于IMU实际测量量,加速度部分包括了物体自身运动带来的(usually aligned with object body frame)以及地球的重力,因此在timestamp i下所测量的加速度应该为:

Body Coordinate Frame vs. Global Coordinate Frame($\widehat{R}$i一般则是通过IMU预积分或者EKF得到,精度也不够)

接下来作者通过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

下载数据集

获取预训练模型

  • 作者提到要运行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
    }
]
MH_02_easy_state MH_04_difficult_state
V1_01_easy_state V1_03_difficult_state
V2_02_medium_state
  • 虽然看似有不少的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}      
  • 可视化结果如下
    1. EKF估算的轨迹 vs 真值轨迹
    2. EKF的bias
    3. 估算角度 vs 真值
    4. 估算位置(蓝色) vs 速度积分估算的位置(红色) vs 真值(绿色)
ekf_result bias
EKF_rot_orientation_compare EKF_vel
MH_02_easy
ekf_result bias
EKF_rot_orientation_compare EKF_vel
MH_04_difficult
ekf_result bias
EKF_rot_orientation_compare EKF_vel
V1_03_difficult
ekf_result bias
EKF_rot_orientation_compare EKF_vel
V2_02_medium
  • 最后的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
    }
]

参考资料