1. 为什么我要从GPS转向视觉惯性导航
去年冬天我在一个地下停车场里调试一台巡检小车,GPS信号直接飘到了隔壁小区,定位跳变超过三十米,小车差点撞上消防栓。那次之后我就下定决心,把定位方案从单一GPS切换到双目相机+IMU的视觉惯性导航架构。实测下来,这套方案在室内、地下、楼宇之间这些GPS完全失效的场景里,定位精度能稳定在厘米级,而且成本比激光雷达方案低不少。
这篇文章我想把整套搭建过程完整地分享出来,包括硬件选型、双目相机标定、IMU和相机的时间同步、VIO算法的ROS配置,以及我在调试过程中踩过的那些坑。适合有ROS基础、想给自己的小车或机器人加上室内定位能力的朋友,也适合正在做SLAM相关项目、需要一套可复现方案的同学。哪怕你之前只玩过GPS导航,跟着走一遍也能把视觉惯性里程计跑起来。
核心思路其实不复杂:双目相机负责提供视觉特征和深度信息,IMU负责提供高频的运动先验,两者通过紧耦合的VIO算法融合,输出高频、低漂移的位姿估计。GPS在这里反而变成了可选项,只在室外开阔场景做全局校正用。下面我按实际搭建顺序,从原理到实操一步步拆。
2. 视觉惯性导航的整体设计与选型思路
2.1 为什么是双目+IMU而不是单目或RGB-D
先说为什么不用单目。单目VIO最大的问题是尺度不可观,也就是说你没法从单张图像序列里直接恢复出真实的物理尺度,必须靠IMU的加速度积分来估计,一旦IMU有偏置或者运动激励不足,尺度就会漂。双目相机天生有基线,三角测量直接给出深度,尺度是客观可测的,这一点在工程上省了太多事。
再说RGB-D。RGB-D在室内近距离确实好用,但它的红外结构光或ToF在强光下容易失效,室外基本废掉,而且有效距离通常只有3到5米。双目相机靠被动视觉,室内室外都能用,有效距离取决于基线和分辨率,我用的基线12厘米、分辨率1280x720的双目模组,有效深度能到15米左右,足够小车用了。
IMU的选择上,我建议至少用6轴IMU(三轴加速度+三轴陀螺仪),如果预算允许上9轴(加磁力计)更好,但磁力计在室内电机干扰下反而不靠谱,所以我的方案里磁力计只做辅助,不参与核心融合。IMU的核心价值在于它能在图像帧之间(通常10到30Hz)提供100到200Hz的高频运动估计,让VIO的输出频率和鲁棒性都上一个台阶。
2.2 紧耦合与松耦合的取舍
VIO算法分松耦合和紧耦合两大流派。松耦合是把视觉位姿和IMU位姿分别算出来再做加权融合,实现简单但精度一般,视觉丢失时容易崩。紧耦合是把视觉重投影误差和IMU预积分误差放在同一个优化框架里联合优化,精度高、鲁棒性强,但计算量大、实现复杂。
我最终选的是VINS-Fusion这套紧耦合方案,原因是它在ROS生态里成熟度高、社区活跃、支持双目+IMU组合,而且有现成的配置文件可以改。另一套可选的是OKVIS,精度也不错但配置起来更折腾。如果你只是想快速跑通,VINS-Fusion是更稳妥的起点。
2.3 硬件清单与预算参考
下面是我这套小车的实际配置,供你参考:
| 部件 | 型号/规格 | 大致价格 | 备注 |
|---|---|---|---|
| 双目相机 | 全局快门,基线12cm,1280x720@30fps | 800-1500元 | 必须全局快门,卷帘快门会糊 |
| IMU | 6轴,200Hz输出,带硬件时间戳 | 200-600元 | 最好和相机同源触发 |
| 主控 | 树莓派4B 8G 或 Jetson Nano | 500-1000元 | 跑VIO建议Jetson |
| 底盘 | 阿克曼或差速小车 | 500-2000元 | 带编码器更好 |
| 电源 | 12V锂电池+稳压模块 | 200元 | 注意IMU供电要干净 |
提示:双目相机一定要选全局快门的。我一开始图便宜用了卷帘快门的模组,小车一动图像就有果冻效应,特征点匹配直接崩,换了全局快门后问题消失。这个钱不能省。
3. 双目相机标定与IMU标定的实操细节
3.1 双目相机标定的完整流程
标定是整个系统精度的地基,标定不准后面全白搭。我用的是ROS官方的camera_calibration包,配合一张9x6、方格边长25mm的棋盘格。
第一步,先把棋盘格打印出来贴在硬纸板上,确保平整,翘边会导致角点检测误差。第二步,启动标定节点:
rosrun camera_calibration cameracalibrator.py \ --size 9x6 --square 0.025 \ right:=/stereo/right/image_raw \ left:=/stereo/left/image_raw \ right_camera:=/stereo/right \ left_camera:=/stereo/left第三步,移动棋盘格,让它在画面里覆盖上下左右远近各种姿态。X轴要覆盖满画面宽度,Y轴覆盖高度,Size要从小到大多个距离,Skew要有倾斜角度。四个进度条都变绿了再点CALIBRATE。
第四步,等标定完成后点SAVE,会在/tmp下生成一个压缩包,解压后得到left.yaml、right.yaml、ost.yaml等文件。把这些文件里的内参、畸变系数、外参提取出来,写进VINS的配置。
这里有个关键点:标定时的图像分辨率必须和实际运行时的分辨率一致。我试过用640x480标定然后跑1280x720,结果重投影误差直接翻倍。另外标定完一定要看ost.yaml里的epipolar error,正常应该在0.2像素以内,超过0.5就重新标。
3.2 IMU标定:别跳过这一步
很多人觉得IMU出厂就有参数,直接用就行,这是大坑。IMU的零偏和尺度因子会随温度和使用时间漂移,不标定的话VIO初始化阶段就会漂。
我用的是imu_utils配合code_utils这套工具。流程是:先把IMU静止放置两小时采集数据,然后用Allan方差分析出噪声密度和随机游走系数。具体操作:
roslaunch imu_utils my_imu.launch rosbag record /imu/data -O imu_calib.bag # 静止采集2小时后 roslaunch imu_utils imu_an.launch跑完会生成一个imu_param.yaml,里面有gyr_n、gyr_w、acc_n、acc_w四个关键参数,分别对应陀螺仪噪声、陀螺仪随机游走、加速度计噪声、加速度计随机游走。这四个值直接填进VINS配置的imu段。
注意:采集IMU数据时一定要放在稳固的桌面上,不能有震动。我有一次放在机箱旁边,风扇震动导致Allan方差曲线完全不对,白采了两小时。
3.3 相机和IMU的外参标定
相机和IMU之间的相对位姿(旋转+平移)叫外参,这个必须标,否则融合出来的轨迹会扭曲。标定工具推荐kalibr,它能同时标定相机内参、IMU参数和相机-IMU外参。
流程是:先录一段包含棋盘格和IMU数据的rosbag,然后跑:
kalibr_calibrate_imu_camera \ --bag calib.bag \ --cam camchain.yaml \ --imu imu.yaml \ --target aprilgrid.yaml录bag的时候要充分激励所有轴:绕X、Y、Z轴各转几圈,再平移晃动,让IMU的加速度和角速度都有变化。我一般录90秒左右,太短了标定不准,太长了计算慢。
标定完会输出camchain-imucam.yaml,里面的T_cam_imu就是外参矩阵。这个矩阵填进VINS配置的extrinsicRotation和extrinsicTranslation。
4. VINS-Fusion的ROS配置与跑通实录
4.1 环境准备与依赖安装
我用的系统是Ubuntu 20.04 + ROS Noetic,这是目前VINS-Fusion兼容性最好的组合。如果你用Ubuntu 22.04,ROS 2的生态还在完善,VINS-Fusion的ROS 2版本不如Noetic版稳定,建议先用Noetic。
依赖安装:
sudo apt-get install ros-noetic-cv-bridge ros-noetic-tf \ ros-noetic-image-transport ros-noetic-camera-info-manager \ libeigen3-dev libceres-devCeres Solver是VINS的核心优化库,版本建议2.0以上。如果apt里的版本太老,就源码编译:
git clone https://ceres-solver.googlesource.com/ceres-solver cd ceres-solver && mkdir build && cd build cmake .. && make -j4 && sudo make install然后编译VINS-Fusion:
cd ~/catkin_ws/src git clone https://github.com/HKUST-Aerial-Robotics/VINS-Fusion.git cd .. && catkin_make4.2 配置文件的关键参数解读
VINS-Fusion的配置文件在config/目录下,我以euroc_stereo_imu_config.yaml为模板改。几个必须改的地方:
imu_topic: "/imu/data" image0_topic: "/stereo/left/image_raw" image1_topic: "/stereo/right/image_raw" output_path: "/home/yourname/vins_output/" cam0_calib: "left.yaml" cam1_calib: "right.yaml" image_width: 1280 image_height: 720 # 外参 body_T_cam0: !!opencv-matrix rows: 4 cols: 4 dt: d data: [ ... ] # 从kalibr结果填 # IMU参数 acc_n: 0.01 gyr_n: 0.001 acc_w: 0.0001 gyr_w: 0.00001acc_n和gyr_n就是前面Allan方差标出来的噪声密度,acc_w和gyr_w是随机游走。这几个值填错会导致初始化失败或者轨迹漂移。
还有一个容易忽略的参数是estimate_extrinsic,如果外参标定得准就设为0(不优化),如果不太准就设为1(在线优化)。我建议先设0,跑通了再考虑要不要开在线优化。
4.3 启动与数据回放
启动VINS:
roslaunch vins vins_rviz.launch rosrun vins vins_node ~/catkin_ws/src/VINS-Fusion/config/my_config.yaml然后回放你录的bag:
rosbag play -r 0.5 your_data.bag-r 0.5是半速回放,给VIO算法足够的计算时间。如果你的主控性能强,可以1倍速甚至2倍速。
跑起来后RViz里应该能看到:特征点跟踪的绿色点、当前位姿的坐标系、以及累积的轨迹线。如果轨迹线平滑且闭合误差小,说明配置对了。
4.4 从VIO到完整导航的衔接
VIO输出的是/vins_estimator/odometry话题,类型是nav_msgs/Odometry。要接到导航栈里,需要把它转成tf和odom:
<node pkg="tf" type="static_transform_publisher" name="vins_to_base" args="0 0 0 0 0 0 /vins_world /odom 100" />然后在move_base的配置里把odom话题指向VIO输出。这样路径规划和避障就能用上视觉惯性里程计了。
提示:VIO的坐标系原点是初始化时刻的相机位置,每次启动都不一样。如果需要全局一致的坐标,得配合回环检测或者预先建好的地图做重定位。
5. 常见问题排查与避坑经验
5.1 初始化失败的几种典型情况
VINS初始化失败是最常见的问题,表现是RViz里一直不出轨迹,终端报initialization failed。原因通常有三个:
一是运动激励不足。VINS需要足够的加速度和角速度变化才能估计出尺度和重力方向。解决办法是启动后让小车先走一段S形或者转几个弯,别一上来就直线匀速。
二是IMU和相机时间不同步。如果IMU时间戳和图像时间戳差超过10毫秒,初始化就会飘。检查方法是看/imu/data和/stereo/left/image_raw的header.stamp,差值要稳定。如果不同步,要么用硬件触发,要么在配置里加td参数做时间偏移估计。
三是外参填错。旋转矩阵填反了或者平移单位搞错(米和厘米混了),初始化直接崩。建议用kalibr标定后直接复制结果,别手改。
5.2 轨迹漂移的排查思路
跑着跑着轨迹慢慢偏了,这是VIO的固有问题,但可以通过以下方式缓解:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 缓慢单向漂移 | IMU零偏未标定 | 重新标定IMU,检查零偏 |
| 转弯后突然跳变 | 特征点丢失 | 增加纹理,调低特征点阈值 |
| 尺度逐渐变大 | 双目基线标定不准 | 重新标定双目外参 |
| 上下抖动 | 重力方向估计错误 | 检查初始化时的静止时间 |
我实测下来,纯VIO在跑100米左右会有1%到2%的累积误差,这是正常的。要消除累积误差,必须加回环检测或者全局重定位。VINS-Fusion自带回环检测模块,在配置里把loop_closure设为1就能开。
5.3 计算资源不够怎么办
VIO是计算密集型任务,树莓派4B跑1280x720的双目VIO大概只能到10Hz,Jetson Nano能到20Hz以上。如果资源紧张,可以:
- 降低图像分辨率到640x480
- 减少特征点数量(
max_cnt从150降到100) - 关闭回环检测
- 用
-r 0.5降速回放
但降分辨率会牺牲深度精度,降特征点会牺牲鲁棒性,需要权衡。我的建议是优先保证分辨率,特征点数量可以适当降。
5.4 几个我踩过的坑
第一个坑是相机和IMU的供电干扰。我一开始把IMU和电机接同一个电源,电机一转IMU数据就跳。后来给IMU单独加了LC滤波和稳压,问题解决。IMU对电源噪声非常敏感,一定要用干净的电源。
第二个坑是标定板的平整度。我用普通A4纸打印棋盘格,纸受潮后翘边,标定出来的畸变系数完全不对。后来换成亚克力板贴棋盘格,重投影误差从0.8像素降到0.15像素。
第三个坑是时间戳的时区问题。ROS的header.stamp是UTC时间,如果相机驱动用了本地时间,和IMU就对不上。检查方法是rostopic echo看两个话题的时间戳差值是否恒定。
6. 从跑通到实用:我的调试心得
跑通demo和真正能用是两回事。我在实际部署中做了几件事让系统更稳:
一是加了状态监控节点。实时监控VIO的输出频率、特征点数量、重投影误差,一旦异常就触发降级策略(比如切到纯IMU航迹推算)。这个节点用Python写就行,订阅/vins_estimator/odometry和/vins_estimator/keyframe_pose,判断时间戳间隔和协方差。
二是做了坐标系对齐。VIO的world系和车体的base_link系之间有个固定的旋转,这个旋转取决于相机安装角度。我用tf树把它固定下来,避免每次启动都要手动对齐。
三是录了标准数据集做回归测试。每次改配置或者换硬件,都回放同一段bag,对比轨迹的ATE(绝对轨迹误差)。这样能快速判断改动是变好还是变坏。
最后分享一个实用技巧:如果你觉得VINS-Fusion配置太复杂,可以先从单目+IMU跑通,理解整个数据流,再升级到双目。单目配置简单很多,虽然尺度不可观,但用来验证时间同步和外参是否正确足够了。等单目跑顺了,双目就是加一路图像和改几个参数的事。
这套系统我现在跑了大概半年,在地下停车场、办公楼走廊、室外园区都测过,定位稳定性比纯GPS方案好太多。当然它也不是万能的,强光直射、大面积白墙、快速旋转这些场景还是会丢,但配合轮式编码器做融合,基本能满足大部分室内导航需求。如果你也在做类似的项目,希望这些经验能帮你少走点弯路。