双目立体视觉这个方向,我断断续续折腾了快两年,从最开始连视差和深度都分不清,到后来能自己搭一套完整的测距流程,中间踩的坑实在太多了。这篇笔记不是教科书式的推导,而是把我自己学习过程中真正卡住的地方、想明白的关键点、以及那些"当时要是有人告诉我就好了"的经验,尽量完整地摊开讲一遍。如果你刚接触双目视觉,或者已经看过一些资料但总觉得隔着一层窗户纸,那这篇内容应该能帮你少走不少弯路。
双目立体视觉要解决的问题其实很朴素:用两个摄像头模拟人的双眼,通过同一场景在两个视角下的位置差异,反推出物体离我们有多远。它能做三维重建、深度测量、障碍物检测、尺寸测量这些事,适合做机器人、自动驾驶感知、工业检测、AR/VR 以及任何需要"知道距离"的场景。入门门槛不算高,但想真正跑通并跑准,里面的细节比想象中多得多。
1. 双目立体视觉到底在做什么
1.1 从"两只眼睛"到"一个深度值"
人为什么能判断远近?因为左右眼看到的画面有细微差别,大脑根据这个差别算出距离。双目视觉干的就是这件事,只不过把大脑换成了算法。
核心逻辑可以用一句话概括:同一个空间点,在左右两个相机里成像的位置不一样,这个位置差(视差)越大,说明物体越近;视差越小,说明物体越远。深度和视差成反比关系,这是整个双目视觉的基石。
我第一次理解这个反比关系时,用了一个很土但很有效的类比:你伸出一根手指放在眼前,闭左眼和闭右眼,手指相对背景跳动的幅度很大;把手指伸远,再交替闭眼,跳动幅度就小了。跳动幅度就是视差,手指越远视差越小。这个直觉一旦建立,后面所有公式都只是把这个直觉量化而已。
1.2 为什么必须做"标定"和"校正"
理想情况下,我们希望两个相机是完全平行、水平对齐的,这样同一个点在左右图里只会在水平方向上有位移,找对应点就变成"在同一行里找"。但现实是,你手工装的两个相机,几乎不可能做到完全平行,总会有微小的旋转和偏移。
所以流程里有两个绕不开的步骤:
- 标定(Calibration):算出每个相机的内参(焦距、主点、畸变系数)和两个相机之间的外参(旋转、平移关系)。
- 校正(Rectification):利用标定结果,把两张图重新投影到"虚拟的平行相机"视角下,让对应点落在同一水平线上。
提示:很多人一上来就想跳过标定直接算深度,结果视差图乱七八糟。标定质量直接决定最终精度上限,这一步偷懒,后面全白搭。
1.3 一个必须记住的核心公式
深度计算公式是整个系统的灵魂:
Z = (f × B) / d
其中 Z 是深度(距离),f 是焦距(像素单位),B 是基线(两个相机光心的距离),d 是视差(同一像素点在左右图的水平坐标差)。
这个公式告诉我们几件很关键的事:
- 基线 B 越大,同样视差下能测的距离越远,但近处盲区也越大。
- 焦距 f 越大(长焦),远处精度越高,但视野越窄。
- 视差 d 的精度直接决定深度精度,而 d 通常只能精确到亚像素级别。
我实测下来,一个基线 60mm、焦距约 700 像素的双目系统,在 1 米处的深度误差大概在几毫米到一两厘米之间,具体取决于标定和匹配质量。这个数量级心里要有数,别指望几十块的摄像头做到毫米级。
2. 硬件选型与搭建的那些门道
2.1 相机怎么选:不是越贵越好
入门阶段最容易犯的错,是以为买两个贵相机就能出好效果。实际上双目系统对"两个相机的一致性"要求,远高于对单个相机绝对性能的要求。
选型时我关注这几个点:
| 关注项 | 为什么重要 | 入门建议 |
|---|---|---|
| 是否可同步触发 | 运动场景下不同步会导致视差错误 | 优先选支持硬件同步的型号 |
| 是否同型号同批次 | 内参差异小,标定更稳 | 尽量买同一批次两只 |
| 全局快门 vs 卷帘快门 | 卷帘在运动时有果冻效应 | 有运动物体就选全局快门 |
| 分辨率 | 影响视差精度和计算量 | 入门 640×480 到 1280×720 够用 |
| 镜头可换性 | 决定基线灵活度 | M12 接口比较通用 |
我一开始用的是两个普通 USB 摄像头,结果发现它们根本没法硬件同步,拍静态场景还行,一旦有运动物体,左右图就对不上。后来换成支持同步的模组,问题立刻缓解。这个坑非常典型,如果你的场景里有运动,同步是刚需,不是可选项。
2.2 基线怎么定:一个权衡题
基线 B 是双目系统里最需要动脑子设计的参数。它不是一个"越大越好"的量。
- 基线太小:远处精度差,因为视差本来就小,噪声占比高。
- 基线太大:近处出现"盲区",两个相机看不到同一块区域,而且匹配难度上升。
一个经验公式是:基线大约等于你想测的最远距离的 1/20 到 1/50,同时要保证最近距离处两个相机仍有足够重叠视野。
举个例子,如果你想测 0.5 米到 5 米的范围,基线取 60mm 到 100mm 比较合适。我做过一个 200mm 基线的系统,测 5 米外的东西确实准,但 0.5 米内的物体基本废掉,因为左右图重叠区域太小了。
注意:基线一旦固定,系统的"舒适测距区间"就基本定了。别想着一个基线通吃所有距离,那不现实。
2.3 结构件与装配:精度藏在细节里
两个相机的相对位置必须刚性固定,任何松动都会让标定参数失效。我用过 3D 打印的支架,也用过铝型材,结论是:铝型材或金属件更靠谱,3D 打印件受温度和应力影响会轻微变形。
装配时还有几个细节:
- 两个相机尽量保持光轴平行,减少校正时的畸变压力。
- 螺丝要拧紧,但别用力过猛压坏模组。
- 装好后拍一张标定板,看看左右图里标定板是否大致水平对齐,能提前发现大问题。
我踩过的一个坑是:支架装好后没锁死,标定完过两天再测,深度全飘了。后来养成习惯,标定前后都拍一张固定场景做对比,一旦发现漂移就重新标定。
3. 标定:整个流程里最不能省的一步
3.1 标定板的选择与拍摄技巧
标定板常用棋盘格或圆点阵列。棋盘格最经典,OpenCV 支持也最好。我用的是 9×6 的棋盘格,方格边长 25mm,打印在哑光纸上再贴到硬板上,避免纸张弯曲。
拍摄标定图有几个要点,这些是我反复试出来的:
- 数量:15 到 25 张比较稳妥,太少参数不稳,太多收益递减。
- 姿态:标定板要覆盖画面各个区域,包括四个角和中心,还要有不同倾斜角度。
- 清晰:避免运动模糊,最好用三脚架固定相机,移动标定板。
- 光照:均匀照明,避免反光和阴影,否则角点检测会出错。
提示:拍完一定要肉眼检查每张图的角点检测结果。OpenCV 的 findChessboardCorners 偶尔会漏检或错检,一张坏图就能把标定结果带偏。
3.2 标定流程与参数解读
标定分两步:先单目标定得到每个相机的内参和畸变,再双目标定得到两个相机之间的旋转和平移。
用 OpenCV 的话,核心就是calibrateCamera和stereoCalibrate两个函数。标定完你会拿到这些关键参数:
- 内参矩阵:包含焦距 fx、fy 和主点 cx、cy。
- 畸变系数:通常是 k1、k2、p1、p2、k3 五个。
- 外参:旋转矩阵 R 和平移向量 T,T 的模长就是基线。
重投影误差是判断标定质量的核心指标。一般来说,平均重投影误差小于 0.5 像素算不错,小于 0.3 像素算很好。如果超过 1 像素,基本可以判定标定有问题,得回去查标定图。
我第一次标定误差到了 1.5 像素,排查半天发现是标定板打印后没贴平,中间鼓起来了。重新做了一块平整的板,误差直接降到 0.3 像素。标定板本身的平整度,比你想的重要得多。
3.3 校正与查看效果
标定完要做立体校正,把左右图重投影成平行视角。OpenCV 里用stereoRectify加initUndistortRectifyMap,然后对每帧图做remap。
校正效果好不好,有个很直观的检查方法:在校正后的左右图上画水平线,同一个物体特征应该落在同一行上。如果明显错行,说明校正有问题。
我习惯把校正后的左右图并排显示,再叠加几条水平参考线,一眼就能看出对齐情况。这个可视化习惯帮我省了很多调试时间。
4. 立体匹配:从两张图到一张深度图
4.1 匹配到底在匹配什么
校正之后,同一个点在左右图里只差一个水平位移,也就是视差。立体匹配的任务就是:对左图里的每个像素,在右图同一行里找到它对应的像素,算出视差。
听起来简单,做起来难。难点在于:
- 无纹理区域(比如白墙)找不到可靠对应点。
- 重复纹理(比如栅栏)容易匹配错。
- 遮挡区域在另一张图里根本不存在。
- 光照差异会让同一块区域看起来不一样。
4.2 经典算法与参数调优
入门阶段我建议先用 OpenCV 自带的 SGBM(半全局块匹配),它平衡了效果和速度,参数也相对好调。关键参数有这么几个:
| 参数 | 作用 | 调优经验 |
|---|---|---|
| minDisparity | 最小视差 | 通常设 0,除非有特殊裁剪 |
| numDisparities | 视差搜索范围 | 必须是 16 的倍数,越大越慢 |
| blockSize | 匹配块大小 | 奇数,纹理少调大,细节多调小 |
| uniquenessRatio | 唯一性阈值 | 调大能减少误匹配,但会丢点 |
| speckleWindowSize | 斑点过滤窗口 | 去掉小面积噪声块 |
| speckleRange | 斑点视差范围 | 配合上一项使用 |
我调参的顺序一般是:先定 numDisparities(根据最近最远距离估算),再调 blockSize,最后用 uniquenessRatio 和 speckle 过滤去噪。
numDisparities 怎么估?用公式反推:d = f × B / Z。比如 f=700,B=60mm,想测最近 0.5 米,那 d = 700×60/500 ≈ 84,所以 numDisparities 至少设到 96 或 112。
注意:numDisparities 设得过大,不仅慢,还会引入更多误匹配。够用就行,别盲目拉满。
4.3 视差图后处理
原始视差图通常很脏,需要后处理:
- 左右一致性检查:左图算出的视差和右图反算的视差差异过大就标记为无效,能有效去掉遮挡区错误。
- 亚像素插值:把整数视差细化到小数,提升深度精度。
- 空洞填充:对无效区域做插值,但要注意别把错误也填进去。
- 滤波:中值滤波或双边滤波去噪,双边滤波能保边。
我一般会保留一张"有效掩码",明确哪些像素的深度是可信的。下游用的时候只看掩码内的值,比盲目相信整张图靠谱得多。
5. 从视差到深度:精度与误差分析
5.1 深度计算与可视化
拿到视差图后,逐像素套公式 Z = f×B/d 就能得到深度图。但要注意,这里的 f 是校正后的焦距,B 是校正后的基线,不能直接用标定前的原始值。
深度图可视化时,我习惯用伪彩色映射,近处暖色远处冷色,一眼就能看出距离分布。但伪彩色只是给人看的,实际计算一定要用原始浮点值。
5.2 误差从哪来
深度误差不是均匀的,它和距离的平方成正比。对公式求导可以得到:
ΔZ = Z² × Δd / (f × B)
这个式子说明:距离越远,同样的视差误差导致的深度误差越大。这就是为什么双目在远处精度会急剧下降。
举个例子,f=700,B=60mm,视差误差 Δd=0.5 像素:
- 在 1 米处:ΔZ = 1000²×0.5/(700×60) ≈ 11.9mm
- 在 3 米处:ΔZ = 3000²×0.5/(700×60) ≈ 107mm
差距接近十倍。所以设计系统时,一定要明确"我主要关心哪个距离段",然后针对性地选基线和焦距。
5.3 提升精度的几个实操手段
- 提高标定质量:这是性价比最高的手段,误差能从源头降下来。
- 亚像素匹配:把视差精度从 1 像素提到 0.1 到 0.25 像素。
- 增大基线:直接提升远处精度,但要权衡盲区。
- 多帧融合:静态场景下多帧平均,能显著降噪。
- 结构光辅助:主动投射纹理,解决无纹理区域匹配难题。
我在一个静态测量项目里用了多帧平均,20 帧平均后深度噪声降了大概一个数量级,效果非常明显。代价是只能用于静态场景。
6. 常见问题与排查速查
6.1 典型问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 深度整体偏移 | 标定不准或基线错误 | 检查重投影误差和 T 向量 |
| 视差图大片空洞 | 无纹理或匹配范围不足 | 增大 numDisparities,加纹理 |
| 边缘深度跳变 | 遮挡区误匹配 | 开左右一致性检查 |
| 远处深度噪声大 | 视差精度不足 | 增大基线或亚像素优化 |
| 左右图对不齐 | 校正失败 | 重做标定,检查校正映射 |
| 运动场景错乱 | 相机不同步 | 改用硬件同步触发 |
6.2 几个我踩过的坑
坑一:忽略镜头畸变。广角镜头畸变很大,不校正的话边缘区域视差全错。一定要用畸变系数做去畸变。
坑二:标定板和实际场景距离不匹配。如果你在 0.5 米标定,却用来测 5 米,精度会打折。标定距离尽量覆盖工作距离。
坑三:以为视差图越密越好。实际上密集视差图里很多是错的。宁可要稀疏但准确的视差,也不要密集但充满错误的视差。
坑四:忘了时间同步。动态场景里,左右图哪怕差几毫秒,快速运动的物体视差就错了。
6.3 调试小技巧
- 用一张固定场景反复测,建立"基准深度",任何改动后对比基准,能快速发现回归。
- 把中间结果(校正图、视差图、深度图)都可视化出来,别只看最终结果。
- 记录每次标定的参数和误差,形成自己的标定档案,方便追溯。
7. 进阶方向与扩展思路
7.1 从传统方法到深度学习
传统 SGBM 在弱纹理和重复纹理场景下确实吃力。近几年基于深度学习的立体匹配(如端到端的视差网络)在这些场景下表现好很多,代价是需要训练数据和算力。
入门阶段我还是建议先把传统方法跑通,理解视差、标定、校正这些基础概念。等基础扎实了,再上深度学习,你会更清楚网络在解决什么问题,而不是把它当黑盒。
7.2 主动双目与结构光
如果场景本身没纹理,可以主动投射随机散斑或条纹,人为制造纹理,这就是主动双目。它把"无纹理"这个最大难题直接绕过去了,工业上用得很多。
7.3 与其他传感器融合
双目加 IMU 可以做视觉惯性里程计,双目加激光雷达可以互补。双目在近处纹理丰富时精度高,激光在远处和弱纹理时更稳,融合起来能覆盖更广的场景。
8. 给入门者的学习路径建议
如果你现在从零开始,我建议按这个顺序走:
- 先理解原理:把视差、深度、基线、焦距的关系彻底搞明白,能手推公式。
- 跑通标定:用 OpenCV 完成一次完整标定,把重投影误差压到 0.5 像素以内。
- 实现校正:看到左右图对齐的效果,建立信心。
- 跑通 SGBM:得到第一张视差图,哪怕很脏。
- 调参优化:针对自己的场景调 blockSize、numDisparities 等。
- 算深度并验证:用已知距离的物体验证精度。
- 再考虑进阶:深度学习、主动双目、多传感器融合。
每一步都别跳,尤其是标定和校正。我见过太多人直接跳到匹配,结果卡在"为什么深度全是错的",回头发现是标定就没做好。
最后分享一个我自己的习惯:每完成一个阶段,就把当前系统的能力边界写下来——能测多远、精度多少、什么场景会失效。这份"能力清单"比任何参数都更能帮你判断系统是否适合某个新任务。双目视觉不是万能钥匙,知道它什么时候不灵,和知道它什么时候灵同样重要。