返回专辑
·Johan·15 分钟阅读

多视图三角化的角度条件:基线够长仍可能数值病态

从射线夹角、深度参数化与协方差传播解释退化配置,比较线性三角化与非线性精炼;用已知尺度场景的重投影与深度误差切片验收。

多视图三角化的角度条件:基线够长仍可能数值病态

VO 或离线重建里常见一种误判:两帧相机中心已经拉开半米,日志里的 baseline 看起来足够,三角化出来的点却仍沿某个方向拉成一条雾带;重投影误差只有零点几像素,深度和尺度却在后端里慢慢把轨迹拽歪。上一篇讨论的是基线太短导致视差不足,本篇换一个更隐蔽的问题:基线长度不是充分条件。三角化真正吃的是观测射线的空间夹角、这些射线在三维中的分布、像素噪声被深度参数化放大的方式,以及线性方程组在当前坐标层上的条件数。

工程上最危险的不是所有点都失败,而是一部分点通过了正深度和重投影门限,协方差却极端各向异性。它们在局部 BA 里看似贡献约束,实际只约束了横向投影,对深度方向几乎没有信息。若这些点被当作尺度锚点,回环前的轨迹会出现缓慢漂移;若用于机械臂抓取或障碍距离估计,系统会在某些视角上显得“偶尔很准、偶尔离谱”。因此验收不能只看 mean reprojection error,而要把射线夹角、信息矩阵谱、已知尺度切片一起看。

1. 基线长度与三角化角度不是一回事

设第 个相机中心为 ,归一化像素反投影后的单位射线为 。两视图三角化常用夹角

其中两条射线需先变到同一坐标系。很多日志只记录 ,这会漏掉一个事实:相机可以平移很远,但如果目标也远,或者目标位于运动方向附近,两条射线的夹角仍然很小;相机也可以绕物体侧向移动,却因为匹配点集中在一条窄纹理带上,使多视图射线几乎落在同一个局部平面里,深度方向和某个横向方向强相关。

用简化几何看,目标距离为 ,有效横向基线为 ,小角度下 。这里的 是相对目标视线的垂直分量,不是里程计报告的相机中心距离。车沿着走廊向前跑,基线数值增长很快,但走廊尽头标志牌的 可能很小;无人机绕塔做斜向轨迹时,某些立面上的点虽然有明显视差,射线集合却接近共面,法向深度会被噪声拉长。把“基线够长”写成唯一门限,会把这些场景误判为可观。

2. 从两条最近点线段看不确定方向

两条带噪射线通常不相交,mid-point 三角化会找两条直线上的最近点再取中点:

,解 时会出现分母

这条式子比“视差越大越好”更有诊断价值:当 小,深度参数对像素扰动的灵敏度按 放大;当多视图射线在某个方向上分布很窄,信息矩阵会在对应特征向量上接近奇异。注意这里讨论的不是固定小基线,而是角度条件。只要有效夹角小,哪怕相机中心相距很远,点的深度仍会像挂在一根长杆末端,微小角度误差就能扫出很大空间范围。

实际系统里还会叠加时间同步、滚快门和畸变残差。它们在像素平面看起来可能只有亚像素级,却不是独立同分布白噪声;当三角化角度不好时,这些系统误差会沿病态方向被放大。于是会出现一个反直觉现象:重投影误差没有明显变坏,三维点云却在深度上“呼吸”。重投影只问点投回相机是否贴近观测,不直接惩罚沿射线方向滑动;角度不足时,沿两条射线共同近似方向移动,投影变化本来就很小。

3. 纯旋转与近纯旋转:有匹配但没有尺度

纯旋转是最干净的退化例子。相机中心不动,所有观测射线只是在同一个中心旋转,几何上没有三角化基线。此时本质矩阵可以从匹配中给出旋转约束,甚至极线 RANSAC 的 inlier 很漂亮,但三维点深度没有尺度信息。若代码只检查 recoverPose 后的正深度比例,很容易被数值噪声制造出的“正深度”骗过。

更麻烦的是近纯旋转。手持相机转身时会有少量平移,轮式机器人原地调整航向时轮滑也会带来非零 。这些平移让公式不再严格退化,但三角化角度仍集中在很小范围;线性求解会吐出有限深度,非线性精炼也能把重投影压低。工程策略应把 translation_norm、旋转角、点的角度分布一起记录:若旋转很大而有效夹角中位数很小,应拒绝创建地图点,等下一段带平移的关键帧,而不是试图靠 BA “炼出”深度。

一个实用门限不是固定写死 ,而是按用途分层。用于 VO 初始化的点,夹角可以略松,但要标记低置信度,不作为尺度强约束;用于局部地图长期保留的点,应要求角度分布和条件数同时达标;用于安全距离或抓取的点,必须经过已知尺度验证。角度门限还要随像素噪声、焦距和目标距离变化,窄视场长焦下同样的像素误差对应更小角度误差,但深度误差仍由射线几何决定。

4. DLT 的条件数:线性解不是几何保证

OpenCV 常见入口是 triangulatePoints,本质是把每个观测写成 ,堆成 ,再用 SVD 取最小奇异值对应的齐次解。DLT 快、稳定、适合初始化,但它的输出质量依赖 的尺度归一化和谱间隔。若最小两个奇异值接近,说明存在一族三维点能给出相近代价;此时取出的那个 只是数值上某个方向的选择,不代表深度可靠。

判断 DLT 是否病态,不应只看 是否接近零或 是否为正。更有用的是记录

大表示整体条件差, 小表示最小奇异向量不稳定。两者都要结合坐标层解释:像素坐标未去畸变、内参尺度混入 、不同相机分辨率未归一化,都会让条件数变坏并污染门限。Hartley normalization 对八点法是常识,对 DLT 三角化同样有意义;至少应确保观测和投影矩阵位于同一层,别把 raw 像素点塞给归一化投影矩阵。

cpp
struct TriangulationQuality {
  double ray_angle_deg = 0.0;
  double cond_A = 0.0;
  double spectral_gap = 0.0;
  double min_depth = 0.0;
};

TriangulationQuality scoreTriangulation(
    const Eigen::MatrixXd& A,
    const Eigen::Vector3d& d0,
    const Eigen::Vector3d& d1,
    double z0,
    double z1) {
  Eigen::JacobiSVD<Eigen::MatrixXd> svd(A);
  const auto s = svd.singularValues();
  TriangulationQuality q;
  const double cos_angle = std::clamp(d0.normalized().dot(d1.normalized()), -1.0, 1.0);
  q.ray_angle_deg = std::acos(cos_angle) * 180.0 / M_PI;
  q.cond_A = s(0) / std::max(s(s.size() - 1), 1e-12);
  q.spectral_gap = s(s.size() - 2) / std::max(s(s.size() - 1), 1e-12);
  q.min_depth = std::min(z0, z1);
  return q;
}

这段代码只是质量记录,不是最终门限。真正上线时要把 ray_angle_deg 的分位数、cond_A 的长尾和失败点的图像位置一起画出来。若坏点集中在画面边缘,优先查畸变和同步;若坏点集中在远处或运动方向,优先查关键帧选择;若所有点条件数突然变坏,通常是坐标层或外参符号出错。

5. 深度、逆深度与锚定视图

三角化结果进入优化后,参数化会决定数值表现。直接优化三维坐标 在距离较近、角度较好时直观;但远点的深度不确定性巨大, 方向的尺度会把 Hessian 拉得很扁。单目 VO 和 SLAM 常用逆深度 ,把远点压到接近零的区域,能让初始化和边缘化更平滑。它没有凭空增加信息,只是把“无穷远附近”的数值尺度处理得更适合优化器。

锚定视图也很关键。以首次观测帧为锚,点可写成

后续观测通过相对位姿投影。若锚定帧本身处在近纯旋转段,或者第一、第二观测夹角很差,逆深度的初值会被拉到错误区域;后续即使出现更好视角,优化也可能因为鲁棒核、边缘化先验和 outlier 策略而不愿大幅移动。工程上我更倾向把“可创建点”和“可延迟候选点”分开:角度差的匹配先作为 bearing track 留存,等到出现足够视角再三角化,而不是先生成一个低质地图点再指望后端修正。

深度参数化还影响协方差解释。三维坐标下的协方差椭球若沿视线很长,转成逆深度后可能数值看起来不大;这不是不确定性消失,而是变量单位换了。把点交给融合、避障或抓取模块时,接口应明确传的是三维协方差、逆深度方差,还是仅传质量等级。最糟糕的接口是只传 Point3d 和一个 reprojection RMS,下游无法知道这个点到底是横向准、深度虚,还是整体都差。

6. 非线性精炼能修什么,不能修什么

非线性三角化通常最小化多视图重投影误差:

相比 DLT,它使用真实投影模型和噪声权重,能处理不同相机焦距、不同测量协方差,也能在初值合理时给出最大似然意义更清楚的解。Ceres 或 g2o 里对单点做几轮 Gauss-Newton,通常比直接信 DLT 更稳,尤其是在多视图数量超过两帧时。

但非线性精炼不能改变可观性。线性化后的正规方程近似为

有很小特征值,说明某个方向上的投影残差变化很慢;优化器可以沿该方向走很远而几乎不增大代价。此时让迭代次数更多、把收敛阈值调小,只会得到一个更精确的病态解。正确做法是在精炼后检查 的谱、协方差主轴和视角分布:若最大方差方向接近平均观测射线,且方差超过用途允许范围,就不要把该点升级为稳定地图点。

鲁棒核也有副作用。坏匹配会被 Huber 或 Cauchy 降权,这是好事;但当视角差、深度方向弱约束时,鲁棒核可能掩盖一两个关键视图的冲突,使点保留在一个看似低代价的位置。多视图三角化应记录每个观测的 whitened residual,而不是只存总 cost。若某个视图角度贡献最大却被鲁棒核强烈降权,剩下视图可能已经不足以约束深度。

7. 协方差传播:把像素噪声变成三维告警

假设每个像素观测噪声为 ,在三角化解 附近线性化投影残差,可得三维点的信息矩阵 。协方差近似为

这里的 是所有视图投影对三维点的导数堆叠。对单个针孔相机,投影对相机系点的导数含有 项;多个相机叠加后,真正提供深度约束的是这些雅可比行在不同视角下的差异。若所有视角几乎相同, 的行空间主要约束垂直于射线的两个方向,沿射线方向的信息很弱。

协方差传播的价值不是追求绝对精确,而是提供可排序的风险指标。可以记录最大特征值 、各向异性比值 、最大特征向量与平均射线的夹角。一个点若重投影 RMS 为 0.3 px,但 对应的标准差是 2 m,就不该参与 20 cm 级避障判断。反过来,近距离纹理点可能重投影 RMS 稍大,却因视角好而三维协方差可接受,应该交给鲁棒后端继续判断。

要注意协方差依赖外参和时间戳。若相机间外参不确定,像素噪声传播只给出了条件协方差;外参误差应通过联合雅可比或额外 margin 体现。多相机 rig 中,基线机械尺寸看似固定,但温漂、快门时差和 rolling shutter 会把射线方向系统性扭曲。对已知尺度验收不达标时,不要先把像素噪声调大来“解释”误差,应先切分相机对、图像区域和时间段,定位是几何退化还是标定残差。

8. 多视图不是越多越稳:角度覆盖要均匀

加入第三、第四个视图通常能改善三角化,但前提是它们提供新的射线方向。如果相机在一条直线上前后移动,且点在运动方向附近,多视图只是重复同一种弱约束;如果机器人沿货架平行移动,货架平面上的特征可能在某个法向上长期不可观。此时 num_observations >= 4 不能替代角度覆盖检查。

我会把每个地图点维护成一个小的观测摘要:最大射线夹角、夹角分位数、观测中心的 PCA 主轴、信息矩阵特征值。PCA 不是为了做漂亮统计,而是区分“多帧但同向”和“多帧且覆盖好”。对长期地图点,只有当新增观测显著改善最小特征值或扩大角度覆盖时,才值得触发重三角化;否则它只是增加计算量,并可能把同步或外点误差带进来。

9. 已知尺度场景的验收切片

上线前需要一个已知尺度场景,而不是只看公开数据集的轨迹指标。可以用标定板、AprilTag 阵列、激光测距平面、全站仪点位,或工位上尺寸可靠的夹具。验收时按三类切片看:第一,按射线夹角分桶,例如 以上,分别统计深度误差和正深度比例;第二,按图像区域切片,中心、边缘、强畸变区分开;第三,按相机运动类型切片,侧向平移、前向平移、近纯旋转分别跑。

重投影误差也要切片,但它只作为一致性指标。一个健康结果应满足:角度增大时深度误差明显下降,协方差预测的高风险点确实覆盖大部分深度外点;低角度桶即使重投影小,也应被质量门限挡在下游之外。若所有角度桶的重投影都小,而深度误差只在某个图像区域变坏,优先查畸变模型或 rolling shutter;若深度误差随角度改善不明显,可能是外参尺度、时间同步或 ground truth 对齐问题。

验收报告不要只给一个 RMSE。更有用的是表格:每个角度桶的点数、中位深度误差、P90 深度误差、reprojection P90、协方差覆盖率、被拒点比例。覆盖率可以这样定义:若预测一维深度标准差为 ,真实误差落在 内的比例是否接近预期。它不是严格统计检验,但能发现“协方差总是过度自信”的实现问题。

如果系统没有可靠真值,也至少要做相对验收:同一段数据用不同关键帧间隔、不同相机对、不同角度门限重放,深度排序和拒绝比例应随几何条件单调变化。若门限越严反而外点越多,通常说明质量指标接错了坐标层,或重三角化使用了不一致的位姿版本。

10. 日志字段与在线告警

三角化质量如果只在离线 notebook 里看,现场排障仍然会慢。建议在地图点创建阶段就持久化最小字段:创建点的相机对、两帧时间差、有效夹角、DLT 条件数、非线性迭代次数、最终重投影分位数、最小 Hessian 特征值、最大协方差主轴和平均射线夹角。字段不必每帧全量上报,可以按点采样或按关键帧聚合,但必须能回放到具体图像区域和运动片段。这样遇到“某个 bag 才炸”的问题时,能直接问坏点来自哪种角度和哪对相机,而不是从后端残差倒推。

在线告警也应分层。轻告警用于提示关键帧策略:连续若干关键帧的角度中位数过低,就延迟初始化或降低地图点创建率。中告警用于保护后端:若新点的条件数长尾突然变粗,暂停把它们设为可边缘化先验,避免病态点被压进滑窗。重告警用于保护下游:若某一距离切片里的深度协方差持续超阈值,避障或抓取模块只能消费横向位置,不能消费绝对深度。告警文案要写成几何事实,例如“前向运动目标角度不足”,不要写成“算法不稳定”,否则现场同事无法决定是换轨迹、等关键帧还是查标定。

还有一个容易遗漏的细节:质量门限要随地图点生命周期变化。新点可以宽进严出,先作为候选 track 观察;进入局部 BA 前要满足角度和谱条件;成为长期地图点前要经过多视图覆盖;被下游请求 metric depth 时再按用途校验协方差。这样的状态机比一次性筛掉所有低角度点更稳,因为它保留了未来视角变好的可能,也避免低质初值提前污染尺度。

多相机 rig 还要把告警按相机对拆开。前左、前右、侧后相机的有效基线方向不同,同一个点在某一对上病态,在另一对上可能很好。若只看全局均值,坏相机对会被健康相机对稀释;若只看单点失败,又很难区分几何角度差和某个外参漂了。按相机对维护角度桶和深度误差桶,能把“换轨迹可解决”和“必须重新标定”分开。

11. 落地策略:先拒绝,再优化

我会把三角化 pipeline 拆成四道门。第一道是几何门:检查有效夹角、cheirality、相机对的基线方向和近纯旋转状态。第二道是数值门:检查 DLT 条件数、谱间隔、齐次坐标尺度和坐标层一致性。第三道是优化门:非线性精炼后检查 per-view residual、Hessian 特征值和协方差主轴。第四道是用途门:根据下游是 VO 初始化、地图维护、抓取还是避障,选择不同的保留等级。

这套策略听起来比一个 min_parallax_deg 麻烦,但它能解释现场问题。看到“基线半米仍深度炸”,先看有效横向基线和射线角度;看到“重投影很好但尺度漂”,看 Hessian 最小特征值和协方差主轴;看到“多帧观测仍不稳”,看角度覆盖而不是观测数量;看到“非线性后更自信但更错”,看鲁棒核降权和外参误差。三角化不是把匹配点变成三维点的格式转换,而是一次可观性审查。只有把角度条件、参数化和协方差一起记录,基线足够却数值病态的点才不会悄悄进入系统核心。

← 全部文章

johan's blog