【论文导读】VINet: Visual-Inertial Odometry as a Sequence-to-Sequence Learning Problem

993 阅读17分钟

论文名称:VINet: Visual-Inertial Odometry as a Sequence-to-Sequence Learning Problem

开源项目地址:github.com/HTLife/VINe…

期刊:AAAI

时间:2017

提出了一种端到端可训练的深度学习框架,用于解决单目视觉-惯性里程计(VIO)问题

即将视觉(RGB 图像)惯性测量单元(IMU)数据融合,估计相机/机器人/无人机的运动轨迹

VINet 是第一个端到端可训练的视觉-惯性里程计系统,使用CNN+LSTM结构在EuRoC和KITTI数据集上达到了与传统优化方法相当甚至更好的精度,同时具备对标定误差和时间同步误差的鲁棒性

所用数据集

EuRoC MAV Dataset数据集,室内无人机类型,传感器为 IMU + 单目相机,高精度 Vicon 地面真值

projects.asl.ethz.ch/datasets/do…

KITTI Odometry Dataset数据集,户外驾驶类型,传感器为 IMU + 单目相机,用于自动驾驶场景评估

www.cvlibs.net/datasets/ki…

数据均非自己采集

主要工作

任务为视觉-惯性里程计(VIO)

输入为单目RGB图像+IMU数据(角速度、加速度),输出为相机/设备的6自由度位姿(3D平移+四元数旋转)

方法:使用端到端的序列到序列学习模型(CNN + LSTM) ,称为 VINet

创新点:

  • 首次提出端到端可训练的VIO系统
  • 引入SE(3) 流形上的可微分层处理旋转和平移
  • 支持多频率输入(IMU 100Hz,图像 10~20Hz)
  • 无需手动标定相机与IMU之间的外参和时间同步
  • 可通过训练增强对传感器标定误差的鲁棒性

大方向为视觉-惯性融合导航(VIO),预测类型为长期轨迹预测(整个序列的位姿)

模型类型为序列到序列回归模型(CNN+LSTM),支持端到端,支持实时(10~20Hz图像频率,100Hz IMU)

深度学习模型结构

原始传感器输入(网络入口)

单目传感器RGB,频率为10-20Hz,单帧原始维度3×H×W,EuRoC:3×480×752;KITTI:3×370×1226

IMU频率100-200Hz,单帧原始维度6×1,3-axis acc + 3-axis gyro,已归一化

训练时采用「滑动窗口 + 状态 carry」方式,窗口长度 T ≈ 8-16 帧图像(对应 80-160 个 IMU 采样)。

Visual-CNN 模块

帧间运动特征提取,目的是把「两张连续 RGB」映射成一条低维「视觉运动向量」

Visual−CNN 并不是 VINet 自己重新设计的“新架构”,而是直接裁剪自 FlowNet-S(Dosovitskiy 等人 2015 年提出的光流网络)。作者保留它“ encoder 部分”,也就是能把两张连续 RGB​ 映射成“运动敏感张量” 的所有卷积层,砍掉后面专门上采样回稠密光流的后半段

补充说明

kernel 中文里叫卷积核/滤波器,其本质为一个小的权重矩阵

  • 比如 conv1 的参数 kernel-size =7×7,每个通道对应 7×7 个可学习权重

    此时输入有 6 个通道,则一个完整的 kernel 形状是 (64, 6, 7, 7)​

    • 64 表示“要输出 64 张特征图”,也称 输出通道数 C_out
    • 6 表示“与输入 6 通道逐一做内积”,也称 输入通道数 C_in
    • 7×7 就是空间上的“小窗口”

stride 为步长,表示 kernel 窗口在输入平面上“滑动的步长”

  • conv1 中 stide=2,表示横向、纵向都隔 2 像素取样一次

    • 就会造成 480 → 240,752 → 376(正好减半)
 H_out = (H_in + 2×pad − kernel_size) // stride + 1

pad 即为 padding​ 填充

控制输出空间尺寸,同时保证边界像素也被充分利用

  • conv1:pad = 3 ⇒ 上下左右各补 3 圈 0

代入上式:(480 + 2×3 − 7)/2 + 1 = 240

如果不 pad,输出会是 237,尺寸“不整齐”且边界信息丢失多

一个卷积层权重矩阵形状 (C_out, C_in, kH, kW) ,另外每个输出通道还要1个bias

 Params = (C_in × kH × kW + 1) × C_out
  • conv1 代入得到 = (6 × 7 × 7 + 1) × 64 = (294 + 1) × 64 = 18 880

每个输出通道共享同一组 (C_in×kH×kW) 个权重——所谓“权值共享”

所以不管输入图像有多大,参数量只取决于 (kernel 面积 × 输入通道 + 1) × 输出通道,与空间尺寸无关

输入

输入为一对时间相邻的RGB帧,I_t, I_{t+1} ∈ ℝ^{(3×H×W)},先沿 channel 维拼接,得到 6×H×W

所有卷积后面都接ReLU,所有 stride=2 的 conv 同时完成“降采样 + 通道升维”

省略 BatchNorm 细节(原 FlowNet 没有 BN,VINet 也未加)

conv1

首层降采样+大感受野

  • 输入为 6×480×752,kernel 为 7×7, stride=2, pad=3,输出通道 64,输出尺寸为 64×240×376
  • 参数量为 (7×7×6)×64+64=18,880

把“双图拼接”后的 6 通道图像一次性压到 64 维,同时降采样 1/2减轻后续计算量;7×7 大 kernel​ 直接给出较大运动范围的感受野

感受野表示特征图上的一个点在原始输入图像上能“看到”的区域大小

  • 第一层就用 7×7,意味着 conv1 输出中的一个像素已经融合了输入图 7×7 邻域的信息
  • 后续再经过 stride =2 的层层降采样,到 conv6 时一个特征点对应原图 大于 200×200 的像素
  • 对于光流/ego-motion 任务,大位移运动需要大感受野才能被“一次性”捕获,而小 3×3 要靠很多层才能攒出同样视野(7×7 是“一步到位”)

conv2

  • 输入为 640×240×376,kernel 为 5×5, stride=2, pad=2,输出通道 128,输出尺寸 128×120×188
  • 参数量为 (5×5×64)×128 + 128 = 204,928

进一步缩小空间提高语义级别;5×5 kernel 保持适中运动范围。

conv3 → conv3_1

(两个连续 3×3,通道翻倍)

  • conv3 参数 128×120×188 → 256×60×94 (stride=2),参数量 3×3×128×256 + 256 = 295,168
  • conv3_1 参数 256×60×94 → same (stride=1),参数量 3×3×256×256 + 256 = 590,080

stride=2 负责再降 1/2;conv3_1 是“same-resolution” refinement,增强非线性表达能力

conv4 → conv4_1

(重复套路,512 维)

  • conv4 参数 256×60×94 → 512×30×47 (stride=2),参数量 3×3×256×512 + 512 = 1,180,160
  • conv4_1 参数 512×30×47 → same (stride=1),参数量 3×3×512×512 + 512 = 2,359,808

空间降到 1/8 原尺寸,通道升至 512​,语义 / 运动抽象级别更高

conv5 → conv5_1

(进入 1024 维高语义)

  • conv5 参数 512×30×47 → 512×15×24 (stride=2),参数量 3×3×512×512 + 512 = 2,359,808
  • conv5_1 参数 512×15×24 → same,参数量 同上

最后一次 2 倍降采样;15×24 已接近“特征图”级别可粗略认为每个像素对应原图 32×32 区域

conv6

(得到 1024-d “运动向量”)

  • 输入参数 512×15×24,kernel 为 3×3, stride=2, pad=1,输出通道 1024,输出尺寸 1024×8×12,
  • 参数量 3×3×512×1024 + 1024 = 4,719,616

空间压到 8×12 ≈ 100 个 cell,每个 cell 感受野 ≈ 1/8 图像

通道升至 1024​,形成高维、大感受野的“帧间运动描述子”

压平 → 视觉特征向量

AdaptiveAvgPool2d(1)​ 或直接 .view(1024,-1).mean(-1)

conv6 输出1024×8×12,要把它变成固定长度1024维向量,才能和IMU向量拼在一起

有两种等价的方法:

  • AdaptiveAvgPool2d(1)

    官方Pytorch写法,不管输入空间是8×12还是5×9,强制做全局平均池化,输出尺寸 = (1024, 1, 1)

    再 .squeeze() → 1024

  • 手工一行代码

x = x.view(1024, -1)          #  reshape 成 [1024, 96]
x = x.mean(dim=-1)            #  对最后一维求平均 → [1024]

效果与 AdaptiveAvgPool 完全一致,但更直观:把每个通道的所有空间位置取平均,得到通道级全局特征

  • 消除空间尺寸不确定性,输出维度永远 = C_out
  • 运算无参数、可微分,可端到端训练
  • 对微小平移不敏感,起到平移鲁棒的作用

参数 1024×8×12 → 1024×1

再经 fc(1024→1024)(FlowNet 原实现没有,VINet 加了一层线性,参数 1024×1024+1024≈1M)

最终输出 v_{vis} ∈ ℝ^{(1024×1)} —— 这就是送入 Core-LSTM​ 的“视觉运动线索

推理阶段只跑前向,且只需到 conv6;原 FlowNet 后面还有 4 级 upsample/refine 层(总计 38 M)被全部砍掉,因此 VINet 实际视觉前端比完整 FlowNet 轻量约 40%

各步骤在 VINet 中的功能角色

  1. 逐级降采样:把像素级光流任务转成“高层运动语义”任务,降低计算
  2. 通道扩增补偿空间信息损失,获得足够判别维度。
  3. 最后 1024-d 向量:与 IMU-LSTM 的 1000-d 向量长度接近,便于 Core-LSTM 拼接融合
  4. 端到端可微:所有 conv/fc 均参与 BPTT,视觉权重会根据 pose-loss 自动调整——网络自己学会“提取对 ego-motion 最有用的那部分光流 / 结构 / 边缘特征”

IMU LSTM模块

IMU-LSTM 模块负责高频惯性数据(通常 100–200 Hz)压缩成与图像帧同步的单一特征向量,再交给 Core-LSTM 做跨模态融合

  • EuRoC 数据集为例(IMU 200 Hz,图像 20 Hz)逐步拆解

原始输入数据使用IMU传感器,200Hz频率,单帧维度维6×1,表示3轴加速度+3轴角速度

一个图像周期(0.05s)内共有K=10个IMU采样,把这段10步序列记作 U=[u₁, u₂, …, u₁₀]∈ℝ^{(6×10)}

数据缓冲与对齐子模块(无参数)

输入为实时IMU缓存队列,输出为滑动窗口 U ∈ ℝ^{(6×10)}(每次移1帧图像)

  • 保证图像帧 t 到来时,网路拿到的是 (t-0.05 s → t]​ 的惯性数据;
  • 后续所有计算都以固定长度10步为输入,便于批训练

线性嵌入层

(Embedding FC)

  • 输入为单步 u_{k} ∈ ℝ^6,输出 x_k ∈ ℝ^{32}(论文隐藏维=32,官方代码也可配置为 64/128​)

运算为 x_k = W_{emb} · u_k + b_{emb},W ∈ ℝ^{(32×6)}

把 6 维物理量映射到更高维隐空间,让网络自由学习“惯性特征” 而非受限于原始加速度/角速度刻度

参数量 32×6+32 = 224

2 层 LSTM 编码器

(核心可训练部分)

层数为 2,隐藏节点 =1000(与 Visual-CNN 输出维度 1024 接近,便于拼接)

方向仅正向(只沿时间正序,足够捕捉短期动力学)

  • t=1,输入 x_{1}, (h_{0}, c_{0}),维度 x_{1}∈ℝ^{32}, h_{0}=c_{0}=zeros(1000),数据来自于嵌入层 / 零初始化
  • t=2…10,输入 x_k, (h_{k-1}, c_{k-1}),维度同上,数据来自于上一步隐状态

可训练参数(单层):4 个门,每门 (W_x, W_h, b) → (32×1000 + 1000×1000 + 1000) × 4 ≈ 4.13 M

双层 ×2 → 8.26 M(IMU 分支的主要参数量集中在这里)

状态抽取子模块(无参数)

输入第 10 步隐状态 h_{10} ∈ ℝ^{1000},输出 v_{imu} = h_{10}(直接复用,不再额外线性变换)

原因:

  • LSTM 最后隐状态已把整个 10 步动力学信息压缩
  • 维度 1000 与 Visual-CNN 的 1024 几乎对齐,后续 Core-LSTM 可直接拼接

Core-LSTM 模块

Core-LSTM 把视觉CNN给出的 1024-D 帧间运动线索、IMU-LSTM 给出的 1000-D 高频惯性线索、上一时刻自己的位姿估计(7-D 四元数+平移)拼成一个超大输入向量,然后用两层隐藏单元 1000 的 LSTM 完成:

  • 跨模态融合(视觉 + 惯性 + 位姿先验)
  • 时序动力学建模(加速度、角速度积分误差补偿)
  • 输出 SE(3) 位姿更新(帧间速度 ξ ∈ ℝ^6​)

把视觉、惯性、先验位姿拉到同一向量,让网络自己决定权重

输入张量准备(无参数)

  • Visual-CNN 来的张量 v_{vis},维度为 1024×1,已全局平均池化
  • IMU-LSTM 来的张量 v_{imu},维度为 1000×1,取最后隐状态 h_{10}
  • 上一时刻位姿 T_{t-1},维度 7×1,其为 四元数q(4)+平移 r(3)

拼接方式 x_t = [v_{vis}; v_{imu}; T_{t-1}] ∈ ℝ^{(1024+1000+7)} = 2031×1

位姿先验被重复利用,形成“输出-反馈”闭环,使网络能累积修正漂移

线性降维嵌入

(Optional FC,官方代码可旁路)

  • 输入 x_{t} ∈ ℝ^{2031},输出 \hat x_{t} ∈ ℝ^{512}(或 1024,可配置)
  • 运算 \hat x_{t} = W_{reduce} · x_{t} + b_{reduce}

把 2031 维超大向量压到 512/1024,减少 LSTM 参数量;让不同模型数值范围先做一次自适应缩放

参数量 512×2031+ 512 ≈ 1.04 M

若关闭该选项,则直接把 2031 喂 LSTM,下文按 dim = D = 2031​ 描述

2031 维太大,先乘个矩阵压到 512 维 → 减少参数量,顺便把不同模态数值尺度拉到同一量级

2 层 LSTM 融合编码器

(核心可训练部分)

层数 =2 (双层),隐藏节点 = 1000(与 IMU 分支一致,便于拼接),方向为单向(只沿时间正序)

  • t 时刻输入x_t, (h_{t-1}^{(1,2)}, c_{t-1}^{(1,2)}),维度x_t∈ℝ^D, 隐状态∈ℝ^{1000}​​,数据来源 上游拼接 / 上一时步

    i_t = σ(W_{xi}x_t + W_{hi}h_{t-1} + b_i)
    f_t = σ(W_{xf}x_t + W_{hf}h_{t-1} + b_f)
    o_t = σ(W_{xo}x_t + W_{ho}h_{t-1} + b_o)
    g_t = tanh(W_{xc}x_t + W_{hc}h_{t-1} + b_c)
    c_t = f_t ⊙ c_{t-1} + i_tg_t
    h_t = o_ttanh(c_t)
    

参数量(单层):4 × (D×1000 + 1000×1000 + 1000) = 4 × (2 031 000 + 1 000 000 + 1 000) ≈ 12.13 M

双层 ×2 → 24.26 M(占 VINet 总参数量 55 M​ 的近一半)

建模长期动力学,补偿积分漂移,融合多模态

双层 1000 节点 LSTM 对“压缩后的多模态序列”逐时刻更新隐状态 → 既融合视觉+惯性,又记住过去 N 帧的动力学,用来预测下一步该怎么动

帧间速度回归头

(se(3) 代数向量)

  • 输入为第二层隐状态 h_t^{(2)} ∈ ℝ^{1000},输出 ξ_t ∈ ℝ^6(3-D 线速度 v + 3-D 角速度 ω)

运算为 ξ_t = W_{vel}·h_t^{(2)} + b_{vel},W ∈ ℝ^{(6×1000)}

  • 把融合后的高维状态映射到李代数 se(3) ,无正交约束,可直接回归;
  • 后续通过指数映射得到 SE(3) 位姿增量

参数量:6×1000 + 6 = 6006​

无约束输出帧间速度,免掉 SO(3)​ 正交投影麻烦

把 1000 维隐状态再乘个矩阵 → 直接输出 6 个数(3 轴线速度 + 3 轴角速度),这叫 se(3) 代数不用管旋转矩阵正交不正交

SE(3) 可微分层

(无参数,仅矩阵运算)

  • 输入:ξ_t ∈ ℝ^6,输出:ΔT_t ∈ SE(3)(4×4 刚体矩阵)

两步完成:

  • ∧ 算子:ξ_t → 4×4 矩阵

    [ξ]^∧ = [ [ω]_×   v
                0     0 ] ∈ se(3)
    

    6 维向量 → 4×4 矩阵:前三列放“角速度反对称矩阵”,最后一列放“线速度”,得到李代数元素

  • 指数映射:ΔT_t = exp([ξ]^∧) ∈ SE(3)​

    对 4×4 矩阵做矩阵指数 → 得到 4×4 刚体变换矩阵 ΔT,满足旋转部分自动是 SO(3),平移部分也自动正确

反向传播 exp(·) 与矩阵乘法均为解析可微,Theano/TensorFlow 自定义 op 已实现梯度

位姿累积与反馈(无参数)

  • 输入:ΔT_t(4×4),上一时刻全局位姿 T_{t-1}(4×4),输出 当前全局位姿 T_t = T_{t-1} · ΔT_t
  • 再分解:把 T_t 转成 7-D 向量 [q; r] → 下一时刻重新喂回 Core-LSTM 输入,形成循环闭环

上一帧的全局位姿 T_{t-1} 左乘 ΔT → 得到当前全局位姿 T_t,完成“积分”

损失函数入口(训练阶段)

帧间 loss:L_{se}(3) = ∥v−v̂∥ + β∥ω−ω̂∥(用 ξ_t 与真值 ξ̂_t​)

  • 帧间 6 维速度对比真值 → 保证每一步别跑飞

全局 loss:L_{SE}(3) = ∥q−q̂∥ + β∥r−r̂∥(用 T_t 与真值 T̂_t​)

  • 全局 7 维位姿对比真值 → 保证整条轨迹不漂移

联合训练,先帧间后全局,防止局部极小;两个 loss 轮流加权,端到端反向传播,所有权重一起更新

SE(3) Concatenation Layer

SE(3) Concatenation Layer,自定义可微分层,将帧间位姿累积为全局轨迹,保持SE(3)结构约束

这一部分和上述 Core-LSTM 模块的 SE(3)可微分层、位姿累积与反馈 部分功能完全重叠

  • 从 ξ_t → 全局位姿 T_t 的整条链路只有一个,官方论文把它叫做 SE(3) Concatenation Layer,也常被口头说成“可微分 exp​ 层”或“几何累积层”

    内部顺序:

    ξ_t ──∧──► ξ^∧ ──exp──► ΔT_t ──左乘──► T_t ──分解──► 7-D pose_t
    

训练方式与损失

训练方式使用Backpropagation Through Time (BPTT),联合优化两个损失:

帧间位姿误差(se(3))、全局轨迹误差(SE(3))

VINet 的训练方式可以概括为 滑动窗口 BPTT + 双损失课程调度 ,它与普通 CNN 或单损失 LSTM 训练有 3 个显著不同:

  1. 长序列不一次展开到底,而是用窗口滑动 + 隐状态 carry 近似完整 BPTT;
  2. 同时优化 帧间速度 loss全局位姿 loss,且权重按课程表切换
  3. 训练集里主动注入标定误差(时间偏移、外参旋转),让网络自己学会鲁棒。

滑动窗口

问题是 EuRoC 一条轨迹 40 s,图像 20 Hz → 800 帧;每帧 480×752,显存无法一次展开 800 步

VINet 做法

  • 设定窗口长度 T = 8~16 帧图像(对应 80~160 个 IMU 步);
  • 每跑完一个窗口,只反向传播这 8~16 步的梯度,然后把两个 LSTM 的隐状态 (h, c) 直接 carry 到下一个窗口,作为初始状态继续训练;
  • 整个序列依次滑过去,近似得到“全序列 BPTT”的效果,但显存占用恒定在 T 步

模型性能对比

对比方法类型简介效果对比
OK-VIS传统优化型VIO基于非线性优化的关键帧VIO(Leutenegger et al.)VINet在标定误差下更鲁棒,精度相当
MSCKF滤波型VIO多状态约束卡尔曼滤波VINet在精度上略优,鲁棒性更强
LIBVISO2 + EKF视觉+IMU融合传统视觉里程计 + EKF融合IMUVINet在KITTI上翻译误差更低,旋转略差