
双目视觉测距这件事我最早接触是在一个机械臂抓取项目里。当时客户给的需求很朴素让机械臂知道目标物体离它有多远。我第一反应是用单目相机加个已知尺寸的参照物去估算结果实测误差大得离谱稍微换个角度就飘。后来老老实实上了双目方案从标定、校正、匹配到三角测量一步步啃下来才算把距离测准。这套东西的门槛其实不在代码而在数学——很多人调库跑通了demo却说不清视差和深度到底是怎么换算的一旦遇到标定误差、匹配失败就完全抓瞎。所以这篇我想把双目测距的数学推导从头捋一遍再把三维重建的完整链路和资源整理出来适合刚入门立体视觉的工程师、做机器人/自动驾驶感知的同学以及想搞明白为什么这么算的开发者。1. 双目测距到底在测什么从视差到深度的直觉1.1 一个生活化的类比双眼为什么能判断远近你闭上左眼只用右眼看手指再换左眼会发现手指相对背景的位置跳了一下。这个跳动量就是视差。手指离脸越近跳动越大伸到一臂远跳动就很小。大脑正是靠两只眼睛看到的这点位置差异反推出物体的远近。双目视觉测距干的就是同一件事只不过把大脑换成了数学公式把手指换成了图像上的像素点。关键结论先摆在这里深度与视差成反比。视差越大物体越近视差越小物体越远。这个反比关系是整套双目测距的基石后面所有公式都是它的展开。1.2 理想双目模型两个相机平行且等高为了让数学好推我们先假设一个理想情况左右两个相机完全相同焦距一样、内参一样光轴严格平行成像平面共面两个光心之间只差一个水平距离。这个水平距离叫基线记作 (B)。相机焦距记作 (f)。在这个模型下空间中同一个点 (P)在左图像上的横坐标是 (x_l)在右图像上是 (x_r)。因为两个相机只水平错开同一个点在两幅图上的纵坐标是一样的只有横坐标不同。定义视差[ d x_l - x_r ]注意这里 (x_l x_r)因为左相机在左边看到的点偏右右相机看到的点偏左所以视差是正值。1.3 三角测量把相似三角形列出来现在把三维场景投影到俯视图从上往下看左光心 (O_l)、右光心 (O_r)、空间点 (P)、以及 (P) 在两个成像平面上的投影点构成两组相似三角形。设空间点 (P) 到相机光心连线的垂直距离也就是我们要的深度为 (Z)。根据相似三角形关系[ \frac{B}{Z} \frac{d}{f} ]稍微变形就得到深度公式[ Z \frac{f \cdot B}{d} ]这就是双目测距最核心的公式。它清楚地告诉我们三件事深度 (Z) 和视差 (d) 成反比验证了前面的直觉基线 (B) 越大同样视差下能测的距离越远但近处盲区也越大焦距 (f) 越大视场越窄远处分辨率越高。1.4 为什么实际中不能直接用这个公式理想模型假设两个相机完全平行等高现实中你手工装两个相机几乎不可能做到光轴严格平行、成像平面严格共面。哪怕差零点几度视差就会引入系统性偏差深度直接算错。所以真实流程里我们必须先做标定求出两个相机的真实内外参再做立体校正把两幅图重投影成虚拟的平行等高相机拍出来的样子。校正之后上面这个简单公式才能用。这一步是很多人忽略的关键也是后面第3节要重点讲的。2. 数学推导的完整链路从针孔模型到深度公式2.1 针孔相机模型三维点怎么变成像素要理解双目先得理解单目。针孔模型描述的是空间点 (P(X,Y,Z)) 经过相机投影落到图像平面上的像素坐标 ((u,v))。写成矩阵形式[ \begin{bmatrix} u \ v \ 1 \end{bmatrix} \frac{1}{Z} \begin{bmatrix} f_x 0 c_x \ 0 f_y c_y \ 0 0 1 \end{bmatrix} \begin{bmatrix} X \ Y \ Z \end{bmatrix} ]中间那个 3×3 矩阵就是内参矩阵(K)。(f_x, f_y) 是焦距以像素为单位(c_x, c_y) 是主点光轴与图像平面的交点通常在图像中心附近。这里除以 (Z) 体现了近大远小的透视效应。内参怎么来的靠标定。常用棋盘格标定板拍十几到几十张不同角度的图用张正友标定法解出来。内参一旦标定好只要不换镜头、不调焦基本可以长期使用。2.2 外参两个相机之间的相对位姿双目系统里我们通常把左相机当作世界坐标系原点那么右相机相对左相机有一个旋转 (R) 和平移 (t)。这个 ((R, t)) 就是外参描述两个相机怎么摆放的。对于理想平行双目(R) 是单位矩阵(t (-B, 0, 0)^T)也就是只沿 x 轴平移了一个基线。一般情况下的外参标定是通过同时看到标定板的左右图像分别求出各自相对标定板的位姿再相减得到相对位姿。这一步的精度直接决定后面测距的精度标定板姿态要尽量覆盖整个视场别只在一个小区域晃。2.3 从一般模型推导视差-深度关系现在把一般情况写出来。空间点 (P) 在左相机坐标系下是 (P_l)在右相机坐标系下是 (P_r)两者关系[ P_r R \cdot P_l t ]经过立体校正后我们可以把两个相机都虚拟旋转到一个共面平行的姿态。校正的本质是找两个旋转矩阵 (R_l, R_r)使得校正后两相机光轴平行、成像平面共面。校正后同一个空间点在左右图上的纵坐标相等视差只体现在横坐标上于是又回到了第1节的简单公式[ Z \frac{f \cdot B}{d} ]所以整个推导的逻辑链是针孔模型描述单相机投影 → 外参描述双相机关系 → 立体校正把一般情况化为理想情况 → 三角测量得到深度。每一步都不能跳跳了就会在工程上出问题。2.4 深度精度与视差精度的关系这里有个非常实用的推导。对 (Z fB/d) 两边求微分[ \frac{\partial Z}{\partial d} -\frac{f \cdot B}{d^2} ]取绝对值深度误差和视差误差的关系是[ \Delta Z \frac{f \cdot B}{d^2} \cdot \Delta d \frac{Z^2}{f \cdot B} \cdot \Delta d ]这个公式信息量极大深度误差与距离的平方成正比。也就是说物体越远误差增长得越快。距离翻倍误差变四倍。这解释了为什么双目在近处很准远处就拉胯。想测远要么加大基线 (B)要么提高视差精度 (\Delta d)亚像素匹配要么增大焦距 (f)。举个具体数字假设 (f 1000) 像素(B 0.1) 米视差精度 (\Delta d 0.5) 像素。在 (Z 1) 米处(\Delta Z 1^2 / (1000 \times 0.1) \times 0.5 0.005) 米也就是 5 毫米。在 (Z 5) 米处(\Delta Z 25 / 100 \times 0.5 0.125) 米误差直接飙到 12.5 厘米。这个数量级一定要心里有数选型时先算一遍。3. 立体校正与极线约束让匹配从二维降到一维3.1 极线约束为什么匹配不用全图搜没校正之前左图上一个点要在右图里找对应点理论上得在整幅图里搜计算量爆炸。极线约束告诉我们左图上的一个点它在右图上的对应点一定落在一条直线上这条线叫极线。原因是左相机光心、空间点、右相机光心构成一个平面这个平面和右成像平面的交线就是极线。有了这个约束搜索从二维降到一维效率提升几个数量级。但一般情况下的极线是斜的不同点的极线方向还不一样处理起来麻烦。3.2 立体校正把极线变成水平线立体校正的目标就是通过重投影让所有极线都变成水平且平行的线。校正后左图上的点 ((u, v))它的对应点一定在右图的第 (v) 行上。这样匹配就变成了在同一行上找最相似的点实现简单、速度快还能用很多成熟的块匹配算法。校正的数学本质是给每个相机找一个旋转矩阵把光轴转到平行同时调整内参让成像平面共面。OpenCV 里的stereoRectify就是干这个的它会输出四个矩阵左右相机的校正旋转、新的投影矩阵以及一个重映射查找表。之后用initUndistortRectifyMap生成映射表remap把原图重映射成校正图。3.3 校正的实操细节与常见坑校正这一步我踩过不少坑说几个关键的标定图像质量决定一切。棋盘格要清晰、平整别用打印出来皱巴巴的纸。角点检测不准后面全白搭。标定姿态要多样。标定板要覆盖图像的四个角、中心还要有不同倾斜角度。只在中间平放拍十几张标出来的畸变系数基本没用。校正后一定要肉眼检查。把校正后的左右图叠在一起看同一水平线上左右图的特征是否对齐。如果明显错位说明标定或校正有问题别急着往下走。图像尺寸变了要重新标定。有人把标定好的参数直接用在缩放后的图上内参没同步缩放结果全错。提示校正效果可以用画水平线的方式快速验证。在校正后的左图上每隔几十行画一条水平线看右图对应行上的同一物体是否落在同一条线上。对齐了就说明校正成功。4. 立体匹配视差图是怎么算出来的4.1 匹配的本质找最像的点校正之后对左图每个像素在右图同一行的某个范围内找最相似的像素两者的横坐标差就是视差。相似度怎么衡量常用的是绝对差之和SAD、平方差之和SSD、归一化互相关NCC等。搜索范围由minDisparity和numDisparities决定前者是最小视差后者是视差搜索范围。4.2 局部匹配 vs 全局匹配局部匹配只看像素周围一个小窗口速度快但噪声大、弱纹理区域容易出错。全局匹配把整幅图的平滑性作为约束一起优化效果好但慢。工程上常用的折中是半全局匹配SGMOpenCV 里的StereoSGBM就是它的实现在精度和速度之间平衡得不错是我最常用的选择。4.3 SGBM 关键参数怎么调StereoSGBM的参数多但真正影响大的就几个参数作用经验取值minDisparity最小视差通常 0若相机有会聚角可设负值numDisparities视差搜索范围必须是 16 的倍数按最近距离估算blockSize匹配块大小3~11 的奇数越大越平滑但丢细节P1, P2平滑惩罚项P18×通道×blockSize²P232×通道×blockSize²uniquenessRatio唯一性比率5~15越大越严格滤掉歧义匹配speckleWindowSize斑点滤波窗口50~200去掉小面积噪点speckleRange斑点视差范围1~2控制斑点合并numDisparities怎么估用公式 (d fB/Z_{min})(Z_{min}) 是你要测的最近距离。比如 (f1000)(B0.1)想测最近 0.5 米那 (d 1000 \times 0.1 / 0.5 200)取 16 的倍数就是 208 或 224。4.4 视差图的后处理原始视差图通常有很多空洞和噪点尤其在弱纹理、反光、遮挡区域。后处理步骤一般包括左右一致性检查分别以左图和右图为基准算视差互相验证不一致的标记为无效。这一步能去掉大部分遮挡区域的错误。亚像素插值在整数视差基础上做抛物线拟合把视差精度提到 0.1~0.5 像素直接提升深度精度。空洞填充对无效区域用周围有效视差做插值或中值滤波填补。中值滤波去掉孤立的噪点。做完这几步视差图会干净很多深度图质量也随之提升。5. 三维重建从深度图到点云5.1 反投影像素加深度变三维点有了深度图每个像素 ((u, v)) 对应一个深度 (Z)就能反投影回三维空间。用内参矩阵的逆[ \begin{bmatrix} X \ Y \ Z \end{bmatrix} Z \cdot K^{-1} \cdot \begin{bmatrix} u \ v \ 1 \end{bmatrix} ]展开就是[ X \frac{(u - c_x) \cdot Z}{f_x}, \quad Y \frac{(v - c_y) \cdot Z}{f_y}, \quad Z Z ]对每个有效像素做这个运算就得到一堆三维点也就是点云。OpenCV 里可以用reprojectImageTo3D一次性完成输入视差图和 Q 矩阵stereoRectify输出直接得到三维坐标图。5.2 点云滤波与可视化原始点云通常很密、有噪声、还有离群点。常用处理体素下采样把空间划分成小立方体每个立方体只保留一个点降低密度。统计离群点移除统计每个点到邻居的平均距离超过阈值的当离群点删掉。直通滤波按坐标范围裁剪比如只保留某个距离区间内的点。可视化可以用 Open3D、PCL 或者 MeshLab。Open3D 的 Python 接口最友好几行代码就能把点云显示出来并旋转查看。5.3 从点云到网格三维重建的下一步点云只是离散的点要做成可用的三维模型还得重建表面。常用方法泊松重建适合封闭、平滑的物体生成的网格质量高。贪婪三角化速度快适合表面比较规则的场景。Delaunay 三角化对点云做三角剖分适合二维平面附近的数据。如果做的是结构光三维重建原理和双目类似只是把另一个相机换成了投影仪主动投射编码图案解决了双目在弱纹理区域匹配困难的问题。结构光的精度通常比被动双目高一个量级但设备更贵、对环境光更敏感。6. 资源清单与选型建议6.1 开源库与工具工具用途特点OpenCV标定、校正、匹配、反投影全流程覆盖文档全首选Open3D点云处理与可视化Python 友好算法丰富PCL点云处理C 为主功能全但上手陡MeshLab网格查看与编辑图形界面适合检查结果COLMAP运动恢复结构、多视图重建学术级精度高6.2 标定板与硬件选择标定板推荐用玻璃基板或铝基板的棋盘格平整度好。打印的纸板容易翘精度差。棋盘格方格大小要匹配拍摄距离一般让棋盘格占图像宽度的 1/3 到 1/2。相机选型看需求测近处用短基线小焦距测远处用长基线大焦距。基线太大会导致近处盲区变大两个相机看不到同一个近处物体就没法匹配。一般基线取预期工作距离的 1/10 到 1/5 比较稳妥。6.3 学习路径建议如果你是新手我建议按这个顺序走先用 OpenCV 跑通单目标定理解内参和畸变再做双目标定和校正肉眼验证校正效果然后跑 SGBM 出视差图调参数最后反投影出点云用 Open3D 看效果。每一步都别跳跳了后面出问题你根本不知道是哪一步的锅。7. 实操中最容易翻车的几个点7.1 标定误差的累积效应标定是整个链路的地基。标定不准后面校正、匹配、测距全歪。我见过有人标定完不检查重投影误差直接往下做结果测距误差几十厘米还找不到原因。标定完一定要看重投影误差一般要小于 0.5 像素最好小于 0.3。如果误差大重新拍标定图检查角点检测是否正确。7.2 弱纹理与重复纹理的匹配失败白墙、纯色桌面这种弱纹理区域局部匹配基本失效视差图一片空洞。解决办法一是用 SGBM 这种带平滑约束的算法二是加纹理比如往被测物上投影随机散斑三是用结构光主动投射图案。重复纹理比如瓷砖、栅栏会导致匹配到错误的位置产生鬼影视差这时候唯一性比率参数要调高配合左右一致性检查过滤。7.3 遮挡区域的无效视差两个相机位置不同必然存在一些区域左相机能看到、右相机看不到反之亦然这些区域没法匹配视差无效。这是原理性的限制没法完全消除只能通过减小基线、增加相机数量来缓解。做点云时这些区域会出现空洞后处理时要注意别把空洞当成真实几何。7.4 深度误差随距离平方增长前面推导过 (\Delta Z Z^2 \Delta d / (fB))这个规律一定要刻在脑子里。很多人拿双目去测几十米外的物体发现误差好几米还以为是算法问题其实是物理限制。要测远就得加大基线或提高视差精度没有捷径。8. 一些个人的经验体会双目视觉这套东西我最大的感受是数学是根工程是叶。公式推导清楚了遇到问题你才知道往哪个方向调。比如测距不准你会先想是标定问题、校正问题还是匹配问题而不是盲目换算法。再比如选基线你会先算一遍精度需求而不是拍脑袋定。还有一个体会是别迷信一键标定的工具。工具能帮你省事但标定质量好不好还得你自己判断。重投影误差、校正后的对齐效果、视差图的质量这些都得肉眼过一遍。我见过太多人跑完工具就直接用结果精度差得离谱。最后说个实用的小技巧调试阶段可以在场景里放几个已知距离的标记物测出来的深度和真实距离对比快速判断整套系统准不准。这个方法比看任何指标都直观。等系统调稳了再撤掉标记物正式跑。