简介:这份资源是面向视觉SLAM初学者与进阶学习者的VIO课程配套资料包,围绕视觉惯性里程计的核心原理与工程实现展开,帮助读者建立从理论到实践的完整认知。内容覆盖视觉SLAM基础、IMU预积分、传感器融合、状态估计与后端优化等关键环节,并配有课程作业与讲评材料,适合希望系统入门VIO、补齐多传感器融合知识短板的学生和开发者。资源包共88个文件,以22个pdf讲义与作业讲解、12个h头文件、11个cpp源码、10个zip作业压缩包为主,另含py脚本、cmake构建文件、md说明及少量数据与配置文件,整体约53.98MB,结构清晰便于按章节检索。目前已有360人学习下载。通过讲义、作业与代码的配合,读者可逐步理解相机与IMU时间戳同步、滑动窗口优化、初始化等难点,并借助作业讲评与优秀案例复盘实现思路,为后续深入研究或项目落地打下基础。
1. 视觉惯性里程计入门:从「跑不起来」到「跑得稳」的第一道坎
视觉惯性里程计(VIO)是机器人、无人机、AR/VR 里绕不开的定位技术。它把相机和 IMU 的数据揉在一起,靠视觉提供环境结构、靠惯性提供高频运动先验,最终输出 6DoF 位姿。很多人第一次接触 VIO 课程入门,卡住的地方不是数学,而是环境——代码拉下来,编译报错,数据集跑不通,轨迹飘得没法看。这篇笔记面向刚入门的工程师和学生,把 VIO 从概念到跑通再到调参的路径拆开讲。我不会假设你手里有某份特定源码,而是按这个方向最常见的工程做法,把每一步该做什么、参数怎么设、哪里容易翻车说清楚。读完你应该能自己搭起一条最小可用的 VIO 验证链路,并且知道轨迹飘了该往哪查。
2. VIO 到底在算什么:状态估计的骨架与传感器分工
2.1 为什么单靠视觉或单靠 IMU 都不够
视觉的问题在于它输出的是「相对」信息。特征点匹配给出的是帧间约束,尺度在单目下不可观,快速运动时特征跟踪直接断掉,纹理缺失的墙面让整个前端哑火。IMU 的问题反过来:它输出高频的角速度和加速度,积分一次得姿态、两次得位置,但零偏和噪声在积分里累积,几秒钟就能漂出几十米。两者互补的逻辑很直接——视觉在慢速、纹理丰富时修正 IMU 的漂移,IMU 在快速运动、视觉短暂失效时撑住姿态估计。
从状态估计的角度看,VIO 要估计的状态量通常包括:当前位姿(位置 + 姿态)、速度、IMU 零偏(加速度计零偏和陀螺仪零偏),有些实现还会把相机-IMU 外参和重力方向放进状态里一起优化。这个状态向量的维度决定了优化问题的规模,也决定了实时性能不能扛住。
提示:入门阶段不要一上来就啃 VINS-Mono 的因子图推导,先把「状态量有哪些、每个量由谁观测、观测方程长什么样」这三件事理清楚,后面看代码会顺很多。
2.2 松耦合与紧耦合:选型的第一道分叉
松耦合的做法是视觉和 IMU 各自算各自的位姿,再用滤波或优化把两个结果融合。优点是模块清晰、调试容易,视觉挂了 IMU 还能单独输出。缺点是丢失了原始观测之间的相关性,精度上限受限。紧耦合把视觉重投影误差和 IMU 预积分残差放进同一个优化目标里联合求解,精度更高、对快速运动的鲁棒性更好,但实现复杂度上一个台阶。
入门课程里常见的路径是:先跑通松耦合的 EKF 方案理解融合逻辑,再过渡到紧耦合的滑动窗口优化。如果你用的是开源框架,判断它属于哪一类,看它的残差项里有没有 IMU 预积分因子和视觉重投影因子同时出现。
2.3 最小可跑通的工程链路
一条能验证的 VIO 链路通常包含这几个环节:数据读取(图像 + IMU 时间戳对齐)、前端特征跟踪、IMU 预积分、初始化(估计重力、尺度、零偏、速度)、后端滑动窗口优化、边缘化。下面用一段伪代码把主循环的骨架写出来,重点是让你看清数据流,而不是直接能编译。
# VIO 主循环骨架(伪代码,用于理解数据流) # 状态: [位置, 姿态, 速度, 加速度计零偏, 陀螺仪零偏] state = initialize_state() for k in range(num_frames): img = get_image(k) imu_batch = get_imu_between(prev_time, curr_time) # 1. IMU 预积分:把两帧之间的 IMU 数据压成相对约束 imu_delta = preintegrate(imu_batch, state.bias) # 2. 前端:提取并跟踪特征点 features = track_features(img, prev_img) # 3. 初始化:前若干帧用来估计重力方向、尺度和零偏 if k < init_frames: state = try_initialize(state, imu_delta, features) continue # 4. 后端:联合优化视觉重投影 + IMU 预积分残差 state = optimize(state, features, imu_delta) # 5. 边缘化:滑出窗口的帧不能直接丢,要保留先验信息 state = marginalize(state)这段骨架里,preintegrate是关键。IMU 预积分的意义在于:两帧图像之间的 IMU 采样可能有几十上百条,逐条放进优化不现实,预积分把它们压成一个相对运动约束,并且允许在零偏更新时用一阶近似修正,避免重新积分。optimize里视觉残差和 IMU 残差的权重需要平衡,这个后面调参章节会讲。marginalize是很多人忽略的一步,滑动窗口滑出的帧如果直接扔掉,历史信息就断了,正确做法是通过舒尔补保留成先验因子。
3. 把 VIO 跑起来:环境、数据与第一个轨迹
3.1 环境搭建的取舍
VIO 依赖的库通常包括 Eigen(线性代数)、Ceres 或 G2O(优化)、OpenCV(图像处理)、ROS(数据通信,可选)。版本冲突是新手第一道血泪经验。常见翻车点是 Ceres 和 Eigen 的版本不匹配导致编译期模板报错,或者 OpenCV 4.x 的 API 变动让老代码里的CV_LOAD_IMAGE_GRAYSCALE直接找不到。
我一般会建议用容器或虚拟环境把依赖锁死,而不是在系统 Python 和系统库上硬装。下面是一个典型的依赖安装流程,用 Ubuntu 环境举例:
# 安装基础依赖(版本仅作示例,按你实际框架的 README 调整) sudo apt-get install -y cmake build-essential libeigen3-dev # Ceres 依赖 sudo apt-get install -y libgoogle-glog-dev libgflags-dev \ libatlas-base-dev libsuitesparse-dev # OpenCV(建议用系统包或源码编译,二选一,不要混用) sudo apt-get install -y libopencv-dev # 编译你的 VIO 工程 mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j4CMAKE_BUILD_TYPE=Release不能省。VIO 后端优化在 Debug 模式下可能慢十倍以上,实时性直接崩掉,很多人以为算法有问题,其实是编译选项没开优化。-j4按你机器的核数调整,内存不够时并行编译会 OOM。
3.2 数据集的选择与时间戳对齐
跑通 VIO 需要图像和 IMU 同步的数据。公开数据集里常见的有 EuRoC(无人机室内)、TUM VI(手持/车载)、KITTI(车载,偏视觉)。入门建议从 EuRoC 的 MH_01 序列开始,它的运动不算剧烈,纹理条件中等,适合验证初始化是否正常。
时间戳对齐是第二个高频翻车点。图像时间戳和 IMU 时间戳如果不在同一时钟域,或者 IMU 频率标称 200Hz 实际有丢包,预积分就会错位。检查方法是把 IMU 相邻采样时间差画出来,正常应该稳定在 5ms 附近,如果出现大跳变说明有丢帧。
# 检查 IMU 时间戳连续性(伪代码) import numpy as np ts = np.loadtxt("imu_timestamps.txt") dt = np.diff(ts) print("mean dt:", dt.mean(), "max dt:", dt.max()) # 正常: mean 约 0.005s, max 不应超过 0.02s # 异常: max 远大于 mean,说明存在丢帧或时间戳跳变 if dt.max() > 4 * dt.mean(): print("警告: IMU 存在明显丢帧,预积分精度会受影响")这段检查看着简单,但能帮你排除掉一大半「轨迹莫名其妙飘」的问题。数据本身有问题,后面调参调到天亮也没用。
3.3 第一个轨迹的评估方法
跑出轨迹后,怎么判断它好不好?绝对轨迹误差(ATE)是常用指标,把估计轨迹和真值对齐后算均方根误差。入门阶段不用追求厘米级,先看两件事:轨迹形状是否和真值大致重合,以及有没有明显的尺度漂移或突然跳变。
评估脚本通常需要做时间戳关联和 Sim3 对齐(单目需要,因为尺度不可观)。如果你跑的是双目或 RGB-D VIO,尺度可观,用 SE3 对齐即可。下面是一个简化的评估流程:
# 轨迹评估流程(伪代码) est = load_trajectory("estimated.txt") # 估计轨迹 gt = load_trajectory("groundtruth.txt") # 真值轨迹 # 1. 按时间戳关联,找最近邻匹配 matched = associate_by_timestamp(est, gt, max_diff=0.02) # 2. 对齐(单目用 Sim3,双目/深度用 SE3) aligned = align_sim3(matched.est, matched.gt) # 3. 计算 ATE ate = compute_ate(aligned.est, aligned.gt) print("ATE RMSE:", ate)max_diff=0.02是关联时间阈值,设太大会把错误匹配算进去,设太小会丢太多帧。EuRoC 这种同步良好的数据集,0.02s 是合理起点。
4. 参数怎么调:初始化、零偏与优化权重的实战设置
4.1 初始化阶段的三个关键量
VIO 初始化要解决的是:重力方向、尺度(单目)、IMU 零偏、初始速度。这几个量估不准,后面整个轨迹都是歪的。常见做法是用前若干帧的视觉结构做 SFM,再和 IMU 预积分对齐求重力方向和尺度。
初始化帧数不能太少,太少噪声大;也不能太多,太多浪费时间且运动可能已经变化。经验值是 1 到 2 秒的数据,对应 10 到 20 帧图像。如果初始化后轨迹立刻发散,先检查重力方向估计是否合理——重力模长应该接近 9.81,方向应该和 IMU 静止时的加速度读数一致。
| 参数 | 典型值 | 作用 | 调大/调小的后果 |
|---|---|---|---|
| 初始化帧数 | 10~20 | 用于 SFM 和 IMU 对齐 | 太小噪声大,太大延迟高 |
| 加速度计零偏随机游走 | 1e-3~1e-2 | 零偏变化速率约束 | 太大零偏乱跳,太小跟不上真实变化 |
| 陀螺仪零偏随机游走 | 1e-5~1e-4 | 同上 | 同上 |
| 重力模长先验 | 9.81 | 约束重力估计 | 偏离真实值会导致尺度估计错 |
4.2 优化权重的平衡
后端优化里,视觉残差和 IMU 残差的权重决定了谁说话更算数。视觉权重过大,快速运动时 IMU 约束被淹没,轨迹会跟着视觉噪声抖;IMU 权重过大,视觉修正不起作用,零偏漂移压不住。
权重的本质是噪声协方差。视觉重投影误差的协方差通常设 1 到 2 个像素,IMU 的噪声参数来自 Allan 方差标定或数据手册。如果你没有标定条件,用框架默认值起步,然后根据轨迹表现微调。判断方法:如果轨迹在慢速段就抖,可能是视觉权重太大;如果快速段漂移明显,可能是 IMU 权重太大或零偏估计不准。
# 优化权重配置示例(YAML 格式,字段名按你框架的实际定义) visual: reprojection_noise: 1.5 # 像素,视觉重投影噪声 imu: acc_noise: 0.08 # 加速度计噪声密度 gyr_noise: 0.004 # 陀螺仪噪声密度 acc_bias_noise: 0.005 # 加速度计零偏随机游走 gyr_bias_noise: 0.0005 # 陀螺仪零偏随机游走这些数值不是万能公式,不同 IMU 器件差异很大。低端 MEMS 的噪声密度可能是高端器件的十倍以上,直接套用会导致优化结果偏差很大。有条件的话做一次 Allan 方差标定,半小时的事,能省掉后面几天的瞎调。
4.3 滑动窗口与边缘化的参数
滑动窗口大小决定了优化里保留多少帧。窗口大,精度高但计算量大;窗口小,实时性好但历史信息丢失快。常见设置是 10 到 20 帧。边缘化时要注意,被边缘化的帧如果和当前帧有共同观测,先验因子的构建不能出错,否则会出现信息重复计数,表现为轨迹过度自信、协方差偏小。
注意:边缘化是 VIO 里最容易写出隐蔽 bug 的地方。如果你发现轨迹在窗口滑动时出现规律性的跳变,优先查边缘化部分的先验构建逻辑。
5. 避坑与排查:VIO 入门最常见的五类翻车
5.1 轨迹整体飘移,形状还对
现象:估计轨迹和真值形状相似,但整体偏移或旋转。原因:初始化时重力方向或外参估计不准,导致整个坐标系有偏差。解决:检查相机-IMU 外参标定,确认重力方向估计的模长和方向,必要时重新做初始化。外参标定建议用专门的标定工具,不要手填。
5.2 快速运动段轨迹断裂或跳变
现象:慢速段正常,一快速运动轨迹就飞。原因:视觉特征跟踪丢失,IMU 预积分独自支撑但零偏没估准,或者优化权重里视觉占比过大导致 IMU 约束被淹没。解决:检查前端特征跟踪数量,快速段特征数骤降是正常现象,但不应降到零;调整 IMU 权重,确认零偏估计收敛。
5.3 编译通过但运行时段错误
现象:程序启动后几秒内崩溃,无明确报错。原因:常见于 Eigen 内存对齐问题(结构体里包含固定大小 Eigen 类型但没加对齐宏)、或者多线程访问共享状态没加锁。解决:检查包含 Eigen 成员的结构体是否用了EIGEN_MAKE_ALIGNED_OPERATOR_NEW,检查后端优化和前端跟踪是否在不同线程里访问了同一份状态。
5.4 轨迹在静止时缓慢漂移
现象:设备不动,轨迹慢慢移动。原因:IMU 零偏没估准,或者视觉特征在静止时匹配到了噪声。解决:静止段是检验零偏估计的好时机,如果静止漂移明显,说明零偏收敛有问题;检查视觉前端在低纹理或重复纹理下是否产生了错误匹配。
5.5 评估 ATE 很大但肉眼看着还行
现象:轨迹可视化看着挺准,ATE 却很大。原因:时间戳关联错误,或者对齐方式选错(单目用了 SE3 而非 Sim3)。解决:检查关联的时间阈值,确认单双目类型和对齐方式匹配,可视化关联后的轨迹对,看是否有明显错位。
6. 从跑通到用好:验证思路与一个实用技巧
跑通第一个轨迹只是起点,真正要判断一套 VIO 方案值不值得深入,得看它在边界条件下的表现。我一般会做三组验证:第一组是正常序列,看 ATE 和轨迹形状;第二组是快速运动或弱纹理序列,看鲁棒性;第三组是长时间序列,看漂移累积速度。三组都过,才说明这套方案有实用价值。
一个容易被忽略但很实用的技巧是:把 IMU 预积分的残差单独打印出来看。正常收敛时,预积分残差应该在噪声水平附近波动;如果残差持续偏大,说明零偏估计或外参有问题;如果残差突然跳变,说明 IMU 数据有丢帧或时间戳异常。这个信号比看最终轨迹更早暴露问题。
# 监控 IMU 预积分残差(伪代码) residual = compute_preintegration_residual(state, imu_delta) # 正常: 残差在噪声协方差范围内波动 # 异常: 持续偏大 -> 零偏/外参问题; 突然跳变 -> 数据问题 if residual > 3 * expected_noise: print("预积分残差异常:", residual, "检查零偏和数据完整性")另一个习惯是:每次改完参数,不要只看最终 ATE,把中间过程量(特征跟踪数、零偏估计值、优化迭代次数)一起记录。VIO 调参是个多变量问题,只看一个指标很容易陷入「按下葫芦浮起瓢」的循环。我自己踩过的最大坑就是盯着 ATE 调了一整天,最后发现是数据里有一段 IMU 丢帧,跟参数一点关系没有。先把数据质量确认干净,再动算法参数,这个顺序能帮你省下大量时间。希望帮到你。
本文还有配套的精品资源,点击获取