
做视觉惯导融合十个项目有八个卡在第一次跑通标定环节。别问我怎么知道的这个 VO 与 INS 外参标定的 MATLAB 工程就是用来解决这一关的通过优化求解相机到 INS 坐标系的旋转、平移和尺度把两个传感器的轨迹对齐到同一个空间基准上为后续多传感器数据融合铺路。这篇内容适合正在做视觉里程计/惯性导航联合定位、想自己实现一套外参标定代码的开发者也适合那种“算法流程看过不少、一写 MATLAB 就崩”的实操型选手。先说清楚一个容易被误解的点外参标定不是“标定一次就永远不用管”的事。它解决的问题是相机坐标系和 INS 坐标系之间的刚体变换关系。这个关系如果给错后面无论是松耦合的滤波融合还是紧耦合的滑窗优化都会在残差里带进系统性偏差。你会在实验中发现明明加速度计和陀螺仪噪声模型没问题视觉特征跟踪也正常融合结果就是飘。问题往往不在前端而在外参。这套工程把标定拆成“数据准备 → 初值求解 → 非线性优化 → 结果验证”四步用 MATLAB 写起来逻辑清楚跑完还能直接看到每次迭代的残差变化适合用来摸清标定原理。下面我就按实际开发顺序把这个工程的核心设计、数学建模、代码实现和踩坑记录完整拆一遍。1. 项目整体设计思路为什么用“轨迹对齐”来做外参标定1.1 标定问题的本质是坐标系变换不是单纯的参数拟合先看基本面VO 输出的是相机位姿序列INS 输出的是世界系下的位置、速度和姿态。两者在同一个运动过程中各自估计出一条轨迹但由于坐标系定义不同同一时刻的两个位置点之间存在一个固定的旋转和平移关系。如果 VO 是单目还会多出一个尺度因子。所以这个项目本质上做的事情是把同一段运动下的 VO 轨迹拆成 N 个位置点把 INS 轨迹在对应时间戳下拆成 N 个位置点然后寻找一组变换参数使得两个点云尽可能重合。这就是三维点集配准的经典问题早期工作一般用 Horn 的绝对定向法也就是四元数线性求解更高阶的做法则是构建非线性最小二乘用 LM 或 Dogleg 算法迭代优化。用轨迹对齐而不是用“同时采集标定板图像 IMU 静止测量重力”这类方案最大的好处是不需要外部运动捕捉系统也不需要专门的标定间。你只需要让设备在手里做一段包含充分旋转和位移的运动两个传感器各自记录数据就能解出外参。对嵌入式平台或相机 IMU 组合模块来说这个条件是很容易满足的。1.2 工程结构划分五个模块各自负责什么实际的 MATLAB 工程不会把所有代码堆在一个脚本里这个项目按我的习惯拆成了五个模块逻辑边界很清楚数据读取模块负责加载 VO 轨迹文件、INS 轨迹文件统一时间基准。数据预处理模块完成时间戳插值、坐标系转换、单位换算。初值估计模块用线性方法求一个大致不离谱的 R、t、s作为优化的起点。非线性优化模块构建残差函数调用lsqnonlin做精细求解。验证与输出模块计算重投影误差、绘制对齐轨迹输出标定结果。模块拆分这件事不是形式主义。我见过很多初学者把所有计算塞进一个脚本里结果换一组数据就要改半天调试的时候满屏幕变量分不清谁是谁。合理拆开后每个模块都能独立测试数据格式变化只改读取模块优化器调参只动优化模块。后面你在实际项目中换相机、换 IMU 型号这套结构还能复用大半。2. 外参标定的数学建模旋转、平移、尺度的细节不容易绕过去2.1 坐标系约定是第一个容易翻车的地方数学建模的第一步不是写公式而是把坐标系定义钉死。相机坐标系一般定义为中心在相机光心X 向右、Y 向下、Z 沿光轴向前INS 坐标系常见定义为中心在 IMU 测量中心X 向前、Y 向左、Z 向上。两者之间差了一个接近 90 度倍数的旋转具体数值取决于安装方式。这个项目里优化求解的目标是找到从 INS 坐标系到相机坐标系的变换关系记作[ p_{cam} s \cdot R_{c \leftarrow i} \cdot p_{ins} t ]这里的 (p_{cam}) 是某个三维点在相机坐标系下的坐标(p_{ins}) 是同一个三维点在 INS 坐标系下的坐标。注意别搞反方向有的资料写成 (p_{ins} R \cdot p_{cam} t)得到的是另一个外参实际使用时容易把自己绕晕。我自己的经验是代码里写清楚“从谁到谁”的注释并且在验证阶段用一段仿真数据测试正反向一致性比口头约定靠谱得多。还有一个容易被忽略的问题INS 解算出来的轨迹位置是什么坐标系下的有些 INS 系统输出的是经纬度和高度需要转成局部笛卡尔坐标有些直接输出东北天坐标系下的位置。不同约定下外参的平移分量完全不同。所以数据预处理里必须包含坐标系转换这一步不能默认 VO 和 INS 已经在同一世界系。2.2 旋转参数化旋转向量、四元数还是旋转矩阵在 MATLAB 的lsqnonlin中做优化时旋转的自由度是 3但旋转矩阵本身是 9 个元素。直接把 9 个元素当作优化变量会带来冗余约束求解器需要额外处理正交性数值稳定性也差。所以一般用旋转向量或单位四元数来参数化。旋转向量是三个元素方向表示旋转轴模长表示旋转角度。MATLAB 里rotationVectorToMatrix和rotationMatrixToVector可以直接互转非常方便。单位四元数是四个元素带有一个单位约束但用四元数做优化时换算成旋转矩阵也简单。就这个项目而言我建议优化变量数量取 7 个旋转向量 3 个、平移 3 个、尺度 1 个。这样残差函数在迭代时不需要处理额外的单位约束。如果非要选四元数则需要用quaternion类或者手写归一化残差把硬约束转成软约束代码复杂度会上升不少。实际对比下来旋转向量在这类“全局轨迹对齐”问题上的收敛速度和稳定性都够了。2.3 残差构建与目标函数设计假设测量到的相机轨迹点集合为 (C_i)INS 轨迹点在相同时间戳下的位置为 (I_i)那么残差可以写作[ r_i C_i - (s \cdot R \cdot I_i t) ]最小二乘目标函数就是[ \min_{R, t, s} \sum_{i1}^{N} | r_i |^2 ]这里面有一个容易忽略的细节轨迹对齐通常涉及几十上百个点但每个点在优化中的权重不应该是相同的。如果某段运动是纯平移那么旋转的可观测性就很弱如果设备大部分时间静止那平移分量又容易被噪声支配。更稳健的做法是按点与点之间的距离或匹配置信度设置权重或者直接使用 IMU 的速度信息来约束尺度。MATLAB 的lsqnonlin默认假设残差是均方差形式你只需要返回一个向量它会在内部自动求和平方。这里要注意残差向量的长度是 3 乘以点云点数。如果轨迹有 200 个采样点残差就是 600 维优化变量是 7 个超定方程组规模足够让求解器收敛到合理结果。3. MATLAB 代码实现关键模块的写法和容易被误解的点3.1 数据读取与时间戳对齐不做插值后面全是坑VO 轨迹和 INS 轨迹的采样频率很难完全相同。常见的情况是 VO 在 20 到 30 HzINS 在 100 到 200 Hz。直接拿两个时间戳不完全对齐的点集做优化残差会带上额外的时间对齐误差标定结果自然不准。处理思路很简单以 VO 的时间戳为基准对 INS 的位置轨迹做线性插值。INS 数据频率高、输出平滑线性插值的精度完全够用。如果反过来以 INS 为基准插值 VO那就需要小心 VO 位姿在帧间变化不是线性的尤其相机快速转动时线性插值四元数或平移会引入不可忽略的误差。代码层面试过的一个稳定写法是% vo_ts: VO 时间戳向量 % ins_ts: INS 时间戳向量 % ins_pos: INS 位置矩阵3 x N vo_pos_interp interp1(ins_ts, ins_pos, vo_ts, linear, extrap);extrap参数要慎用它会把超出边界的值按趋势外推严重时产生离谱数据。我一般会在插值之后做一次数值检查把超出时间范围的样本直接滤掉。3.2 初值估计用 Umeyama 算法先算一个“不蠢”的解直接用随机初值跑lsqnonlin很容易掉进局部最小值尤其是尺度和平移同时求解的时候。正确做法是先做一个线性初值估计把结果当作优化起点。Umeyama 算法是三维点集配准的标准方法先计算两组点的质心再做去质心处理然后用 SVD 分解求解旋转最后通过残差比例求尺度和平移。这个算法在 MATLAB 里用 SVD 一行就能写但要注意两个点集的维度排列——按惯例是 3×N别传成 N×3。求解旋转的核心代码思路如下% X: 3xN, Y: 3xN, 求解 Y s * R * X t mu_x mean(X, 2); mu_y mean(Y, 2); Xc X - mu_x; Yc Y - mu_y; [U, ~, V] svd(Yc * Xc); R V * U; if det(R) 0 V(:, end) -V(:, end); R V * U; end s trace(R * Yc * Xc) / sum(sum(Xc .^ 2)); t mu_y - s * R * mu_x;这段代码算出的 R、t、s 直接作为优化初值。用 SVD 得到的旋转一般不会差太多后续优化只需要在局部范围内精细调整。注意这个代码假设 X 和 Y 都是三维点集如果 VO 轨迹是单目带尺度那么这里的尺度初值能反映出 VO 轨迹相对 INS 轨迹的缩放比例。3.3 优化求解器选型与参数设置MATLAB 里lsqnonlin是最适合这个问题的内置求解器它支持levenberg-marquardt和trust-region-reflective两种算法。对这个规模的小优化问题LM 收敛更快但对初值敏感度略高初值已经由 Umeyama 给好的情况下优先选 LM。优化变量我按旋转向量、平移、尺度的顺序拼接x0 [rotationMatrixToVector(R0); t0; s0]; options optimoptions(lsqnonlin, ... Algorithm, levenberg-marquardt, ... Display, iter, ... MaxFunctionEvaluations, 1e4, ... FunctionTolerance, 1e-10, ... StepTolerance, 1e-10); x_opt lsqnonlin((x) computeTrajectoryResidual(x, vo_pos, ins_pos_interp), x0, [], [], options);残差函数内部要做旋转向量到矩阵的转换function residual computeTrajectoryResidual(x, vo_pos, ins_pos) R rotationVectorToMatrix(x(1:3)); t x(4:6); s x(7); transformed s * (R * ins_pos) t; residual vo_pos - transformed; end这里的vo_pos是相机轨迹位置ins_pos是插值对齐后的 INS 轨迹位置。如果 VO 是单目s初始值不会正好是 1优化过程会自动调整双目的尺度固定为 1那就在代码里固定x(7)1或者使用lsqnonlin的参数上下界把尺度锁死。4. 实操过程记录从数据采集到结果验证每个环节都有“为什么”4.1 数据采集要求不是随便晃两下就行采集数据这一步看着简单实际上决定了标定结果的可用性。要让外参中的旋转近乎可观测运动必须包含充分的旋转激励沿三个轴分别转几圈、做几次“8”字摆动、包含不同速度的平移。如果只做直线平移旋转和平移在残差里会强耦合解出来的旋转可能完全错误但残差看起来却很低。一个具体的采集建议手持设备在室内由近到远走一个“回”字形路线中途多次改变机头朝向路线总时长建议在 30 秒以上包含至少 10 次明显的转向。VO 在弱纹理环境容易丢帧所以环境光照要好场景中最好有静止物体提供特征点。如果项目里已经有一套实时融合系统我建议直接用系统记录原始传感器数据而不是用后处理的数据。因为后处理可能经过了平滑、时间校准等操作数据质量反而被掩盖标定代码跑出的结果无法反映真实硬件状态。4.2 标定结果验证不能只看残差大小很多人跑完lsqnonlin看到残差下降就认为标定完成了这是很危险的。外参标定的真实检验方式是看“对齐后的轨迹一致性”和“融合闭环精度”。我通常做三步验证第一步绘制对齐后的轨迹图。把 INS 轨迹变换到相机坐标系下与 VO 轨迹画在同一张三维图里肉眼观察两条曲线是否基本重合。如果存在局部分叉或剪切变形说明某些时间段数据有问题。第二步做往返一致性检查。同一段路线正走一次、反走一次分别标定比较两次得到的旋转矩阵和平移向量的差异。稳定性好的标定结果差异应该在很小的阈值内如果两次结果差出一大截优先怀疑数据采集质量或时间同步。第三步把标定后的外参接入融合代码做一次真实定位测试。比如在室内走一个闭环查看最终位置误差。外参正确时融合轨迹不会有明显的“转体”或“侧漂”现象。4.3 可视化与输出把结果固化下来这个项目的末尾我会输出一个结构化结果文件包含旋转矩阵、平移向量、尺度因子、最终 RMSE 和迭代次数。保存成.mat文件不算最方便我更喜欢直接生成可读的文本或 YAML 格式方便后续 C 或 Python 工程直接读取。results.R R_opt; results.t t_opt; results.scale s_opt; results.rmse sqrt(mean(sum((vo_pos - (s_opt * (R_opt * ins_pos) t_opt)).^2, 1)));RMSE 的值要结合轨迹尺度看才有意义。如果 INS 轨迹范围是 50 米RMSE 只有几厘米那是很好的结果如果轨迹范围只有 2 米RMSE 也是几厘米那这个标定结果的可靠性就值得怀疑——因为噪声占的比例太大了。5. 使用这个标定工程时我实际踩过的几个大坑5.1 外参初始化失败初值离真解太远优化直接发散这个现象特别常见尤其是用鱼眼相机做 VO 时畸变模型和针孔模型的投影差异很大轨迹解算本身就带偏差。如果 Umeyama 解出的初值不够好LM 算法会在前几次迭代就跑飞。我处理这个问题的方法有两个一是把尺度初值先限制在合理范围内比如以轨迹包围盒对角线长度的比例估算二是先固定尺度只优化旋转和平移等残差进入收敛域后再放开尺度一起优化。% 两阶段优化 x_stage1 [rotationMatrixToVector(R_umeyama); t_umeyama; s_umeyama]; % 固定尺度 fixed_scale s_umeyama; x_stage1(end) fixed_scale; options_stage1 optimoptions(lsqnonlin, Algorithm, levenberg-marquardt); x_opt_stage1 lsqnonlin((x) resid(x, vo_pos, ins_pos), x_stage1, [], [], options_stage1); % 放开尺度 x_stage2 x_opt_stage1; x_opt_final lsqnonlin((x) resid(x, vo_pos, ins_pos), x_stage2, [], [], options_stage1);这个操作在标记点对齐和轨迹对齐中都有效本质上是先用低自由度的强约束把解拉到正确的山谷里再用高自由度拟合细节。5.2 时间戳没有对齐残差看起来很小但轨迹在飘我用过一段 VO 与 INS 时间基准相差 50 ms 的数据集优化后 RMSE 只有几厘米但把对齐后的轨迹叠加可视化明显看出转弯处有一条“分叉”。原因是时间误差在直线运动时对残差影响小转弯时两帧位置变化大时间偏移导致的位置偏差就暴露了。解决方式是在残差模型中加入时间延迟变量把时间偏移也作为优化量之一。比较简单的做法是在数据预处理阶段画互相关图求两个轨迹的最优时间偏移再去插值。实用代码片段是先粗扫一个延迟窗口选使两条轨迹对齐误差最小的延迟值。5.3 尺度因子收敛到不合理数值单目 VO 的轨迹尺度本来就是任意的与 INS 轨迹对齐后尺度因子理论上是固定的。但我在一些低纹理环境下遇到尺度收敛到 0.8 或者 1.3 这种明显偏离真实值的情况。排查后发现问题不在优化方程而在 VO 轨迹本身发生了尺度漂移——长时间运行的单目 VO 会因累积误差导致局部尺度不一致。这种情况下分段落优化是有效的手段把轨迹按时间切成几段每段单独估计尺度和外参观察尺度的时段变化。如果发现某段的尺度与其他段明显不同那这一段 VO 数据大概率丢了尺度约束应该剔除或降权。5.4 INS 数据中的温漂和零偏INS 数据看似比 VO 可靠但消费级惯性器件的零偏会随温度变化。长时间采集后INS 解算的位置本身就有漂移用它作为标准参考会污染标定结果。我的做法是在标定数据采集前让设备充分预热并且在采集中保持设备温度相对稳定避免刚从低温环境拿出设备就直接开始录数据。6. 一点后续扩展想法这个工程还能怎么接着玩这套标定工程目前是离线处理方式跑完一次给出一组固定外参。实际项目中如果传感器安装结构件容易形变或者使用了可调焦相机外参可能随时间变化。后续可以考虑把算法扩展成滑窗式在线标定利用融合滤波器中的残差序列每隔一段时间重估一次外参并把重估结果反馈给前端。MATLAB 环境下可以先做一个基于滑动窗口的仿真验证确认外参更新频率与噪声约束的关系再移植到 C 嵌入式环境。另外标题里提到“滤波跟踪”这个方向本身就是视觉惯导融合的另一个分支。如果后续想走紧耦合融合路线外参标定就不只是几何对齐问题它还会和 IMU 零偏、视觉特征深度耦合在一起。到时候这套纯几何标定的结果可以作为紧耦合系统的初始化值再用基于滤波或图优化的方法做进一步精化。我个人的体会是外参标定这类工作属于“看起来简单、做起来绕”的典型。数学公式十分钟能看懂但真正跑通一套代码、验证完数据质量、解决完一两轮实际工程问题那种收获感远不止是得到一个参数文件。按照上面这套流程走一遍踩掉几个坑之后你会发现后面做多传感器融合时“外参”这个包袱终于可以放下安心去调滤波器和优化策略了。