双目相机测距这几年在机器人避障、AR交互、工业测量这些场景里出现的频率越来越高,很多人一上来就想知道:两个摄像头拍两张图,到底怎么算出距离的?说实话,我第一次接触双目测距的时候也看了不少资料,但很多文章要么只讲公式,要么贴一段OpenCV代码就跑,最后连标定为什么要做都没说清楚。这篇就把双目相机测距从原理到落地完整拆一遍,包括三角测量怎么推导、标定怎么做、实测中会踩什么坑,以及它和单目测距、超声波测距这些方案的实际区别。想自己搭一套测距系统,或者只想搞明白D435这类深度相机内部是怎么回事的,这篇都适合你。
1. 双目测距的核心设计思路
1.1 为什么选择双目方案
先聊一个很实际的问题:测距方案那么多,什么场景下才值得上双目?
超声波测距大家很熟,原理简单,一个发射头一个接收头,算时间差就能得到距离,成本极低。但它的局限也很明显:波束角大,方向性差,测一个平面还行,想测量物体边缘轮廓就力不从心了,而且对吸音材料、曲面物体经常给出离谱的读数。单目测距靠的是先验尺寸或地面假设,本质是在“猜”,换个没见过的东西基本失效。激光雷达精度高,但成本摆在那里,不是所有项目都舍得用。
双目相机测距走的是另一条路:不主动发射任何信号,而是模拟人眼,用两个固定间距的摄像头同时拍摄同一场景,通过左右两幅图像中同一物体的像素偏移来计算深度。它不依赖物体类型,不需要额外发射功率,只要场景里有纹理就能出深度。一台几百块的双目相机,配上标定板和自己写的匹配算法,就能在几米范围内实现厘米级测距,这个性价比确实很有吸引力。
当然,双目不是万能的。纯黑墙面、白墙这类无纹理区域,匹配算法会直接抓瞎;远距离超过基线长度限制后,精度下降非常快;计算量也比单目大不少。所以双目适合的,是“需要被动测距、纹理环境可控、预算有限”的场景,比如室内机器人避障、手势识别、近距离三维重建。
1.2 三角测量原理:核心公式是怎么来的
双目测距的底层原理就是三角测量,几何上非常朴素。
假设左右两台相机的光轴平行,焦距都是 f,两个相机光心之间的水平距离叫基线 B。空间中某一点 P,它在左相机成像平面上的位置是 x_L,在右相机上是 x_R。因为两个相机从不同角度观察 P 点,所以它在两幅图像里的横坐标是不一样的,这个差值 d = x_L - x_R 就是视差(disparity)。
利用相似三角形,可以得到深度 Z 的公式:
Z = (B * f) / d这个公式是整个双目测距的基石,看起来简单,但里面每个量都有讲究。焦距 f 不是镜头参数上标的那几个毫米值,而是以像素为单位的等效焦距,由相机内参给出;视差 d 的单位也是像素。所以算出来的深度 Z 是米或毫米这种真实尺度。
如果两台相机光轴不完全平行,上面的公式就要修正。实际标定的作用,就是把相机从“不平行的真实状态”校正到“完全平行的理想状态”,保证左右图的行对准,这样视差搜索才能沿水平方向一维进行,计算量大减。
我把这个推导过程用大白话再捋一遍:你把左眼闭上看一个近处的杯子,再换成右眼闭上看,会发现杯子在视野里的位置跳了一下,这个跳动的幅度就是视差。手臂伸长拿远一点,跳动幅度变小。所以视差越大,物体越近,反比关系。双目相机的标定和匹配,本质就是在帮机器自动完成“看左眼、看右眼、对比位置差”这件事。
1.3 完整测距流程包含哪些环节
一套完整的双目测距系统,从零搭建的话大致分四个环节:标定、校正、立体匹配、深度计算。
标定是第一步,也是最影响结果的一步。通过拍摄多组不同姿态的棋盘格,解算出左右相机的内参(焦距、主点、畸变系数)和双相机之间的外参(平移、旋转)。标定板角点检测的质量直接决定内外参是否可靠,这也是很多初学者容易忽略的地方——张正友标定法虽然用起来方便,但拍得不合格照样出垃圾结果。
校正环节利用标定得到的参数,对左右图像做畸变消除和立体校正,使得左右图同名点在水平方向保持对齐。这一步OpenCV里用stereoRectify配合initUndistortRectifyMap完成,输出的重投影矩阵 Q 在后面直接用于计算深度。
立体匹配是计算的核心,在左右校正图上搜索每个像素的对应点,生成视差图。匹配算法从简单的块匹配BM到全局匹配SGBM都有,精度和速度的取舍需要按场景考量。
最后一步就简单了,拿到视差图和 Q 矩阵,做一次reprojectImageTo3D,就能得到每个像素的三维坐标,距离自然就出来了。也可以用公式 Z = f*B/d 逐点算,效果一样。
2. 硬件选型、相机标定与关键参数解析
2.1 双目相机的选型思路
市面上做双目测距的硬件大致分三类:自己拼装的双目模组、成品USB双目相机、集成深度计算芯片的一体机。
自己拼装最省钱,两个普通USB摄像头固定在一块铝型材或3D打印支架上就行,基线长度自己定。但麻烦在于两个摄像头需要外部触发同步,否则运动场景下左右图时间不一致,测距会出重影。USB2.0带宽有限,高分辨率高帧率同时开两个摄像头带宽经常不够,得降分辨率妥协。
成品USB双目相机省心不少,常见的比如基于OV5640/AR0134传感器的模组,出厂做过基本校正,提供SDK和示例代码。这类相机适合快速原型验证,但要注意厂家标定的质量参差不齐,很多出厂标定只是“能出图”,不是“能出准”。
Intel RealSense D435 是深度一体机的代表,内部包含红外投影仪和两个红外相机,加上专用深度处理芯片。它的核心思路是主动立体:在双目匹配的基础上,用红外散斑投射器往场景里打纹理,解决无纹理区域的匹配问题,所以室内弱纹理环境比纯被动双目表现好很多。不过红外投影仪在强阳光下会被环境红外光淹没,室外效果反而打折扣,这个后面会详细说。
选型时基线长度要按测距范围估算。基线越长,相同视差误差引起的深度误差越小,远距离测距越准;但基线拉长后近距离物体在左右图上的视差过大,匹配窗口搜不到,近处会有一片黑洞。所以短基线适合近距离高精度,长基线适合中远距离,所谓“合适”完全取决于你的应用场景。
2.2 相机标定的实操流程
标定是整个双目系统里最像“做实验”的环节,下面是一套我实测下来比较稳的流程。
打印一张棋盘格标定板,我用的是10x7内角点的棋盘格,每个格子边长25mm,用A4纸打印后贴到硬纸板上。不建议直接用屏幕显示棋盘格,因为屏幕反光和尺寸精度都会影响角点坐标的准确性。
拍摄时固定相机,把标定板放在不同距离、不同角度下拍20到30组左右图像。记住几个原则:标定板要占画面面积的1/4到1/3左右,太小了角点信息不足,太大了容易出框;倾斜角度不要超过45度,否则角点检测容易失败;必须在同一光照下拍完一轮,中间别来回开关灯,不然局部过曝或过暗都会让角点坐标偏移。
采集完成后,先用cv2.findChessboardCorners检测角点,再用cv2.cornerSubPix做亚像素细化,最后把左右图一一对应的角点坐标交给cv2.stereoCalibrate解算。这里特别强调一下:左右图像序列必须按帧一一配对,我之前就见过有人左右图各拍各的,数量还不一致,标定结果自然是乱的。
标定结果出来后,重投影误差一般控制在0.1到0.3像素之间算正常。如果误差超过0.5像素,基本可以判断采集图像里混了废片,要么是运动模糊,要么是标定板不平整。
2.3 剔除不合格角点:容易忽略却致命的细节
很多资料只讲“拍20张图,标定,完事”,但实际操作中你会发现,拍摄的20组图像里可能有一半角点检测是有问题的。把不合格角点直接喂给stereoCalibrate,就算重投影误差看着还行,深度精度也是稀烂。
我现在的做法是在标定前加一道筛选逻辑。对每组左右图像,检测出角点后计算一个重投影误差,但更实用的方法是直接看角点坐标有没有异常跳动。做法是:先全部检测完,把所有标定板的角点三维坐标(以标定板为坐标系原点)解出来一次,看哪些图像的重投影误差明显大于均值,把误差大于两倍标准差的图像直接剔除掉,再重新标定一次。听起来好像有点“先斩后奏”,但这就是工程上常见的数据清洗思路,即先粗标定一次定位坏数据,剔除后精标定一次出最终参数。
还有一种常见坑:棋盘格检测到了,但角点顺序错乱。比如标定板边缘被遮挡,检测算法自己“脑补”了不存在的角点,视觉上看着对齐了,实际上顺序是错的。这种情况用cv2.drawChessboardCorners把角点画出来逐张检查,比任何自动化指标都直观,尤其第一次标定的时候千万别省这一步。
2.4 D435 这类集成深度相机的使用心得
如果你用的是D435这类深度相机,会省掉很多自己拼装和标定的工作,但也别天真地以为开箱就能出准。D435拿到手建议先做三件事:更新固件、确认RGB和深度图已对齐、把深度精度模式调到适合实际距离的档位。
D435的参数里有个depth_units,默认是0.001,意味着深度值最小刻度1毫米,但也有工厂标定导致的微小偏差。我在室内3米距离上测试,默认参数下测1米内物体精度相当好,但测2米以上物体误差会到5%左右,这个水平做避障够了,做精密测量不够。
D435另一个值得留意的点是IR散斑投影仪的工作距离。它在近距离(0.3到2米)效果好,超过3米散斑变稀,匹配质量下降,加上红外受环境光干扰,实测5米外基本就是摆设。所以别看到“深度相机”四个字就觉得万能,它的能力和适用距离范围写得很清楚,用之前一定仔细读数据手册。
3. 深度计算实战:从视差图到距离
3.1 立体匹配算法怎么选
立体匹配是整个测距链路里最烧计算量,也最容易出问题的一环。常见的三种算法是BM、SGBM和基于深度学习的方法。
BM(Block Matching)是最基础的块匹配算法,以左图某个像素为中心取一个固定尺寸的窗口,沿水平方向在右图搜索匹配的窗口,用绝对差和或平方差和作为相似度指标。它速度快,但窗口大小选不好就会出现纹理重复区域的误匹配,边缘也不太干净。
SGBM(Semi-Global Block Matching)是实际项目里用得最多的方案。它在BM的基础上引入多个方向的平滑约束,把匹配问题建模成一个近似全局的能量最小化问题。OpenCV里StereoSGBM_create一调就行,但需要调几个关键参数:numDisparities(视差搜索范围)、blockSize(匹配窗口大小)、P1和P2(平滑惩罚系数)。
深度学习立体匹配方法比如PSMNet、RAFT-Stereo,精度确实碾压传统算法,但需要GPU推理,很多边缘设备跑不动。项目里如果算力充裕,直接用预训练模型做离线深度估计完全可行,但实时性要求高的嵌入式场景,SGBM还是主力。
我基于自己的实践感受,给出一个粗略的选型对照:
| 算法 | 精度 | 速度 | 适用场景 |
|---|---|---|---|
| BM | 低 | 极快 | 低算力设备、简单纹理场景 |
| SGBM | 中 | 较快 | 室内机器人、嵌入式实时应用 |
| 深度学习方法 | 高 | 慢(需GPU) | 离线重建、高精度测量 |
3.2 SGBM的关键参数与标定后处理
SGBM虽然调起来简单,但参数影响巨大。先说numDisparities,它必须能被16整除,代表搜索的最大视差像素数。这个值设多大由最近的测距距离决定,用公式换算:最小距离 = f * B / numDisparities。比如 f=600px,B=60mm,想测0.5米最近距离,numDisparities至少要720/500*16 取整到 48左右。设小了近处直接没有视差,设大了远距离匹配会出现很多噪声点,因为大视差范围让误匹配概率上升。
blockSize是方形匹配窗口的大小,必须是奇数。窗口越大,对噪声越鲁棒,但会抹平深度突变的位置,物体边缘会被“糊”一圈。我一般室内环境从5开始调,输出视差图噪点多就往上加,边缘糊了就往下减,最终取一个平衡点。
P1 和 P2 是两个平滑惩罚系数。P2 惩罚深度渐变区域的大跳跃,P1 惩罚小跳变,一般建议 P2 = 4 到 8 倍的 P1,P1 本身设成 8 * blockSize^2 起步。这个参数调得好,视差图边缘干净且连续。还有一个细节:用disp12MaxDiff开启左右一致性检查,对遮挡区域的错误匹配有很好的剔除效果,代价是无效像素稍微变多。
校正之后我建议做两个增强处理:中值滤波平滑视差图,去掉孤立噪点;对深度图使用空洞填充。所谓空洞就是那些左右一致性检查被剔除的像素,直接表现为深度图上的黑点,用邻域有效值做插值可以填掉,但不能大面积填充,否则物体会被“穿越”,看起来像是前面有个洞,实际上后面墙的深度被填到前面来了。
3.3 深度转换公式与计算流程
拿到视差图后,深度计算有两种方式。如果你直接做过stereoRectify,它会输出一个重投影矩阵 Q,然后用cv2.reprojectImageTo3D(disparity, Q)一次性算出每个像素的三维坐标 (X, Y, Z),Z 就是距离。另一种是自己写公式,用标定出的 f、B 和视差 d 逐像素算:
Z = f * B / d我自己实现时通常选择第二种,因为更直观,方便在代码里加各种掩膜和滤波。需要注意的坑是OpenCV里SGBM输出的视差图是16位有符号整型,真实视差要除以16,也就是disp.astype(np.float32) / 16.0。这个细节漏掉的话,所有距离都会偏小16倍。
再说一下精度损失的问题。双目测距的误差随着距离平方级增长,这是三角测量的几何本质决定的。同一个基线60mm、焦距600px的系统,在1米处的深度分辨率大约是两个像素视差精度对应的误差,可以估算出小于1%;到3米处,同样的像素视差误差对应的深度误差就会扩大到约3%。所以双目适合的场景是有明确距离上限的,别指望20米外还能测准,除非基线长到离谱。
3.4 一个简单的完整处理流程示例
下面给一个最基础的单帧测距流程,方便你搭框架参考。这里默认你已经完成了标定,得到了左右相机内参、畸变系数、旋转矩阵 R、平移向量 T。
import cv2 import numpy as np # 假设已经通过 stereoCalibrate 得到以下参数 # camera_matrix_L, dist_coeffs_L, camera_matrix_R, dist_coeffs_R, R, T # 1. 立体校正 R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( camera_matrix_L, dist_coeffs_L, camera_matrix_R, dist_coeffs_R, (width, height), R, T, alpha=0) map1_L, map2_L = cv2.initUndistortRectifyMap( camera_matrix_L, dist_coeffs_L, R1, P1, (width, height), cv2.CV_32FC1) map1_R, map2_R = cv2.initUndistortRectifyMap( camera_matrix_R, dist_coeffs_R, R2, P2, (width, height), cv2.CV_32FC1) # 2. 读入左右图并校正 img_L = cv2.imread('left.png', cv2.IMREAD_GRAYSCALE) img_R = cv2.imread('right.png', cv2.IMREAD_GRAYSCALE) rect_L = cv2.remap(img_L, map1_L, map2_L, cv2.INTER_LINEAR) rect_R = cv2.remap(img_R, map1_R, map2_R, cv2.INTER_LINEAR) # 3. SGBM 匹配 stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=64, blockSize=7, P1=8 * 7 * 7, P2=32 * 7 * 7, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2 ) disp = stereo.compute(rect_L, rect_R).astype(np.float32) / 16.0 # 4. 转深度图 depth = np.zeros_like(disp) valid = disp > 0 depth[valid] = (f * B) / disp[valid]注意这里f和B要从标定数据里取,f 用校正后 P1 矩阵的左上角元素,B 可以直接取平移向量 T 的 x 分量的绝对值(近似基线长度),或者直接用 Q 矩阵做反投影。这段代码只是单帧示意,实际项目中别忘了对左右图做降噪、ROI裁剪和深度图后处理。
4. 常见问题与排查技巧实录
4.1 测距结果跳变严重怎么办
视差图在无纹理区域会产生大量误匹配,表现就是测距结果在几毫秒内来回跳。这个问题排查顺序一般是:先看视差图本身干不干净,再检查参数,最后处理深度图。
如果视差图整体都是雪花状噪声,先把blockSize调大,再把numDisparities调小一点,缩小搜索空间。如果只是部分区域噪点多,基本是场景纹理不足,可以考虑加一个红外散斑投射器辅助纹理,或者对被测物体打一些结构光图案。后处理阶段,用中值滤波和深度图的时间序列滤波(比如滑动窗口平均)能明显减小跳动,但会牺牲一点实时性。
跳变还有一个隐蔽原因:光照发生了变化。双目匹配假设左右图的亮度一致,如果场景里灯光是50Hz或60Hz交流电驱动的,两帧的曝光时刻不同会导致亮度差异,匹配时相似度指标受影响。解决方法是尽量开50Hz快门同步,或者加大匹配窗口尺寸来抵消局部亮度差。
4.2 室外强光下效果差
D435这类带红外投射的主动双目最怕室外强光,因为阳光中的红外成分会淹没投射的散斑,等效于变成被动双目,精度大幅下降。纯被动双目在室外倒是没有这个干扰问题,但强阳光下阴影边缘和反光区域会造成很多误匹配。
我的经验是:室外场景尽量避免正午强光,早晚光线柔和时测距效果会好得多。如果在阳光下做长时间测试,给镜头加装红外截止滤波片可以滤掉部分环境红外,但也会削弱被动双目的低照度性能,属于权衡取舍。另外,强烈建议室外场景用曝光固定的手动模式,关闭自动曝光,否则左右相机自动调整曝光会产生明显的亮度差异和时差。
4.3 双目、单目、超声波测距怎么选
很多读者问这几个方案之间到底怎么选,我给一个实用判断框架。
单目测距本质上不是“测距”而是“估计”。它通过已知物体尺寸或者地面平面约束反推距离,对特定场景(比如车道线检测、行人高度估计)调好了很准,换一个物体或换一种环境就完全失效。优点是成本极低,一个摄像头就行,但泛化能力是硬伤。如果你面对的是完全未知的环境,单目就是猜。
超声波的优点是对光照、纹理完全免疫,黑暗环境下照常工作,成本十几块钱搞定。缺点前面也说了,波束宽无法测方向,响应慢,测不平整表面或吸音材质会失灵。超声波测距报警系统这类场景,比如倒车雷达,固定安装测量单一方向的距离是它的舒适区。
双目相机的优势在于同时提供二维图像和三维距离,能够在空间里定位物体。但它的缺点是计算复杂,需要处理光照和纹理问题,而且远距离精度衰减快。如果只是做“距离小于某阈值就报警”这种简单任务,超声波便宜得多;如果要做障碍物的轮廓识别、抓取定位、路径导航,双目值的这个成本。
最后补一句大家都在问的:双目和单目+结构光的关系。D435这类主动双目本质上是在双目基础上叠了一层结构光来增强纹理,既保留了双目的被动测量能力,又解决了无纹理问题,代价是增加了功耗和室外弱点。这三类技术没有谁绝对最好,关键看场景。
4.4 标定误差对测距精度的定量影响
标定的重要性前面反复说了,这里用一个简单例子量化一下。假设基线 B=60mm,焦距 f=600px,实际场景中某点在1米外。如果标定得到的基线误差是1mm,那么按 Z = f*B/d 反推,深度误差大约是:
Z误差 / Z = B误差 / B = 1mm / 60mm ≈ 1.67%也就是说,1米处深度会偏约1.67厘米。看起来还好,但如果你标定出的焦距正负偏差了5个像素,在600像素焦距上大约0.83%的误差,叠加起来就接近2.5厘米了。而当目标距离变到3米时,同样的参数误差会被放大接近3倍,达到7到8厘米的偏差。
这说明标定精度对整个测距系统的上限起着决定性作用。所以每次标定完成后,我强烈建议做一次验证实验:在几个已知距离上放标定板,用测距结果反推误差,如果整体偏差大于预期,优先回到标定环节找问题,而不是在后处理里硬调。
我个人的习惯是标定完成后把重投影误差、标定板位姿、角点分布这些中间结果存档,下次出问题可以直接复盘,不用重新拍。工程做久了你会发现,这小小的一步能省大量排查时间。
双目测距这个方向,原理上就是三角测量,十几行代码就能跑通一个最简版本,但把精度做到稳定可靠,功夫全在细节里。我从标定角点的筛选到SGBM参数的试探,踩过的坑比写过的代码还多,但每次把问题定位到具体环节的时候,收获都特别大。如果你正在搭建自己的双目系统,我的建议是从小范围、固定距离的测距开始,先把标定和匹配这关磨透,再加场景复杂度,成长曲线会平滑很多。这个方向后续还可以往深度学习和硬件加速上拓展,但地基永远是今天说的这些基础功夫。