
1. 外点到底从哪冒出来先把敌人在哪搞清楚视觉SLAM里的外点处理说白了就是一场如何不被少量坏数据带偏的持久战。鲁棒核函数和RANSAC是这场仗里的两件主力武器但它们并不是互斥选项更不是随意替换的开关——用错位置轻则精度掉一截重则整条轨迹在某个转弯处突然跳飞。我带过几个刚入门的同学最常见的误区是一看到轨迹飘就先去调优化器的核函数阈值结果折腾两天毫无起色最后发现问题出在前端匹配那一步外点率已经高到任何鲁棒手段都救不回来。所以这篇东西我打算倒着讲先讲外点从哪来、怎么破坏优化再讲RANSAC这种硬筛和鲁棒核函数这种软压分别怎么用、参数怎么定、什么时候该叠加使用。外点outlier这个词在不同环节含义略有差别但在视觉SLAM的主线上大体可以理解为不满足当前几何模型的观测。它和噪声noise不是一回事噪声是零均值的小幅扰动外点是彻底跑偏的观测一个外点的残差可能是一个内点的几十上百倍。这两者的处理思路完全不同噪声靠滤波和加权外点靠剔除或降权。1.1 特征匹配环节外点的第一生产线绝大多数外点是在特征匹配阶段产生的。描述子本质上是高维空间里的一个向量匹配就是找最近邻这个过程中至少有三类情况会稳定产出外点。第一类是重复纹理。地砖、栅格、货架、树叶、建筑立面的规则窗格这些区域里同一张图上就有几十个几乎一模一样的描述子。最近邻搜索会老老实实返回一个距离很小的候选但这个候选和 query 点根本不是同一个物理位置。这类外点的特点是描述子距离极小用距离阈值根本筛不掉只能靠几何约束。第二类是视角和光照剧变。相机快速旋转、强逆光、过曝欠曝都会让同一个物理点的描述子发生显著变化导致正确的匹配被排到第二第三位。这类外点的描述子距离通常偏大用比值判别Lowes ratio test能拦下一部分。第三类是动态物体。行人、车辆、晃动的枝叶它们本身在动即使描述子匹配得完全正确三角化出来的三维点也是错的。这类外点最阴险因为从匹配质量上看它们完美无缺只有引入时间一致性或者几何一致性才能识别。还有一类容易被忽略的标定误差和深度估计误差。双目或者RGB-D的深度在远处、纹理弱处误差很大三角化出来的点位置漂移投影回图像后残差偏大本质上也是一种外点但它不是匹配错了而是三维点本身不准。假设一段普通室内序列原始暴力匹配的外点率经常在 60% 到 80% 之间。这个数字听起来夸张但你可以自己统计一下取一对图像用最近邻匹配然后手动标注或者用严格的对极几何校验看看有多少匹配满足约束。我实测过几次纹理丰富的办公场景大概能到 70% 左右。这意味着如果不做任何处理后端优化的输入里坏数据比好数据还多。1.2 外点为什么能把最小二乘带跑偏要理解鲁棒核函数的必要性得先看清普通最小二乘的软肋在哪。标准的 bundle adjustment 或者位姿图优化目标函数是残差平方和min Σ ||r_i(x)||²这里r_i是第 i 个观测的残差x是待优化变量。对残差求导得到正规方程解出来的是让平方和最小的那个x。问题就出在这个平方上。举个具体的数假设有 100 个内点残差都在 1 个像素左右它们对代价的贡献是100 × 1² 100。再来 1 个外点残差 100 像素它一个点的贡献是100² 10000。也就是说一个外点的话语权是 100 个内点总和的 100 倍。优化器会不惜牺牲所有内点的精度去迁就这一个外点因为那样总代价下降得更多。从雅可比矩阵的角度看更直观。高斯-牛顿的法方程是H Δx -b其中H Σ J_i^T J_ib Σ J_i^T r_i。外点的r_i很大b就被这个方向主导解出来的增量Δx会朝着把外点残差拉小的方向猛冲而内点的残差反而被推大了。迭代几次之后整个解就歪了甚至可能完全不收敛。更麻烦的是平方函数在残差变大时增长没有上界所以理论上不存在外点太大以至于被自动忽略的机制——越大的外点影响力越强。这就是为什么单纯增加迭代次数、换更好的线性求解器都没用问题的根源在代价函数的形式本身。我见过一个特别典型的案例某次回环检测误检一条错误约束被加进位姿图残差大概 30 米。整个图优化跑完之后那条闭环附近的十几个关键帧位姿全被拽歪了视觉上就是轨迹在那里打了个结。把这条约束去掉一切正常。这就是单个外点在全局优化里的破坏力。所以外点处理的核心思路只有两条要么在优化之前把它识别出来剔掉RANSAC 走这条路要么把代价函数改造成残差大了影响力反而变小的形状鲁棒核函数走这条路。前者是离散决策后者是连续加权两者的适用场景不一样后面会详细展开。2. 鲁棒核函数把平方惩罚换成温和惩罚鲁棒核函数robust kernel / loss function的思路很朴素既然平方增长太激进那就换一个增长慢一点的函数让大残差的边际惩罚递减。极端情况下残差大到一定程度就彻底不惩罚了等效于把这个观测扔掉但保留了函数的连续可导性便于优化器处理。这套方法在统计学里叫 M 估计M-estimator在 SLAM 的工程实现里通常以LossFunction或者RobustKernel的名字出现。2.1 从二次代价到饱和代价四类常用核函数横向对比先明确一个记号问题这是初学者最容易混淆的地方。不同库对核函数的定义方式不一样有的把输入定义为残差本身r有的定义为残差平方s ||r||²。Ceres 里的LossFunction输入是平方后的sg2o 的RobustKernel输入是残差的范数||r||。写代码的时候一定要看清楚否则参数会差一个量级。下面这张表按输入为残差平方s来统一k是用户设定的阈值参数核函数表达式ρ(s)权重w ρ(s)特点L2无核s1无鲁棒性基准对照Huberss ≤ k²2k√s - k²s k²1k/√s小残差保持二次大残差转线性凸函数最常用Cauchy(k²/2)·log(1 s/k²)1/(1 s/k²)全程平滑降权对极端外点更宽容非凸Geman-McClures/(1 s/k²)1/(1 s/k²)²降权更激进适合外点率高的场景Tukey biweight分段s k²时为常数s k²时为 0硬截断完全拒绝大残差非凸性最强Huber 是默认首选原因很实际它是凸函数从任意初值出发都能保证收敛到一个全局最优对于凸问题不会因为核函数本身引入局部极小。代价是它对超大外点的抑制不够彻底残差再大也只是线性增长只是斜率从2r降到了2k。Cauchy 和 Geman-McClure 的降权更狠但都是非凸的优化的收敛域变窄初值不好容易被吸到错误的局部极小里。Tukey 干脆把超过阈值的观测权重直接置零效果等同于动态剔除但非凸性最强实践中通常只在后端位姿图里用而且要求初值已经比较靠谱。我个人的选型习惯前端视觉残差重投影误差用 Huber因为初值经常一般需要凸性保底后端位姿图的回环约束用 Cauchy 或者 Geman-McClure因为这些约束数量少、单条影响大需要更强的抑制如果外点率预估超过 30%考虑上 GNC 这类渐进策略而不是一上来就用最激进的核函数。2.2 IRLS核函数是怎么变成一次次加权最小二乘的核函数不是直接塞进优化器就完事的中间需要一个转换过程叫迭代重加权最小二乘IRLSIteratively Reweighted Least Squares。搞懂这一步你才知道调参时到底在调什么。鲁棒优化的目标函数是min Σ ρ(||r_i(x)||²)对x求一阶条件用链式法则Σ ρ(s_i) · (∂s_i/∂x) 0而s_i r_i^T r_i所以∂s_i/∂x 2 J_i^T r_i其中J_i是残差对状态的雅可比。代回去Σ ρ(s_i) · J_i^T r_i 0对比普通最小二乘的法方程Σ J_i^T r_i 0形式完全一样只是每一项前面多了一个因子w_i ρ(s_i)。这就是 IRLS 的全部秘密核函数的作用等价于给每个残差块动态分配一个权重权重是残差大小的函数。以 Huber 为例它的权重是w 1 当 ||r|| ≤ k w k / ||r|| 当 ||r|| k残差小于阈值时权重为 1和普通最小二乘无异超过阈值后权重按1/||r||衰减残差翻倍权重减半正好抵消掉残差本身增长带来的影响使得该观测对法方程的贡献保持恒定而不再膨胀。实际实现里优化器每轮迭代会先算当前残差按核函数更新所有w_i然后解一次加权正规方程Σ w_i J_i^T J_i Δx -Σ w_i J_i^T r_i更新状态再进入下一轮。所以你会看到开启核函数后迭代次数通常比不开多几轮因为权重在动态变化需要多迭代几次才能稳定。这是正常现象不是收敛出问题。一个实践细节如果某个残差在迭代中反复横跳这一轮权重 0.8下一轮 0.2说明它处在一个半好半坏的边界上通常意味着阈值设置不合理或者该观测本身有问题。调试时可以打印权重直方图健康的分布应该是明显的双峰——绝大多数接近 1少量接近 0中间地带越少越好。2.3 阈值 k 到底怎么定从卡方分布说起k是核函数唯一的参数也是最容易被拍脑袋定的参数。定大了没效果定小了把内点也压下去精度反而下降。有几种靠谱的定法。第一种基于卡方分布。视觉SLAM的重投影残差是二维的两个分量近似独立高斯那么残差平方s r_x² r_y²服从自由度为 2 的卡方分布前提是残差已经按协方差归一化。卡方分布 95% 分位数是 5.99199% 分位数是 9.210。如果我们希望正常内点有 95% 的概率不被惩罚就把阈值设成k² 5.991即k ≈ 2.45在s的单位下。这是最经典的做法也是特征点法框架里卡方检验阈值的来源。注意前提是残差要按观测噪声归一化如果直接用像素单位k的数值取决于像素噪声大小。假设重投影误差的标准差是 1 个像素那归一化后的残差就是像素残差本身k ≈ 2.45像素如果标准差是 2 像素那就要把阈值放大到 4.9 像素。所以正确的流程是先估噪声再定阈值而不是直接抄一个 5.991 完事。第二种基于鲁棒统计的效率常数。Huber 有个著名的结论当残差服从正态分布时取k 1.345σ可以达到 95% 的渐近效率。这里的σ是残差的标准差不是方差且这个常数是针对输入为残差本身的定义。对应到输入为残差平方的定义阈值就变成k² (1.345σ)²。Cauchy 对应的常数是 2.3849σTukey 是 4.685σ。这几个常数是统计学家算出来的含义是在纯高斯噪声下用这个核函数相比不用估计方差的膨胀比例不超过 5%。工程上直接用很有说服力比拍脑袋强得多。第三种自适应估计。真实数据里 σ 事先不知道可以用 MAD中位数绝对偏差来估σ ≈ 1.4826 · median(|r_i - median(r)|)这个估计量对 50% 以下的外点不敏感比直接算标准差稳健得多因为标准差会被外点本身污染。做法是先跑一轮普通最小二乘算出所有残差用 MAD 估出 σ再按1.345σ设 Huber 阈值重新跑。这个两阶段流程在很多工程实现里是标配成本很低效果提升明显。我个人建议的做法是把第二种和第三种结合起来初始化时用 MAD 估尺度迭代过程中每隔几轮重新估一次让阈值跟得上残差分布的变化。有的实现会把这个做成自适应 Huber实测在动态场景里比固定阈值稳定不少。注意不要在优化刚开始、残差还很大的时候就用小阈值。这时候所有观测的残差都很大核函数会把所有观测都当成外点压下去优化直接停滞。正确做法是先跑几轮不带核函数的迭代把残差降下来再开启核函数或者让阈值在迭代初期逐渐收紧类似 GNC 的思路。3. RANSAC用随机采样共识检验硬筛鲁棒核函数是在优化阶段做软性降权RANSAC 走的是另一条路在模型估计阶段直接把不满足几何模型的观测判为外点并剔除。两者解决的问题有重叠但适用的环节不同——RANSAC 适合给定一堆对应关系估计一个几何模型的场景基础矩阵、单应、PnP、三角化核函数适合给定一个已经建好的优化问题让它在有坏观测时也能收敛的场景BA、位姿图。3.1 四步流程与那个决定成败的概率公式RANSAC 的流程本身极其简单四步循环从数据中随机抽取最小样本集估计模型所需的最少点数。用这个最小集拟合一个模型假设。计算所有数据点到该模型的残差残差小于阈值的判为内点统计内点数量。内点数超过当前最优就更新最优模型重复直到满足终止条件。真正需要想清楚的是重复多少次。这个次数由著名的成功率公式给出N log(1 - p) / log(1 - w^n)其中p是期望的成功概率通常取 0.99w是内点率n是最小样本集的点数。含义是一次采样中n个点全是内点的概率是w^n那么一次失败的概率是1 - w^nN次全失败的概率是(1 - w^n)^N令其等于1 - p反解就得到上式。这个公式里有个指数关系导致n的微小增加会带来迭代次数的爆炸式增长。算几个具体数字感受一下取p 0.99内点率 wn2单应n4单应n5本质矩阵n8基础矩阵0.9358240.771825790.5177214511800.3515041893629500.2114249614182786836这张表解释了很多工程现象。为什么单应矩阵4 点法在外点率 50% 时还能跑而基础矩阵8 点法在外点率 50% 时需要上千次迭代因为n差了 4指数放大。为什么工业界宁可多用 5 点法解本质矩阵也不用 8 点法因为 5 点法把迭代次数砍了一个量级。为什么内点率低于 20% 时基本就没救了看看最后一行的数字百万级迭代实时性完全崩了。这也说明在前端把外点率降下来有多重要。如果匹配阶段能把内点率从 50% 提到 70%基础矩阵估计的迭代次数从 1180 降到 79差了 15 倍。所以比值判别、互惠检验这些预处理手段不是可有可无的优化而是让 RANSAC 能不能实时跑起来的前提。3.2 自适应终止不要用固定迭代次数实际工程里很少真的按最坏情况跑固定次数因为w事先不知道。标准做法是自适应终止维护当前找到的最大内点数实时反推w 内点数 / 总数用这个w重算需要的迭代次数N当已完成的迭代数超过N时提前退出。伪代码大概是best_inliers 0 needed float(inf) iters 0 while iters needed and iters max_iters: sample random.sample(data, n) model fit(sample) inliers count_inliers(model, data, thresh) if len(inliers) best_inliers: best_inliers len(inliers) best_model model w len(inliers) / len(data) needed math.log(1 - 0.99) / math.log(1 - w ** n) iters 1这个自适应策略的效果通常非常显著在前几轮就采样到好模型的情况下迭代可能十几次就停了而不是跑满上千次。代价是要处理好w^n接近 1 时的数值问题log(1 - 1)会除零通常加个上限max_iters兜底。还有一个常被忽略的细节随机采样要用真正独立的随机数不能用简单的线性同余或者固定种子否则在某些数据排列下会反复采到相似的组合。工程上建议用std::mt19937之类的现代随机数引擎并且每次调用换种子。3.3 从 RANSAC 到 MSAC、PROSAC、LO-RANSAC 的演进路线原始 RANSAC 有两个明显缺陷一是用内点计数作为评分所有内点一视同仁一个残差刚过阈值的点和残差近零的点贡献相同二是完全随机采样浪费了先验信息。针对这两点工业界做了不少改进下面几个值得了解因为很多开源库默认用的就是这些变体。MSACM-estimator Sample Consensus把评分从计数换成截断的平方残差和。内点贡献r²外点贡献一个常数T²。这样在估计基础矩阵这类问题上精度比原始 RANSAC 明显好因为它会倾向于选择内点虽然数量略少但拟合更精确的模型而不是内点最多但边缘点很多的模型。OpenCV 的默认实现里就有这个选项。PROSACProgressive SAC针对特征匹配场景。既然匹配质量有高低描述子距离小的更可信那就别一视同仁地随机采样而是按质量降序排列先用质量最高的点采样随迭代推进逐步扩大候选集。实践中能减少 30% 到 50% 的迭代次数在纹理丰富的场景收益明显。LO-RANSACLocal Optimization RANSAC的思路是一旦找到一个不错的模型别急着继续随机采样而是用当前所有内点重新做一次最小二乘拟合得到更精确的模型再重新统计内点。这个局部优化步骤能把内点数进一步提升因为它把原本被排除在边缘的观测又拉回来了。很多现代实现包括 USAC 系列都内置了这一步。MAGSAC的贡献是去掉了阈值参数这个最恼人的超参。它的做法是对噪声尺度σ做边缘化把每个点到模型的距离按一组σ假设加权积分最终得到的评分不依赖任何阈值。对于不知道该设多少像素阈值的场景这个特性非常实用。当然代价是计算量略大而且实现复杂度高。选型上我的建议是对极几何估计用 USAC_MAGSAC 或者 USAC_MAGSAC 这类自适应阈值的能省掉大量调参时间PnP 用 EPnP RANSAC 或者 PROSAC如果匹配质量分布很不均匀就用后者三角化点筛选用简单的距离阈值 视差角约束就够了不需要上重武器。3.4 在 SLAM 各环节的落地位置RANSAC 在视觉SLAM的主线上至少出现在这五个地方每个地方的最小样本数、阈值、失败处理策略都不一样。第一处本质矩阵/基础矩阵估计。用于两帧之间的几何校验。本质矩阵用 5 点法基础矩阵用 8 点法或者 7 点法。这里的横轴阈值通常取 1 到 3 个像素取决于特征定位精度。如果 RANSAC 输出的内点数少于一个下限比如 15 个直接判定这一帧对追踪失败不要硬着头皮往下走。第二处单应矩阵估计。用于平面场景或者全景拼接。4 点法。实践中会用单应和基础矩阵同时估计比较两者的内点数和退化程度选更可靠的那个——这也是很多框架处理退化场景的标准做法。第三处PnP 位姿估计。已知三维点和二维匹配估计相机位姿。EPnP 用 4 个点RANSAC 外围筛选。这里的阈值要注意因为三维点的深度误差会传导到重投影误差上远处的点阈值要放宽一些或者直接按深度加权。第四处三角化之后的点筛选。三角化出来的点要检查三件事重投影误差是否在阈值内、视差角是否足够大太小则深度不可靠、深度是否为正。任何一个不满足就标记无效。这一步不做地图里会混进大量远距离的退化点后端优化直接被拖垮。第五处回环检测的几何校验。词袋检索出的候选帧先用 RANSAC 估计相对位姿内点数不足或者估计出来的位姿和里程计先验差异过大就拒绝这个回环。这一步是防止误闭环的最后一道闸门。五个位置串起来看你会发现 RANSAC 的角色很统一在进入优化器之前把明显不对的东西拦掉。至于剩下的那些看起来对但可能仍有偏差的观测交给鲁棒核函数去软性处理。两者的分工就是这么划的。4. 工程实操把两套东西嵌进优化管线理论讲完落到代码上。这一节按实际管线的顺序走一遍先前端几何校验再后端优化最后是把两者串起来的调参顺序。4.1 前端几何校验RANSAC 的实际调用方式以对极几何约束为例用 OpenCV 的实现最省事import cv2 import numpy as np # pts1, pts2: N x 2 的匹配点对 F, mask cv2.findFundamentalMat( pts1, pts2, methodcv2.USAC_MAGSAC, # 自适应阈值省心 ransacReprojThreshold1.5, # MAGSAC 下这个值影响较小 confidence0.999, maxIters2000 ) inlier_mask mask.ravel().astype(bool) pts1_in pts1[inlier_mask] pts2_in pts2[inlier_mask]几个实操要点。confidence设 0.999 而不是 0.99因为多出来的迭代次数通常只有个位数但失败率降了一个量级很划算。maxIters必须设防止极端情况下死循环。mask的返回值有时是N x 1有时是N x 4多模型情况用的时候记得ravel()一下再判断长度。估计出本质矩阵后还要做一步卡方检验来精筛内点。本质矩阵本身只是个粗筛真正的三维一致性要后面才知道。做法是把匹配点用本质矩阵三角化检查重投影误差和深度正负这一步能在 RANSAC 的内点里再干掉 5% 到 10% 的残余坏点。单应矩阵的用法类似但注意findHomography除了返回H和mask还应该检查H的退化性——如果匹配点几乎共线单应估计会病态即使 RANSAC 返回了结果也不可信。判断方法是把内点做一次 PCA如果最小主方向的能量占比过低就认定退化。4.2 后端优化Ceres 里的核函数配置Ceres 的核函数通过LossFunction挂到残差块上一个残差块一个核函数可以混用。#include ceres/ceres.h // 场景一重投影误差初值一般用 Huber 保凸性 ceres::LossFunction* huber new ceres::HuberLoss(1.0); // 场景二回环约束数量少影响大用 Cauchy 强抑制 ceres::LossFunction* cauchy new ceres::CauchyLoss(1.0); // 场景三需要硬截断时用 Tukey注意非凸风险 ceres::LossFunction* tukey new ceres::TukeyLoss(1.0); ceres::Problem problem; // 重投影残差3 维残差7 维位姿 3 维点 problem.AddResidualBlock( ReprojectionCost::Create(observed_uv), huber, pose.data(), point.data() ); // 回环的相对位姿残差6 维 problem.AddResidualBlock( LoopClosureCost::Create(measured_relative_pose), cauchy, pose_i.data(), pose_j.data() );这里有个极其容易踩的坑Ceres 的LossFunction参数含义是残差平方的阈值而 g2o 的RobustKernel::setDelta()参数是残差范数的阈值。同一个数值在两边含义不同差一个平方关系。我见过有人从 g2o 的配置里抄了个delta 1.0到 Ceres 的HuberLoss(1.0)结果实际阈值相当于 g2o 里的 1.0因为sqrt(1.0) 1.0碰巧对了但如果抄的是delta 2.0Ceres 这边实际阈值是 1.414就悄悄变严了。迁移配置的时候务必核对。如果内置的四种核函数不够用可以自定义。自定义的核心是继承ceres::LossFunction并实现Evaluate输入是残差平方s输出是核函数值和一阶导class GemanMcClureLoss : public ceres::LossFunction { public: explicit GemanMcClureLoss(double k) : k2_(k * k) {} void Evaluate(double s, double out[3]) const override { const double denom 1.0 s / k2_; out[0] s / denom; // rho(s) out[1] 1.0 / (denom * denom); // rho(s) if (out[2] ! nullptr) { out[2] -2.0 / (k2_ * denom * denom * denom); // rho(s) } } private: double k2_; };二阶导rho只在部分线性求解器比如需要构造精确 Hessian 的里才用到返回nullptr或者正确的解析式都行。但如果你返回了错误的值比返回nullptr更糟——求解器会静默地用一个错误的 Hessian 去解收敛行为变得诡异且难排查。不确定的时候宁可返回nullptr。4.3 分阶段策略什么时候用哪一套真实系统里这两套方法是配合使用的不是二选一。我在几个项目里摸索出来的流程大致是这样按顺序执行第一阶段前端匹配后立即做几何校验。用 RANSAC 估计本质矩阵或者单应剔除几何上明显不一致的匹配。目标是让进入后端的观测内点率不低于 80%。这一步是硬性的不达标就说明前端有问题往上加核函数没用。第二阶段三角化后做点级筛选。检查重投影误差、视差角、深度符号、点到光心距离是否超过合理范围。这四个条件任意一个不满足就丢弃该点。这一步能干掉大部分匹配正确但深度不可靠的观测。第三阶段局部 BA 开启 Huber阈值按 MAD 自适应。这是主力优化绝大多数残差在这里被处理。阈值不要一上来就很小可以设一个初始值前 3 到 5 轮迭代不更新之后每 5 轮重新按当前残差的 MAD 估一次尺度并更新阈值。第四阶段全局优化和位姿图用更强的核函数。回环约束、全局位姿图观测数量少单条错了代价大用 Cauchy 或者 Geman-McClure。如果回环误检率有疑虑可以用 GNC 的渐进策略先把核函数阈值设得极大等效于不抑制然后在迭代中逐步收紧让外点的权重平滑地降下去。这个技巧比直接上硬阈值稳定很多因为它避免了初值不好时直接被钳死。第五阶段残差诊断。优化收敛后把所有残差的权重打印出来看分布。健康的情况是 90% 以上权重接近 1小于 5% 权重接近 0中间地带很少。如果中间地带很多比如 20% 的权重在 0.3 到 0.7 之间说明阈值设置有问题或者数据里有一大批半好半坏的观测需要回头看是不是标定不准、时间同步有偏差或者某个区域的深度系统性偏移。实操心得不要指望调整核函数参数能弥补前端的系统性缺陷。我见过的绝大多数优化不收敛问题根因都在于标定误差、时间戳对齐、或者某个传感器的外参配错了核函数只是把症状掩盖了一部分。调参之前先做数据体检看看重投影误差的空间分布有没有明显的系统性偏差。5. 常见问题与排查实录这一节整理的是实际调过的、有代表性的问题。有些是参数问题有些是理解偏差还有些是工具链的坑。5.1 问题速查表现象可能原因排查手段处理方向轨迹在特定路段突然跳变该段外点率过高RANSAC 失效统计该段的内点数占比看是否低于 30%检查纹理、加匹配约束、必要时降级到里程计优化迭代次数暴涨但不收敛核函数阈值过小所有观测被压打印权重直方图看是否大量接近 0放大阈值或前几轮不启用核函数开启核函数后精度反而下降阈值过小内点被误伤对比开/关核函数的 RMSE 和权重统计按 1.345σ 重设阈值内点数正常但位姿仍有偏差残余的系统性误差标定、同步看重投影误差的空间分布是否均匀重新标定检查时间戳RANSAC 结果每次运行不一样随机种子未固定或采样不独立固定种子复现调试期固定发布期用真随机单应估计在内点很多时反而失败纯旋转/低视差导致退化检查内点的视差角分布切换到本质矩阵或标记退化回环误检导致全局扭曲词袋误检索 RANSAC 未拦住检查回环的相对位姿与先验的差异加先验一致性校验用 GNC后端优化耗时激增核函数导致迭代次数翻倍Profile 单次迭代耗时和迭代总数可接受或用分段策略5.2 几个踩过的坑和对应的解法坑一把核函数当成万能药忽视前端质量。早期接手过一个项目轨迹抖动严重第一反应是去后端加核函数试了四种核函数、调了十几个阈值组合抖动只是稍微收敛了一点没有根本改善。后来老老实实统计前端的内点率发现平均只有 40% 出头。这个水平下任何核函数都只是把错误平滑了一下解本身还是错的。回头去查前端发现是描述子阈值设得太松加了比值判别和互惠检验之后内点率提到 75%后端的抖动问题自然消失核函数参数用的还是最朴素的那一套。这件事之后我形成了一个固定的排查顺序先看内点率再看残差分布最后才动优化参数。内点率是根残差分布是症参数是药顺序不能反。坑二RANSAC 阈值按像素设忽略了归一化坐标。在某次移植代码的时候把前端的阈值从像素单位直接搬到了归一化坐标系下用结果筛选几乎不起作用——归一化坐标下的残差量级是像素坐标的1/ff是焦距好几百像素的话量级差了两个数量级。正确的做法是把像素阈值除以焦距换算过去或者干脆在像素坐标下做校验再转换回来。这类单位错误非常隐蔽因为代码能跑、不报错只是效果静默地变差。坑三核函数和固定剔除混用导致重复惩罚。有个实现里前端已经用固定阈值剔除了残差大于 4 像素的观测后端又挂了一个阈值 1 像素的 Huber。结果是一部分本来合理的观测被压了两次精度明显下降。核函数和硬剔除的逻辑是重叠的要么前端宽一点后端靠核函数软处理要么前端严格剔除后端用宽松核函数兜底不能两头都紧。我倾向于前者因为硬剔除会损失信息而且边界处的观测被一刀切掉容易造成不连续。坑四GNC 的收缩策略调得太快。GNC 的思路是让核函数从宽松逐渐收紧但如果收缩步长太大、每步之间的迭代次数太少会出现外点还没被正确识别核函数就已经收紧到把内点也压掉的情况。标准的做法是每个非凸度级别至少迭代若干次直到收敛再进入下一级级别之间用几何或者线性的方式递减参数。我试过最激进的策略一步收紧到位效果等同于直接上非凸核函数初值敏感的问题全回来了。坑五诊断信息不落地问题无法复现。优化出问题的时候最难的不是修而是复现。我的建议是从第一天起就把这些信息记录到日志里每轮迭代的残差统计均值、中位数、95 分位、最大值、各残差块的权重分布、内点率、迭代次数、最终代价。有了这些出了问题可以回放而不是靠猜。这套日志在定位某一段轨迹异常这类问题时特别好用看一眼残差的 95 分位曲线就知道是哪个时间段出了问题。我个人在实际操作中的体会是外点处理这件事最容易被低估的部分不是算法本身而是判断当前的外点率处在什么水平。RANSAC 的迭代次数、核函数的阈值、甚至整个优化策略的选择本质上都是内点率的函数。内点率 80% 和 40%需要的完全是两套药方。所以我现在的习惯是接手任何一段数据第一件事就是跑一遍统计把每个环节的内点率打出来形成一个基线后面所有调参都以这个基线为参照。没有这个基线调参就是在黑箱里瞎摸改一个参数好一点、改另一个差一点永远不知道自己在哪。还有一个我用了很久的小技巧在核函数之外额外维护一个观测质量分。每次优化收敛后用最终的权重和残差给每个观测打个分把分数持续偏低的那些观测单独存一份。跑长序列的时候如果某个空间区域的低分观测特别密集通常意味着那个区域有系统性问题路面反光、结构重复、动态物体频繁下次经过那里时可以提前降权或者切换策略。这套机制不复杂但让整个系统从被动容错变成了主动预警在长期运行的场景里价值很大。