简介双目立体视觉是计算机视觉中实现深度感知的重要技术路径它通过模拟人眼三角测量原理利用两个普通RGB相机即可获取场景的三维距离信息。与单目测距依赖先验尺寸、深度相机受环境光限制不同双目方案在室内外均能稳定工作且基线和焦距可灵活选型因此被广泛应用于机器人避障、目标定位、AR测深等工程场景。要真正落地一套双目测距系统核心链路包括相机标定、立体校正、视差计算与深度恢复。其中OpenCV中的SGBM半全局匹配算法是关键环节其参数调优直接决定了视差图质量和最终测距精度。本文从基础原理出发结合实际工程踩坑经验系统梳理了从棋盘格采集、双目标定、remap校正到SGBM参数配置、WLS滤波后处理的完整流程并给出可直接运行的Python代码骨架帮助开发者高效构建稳定可靠的双目测距系统。 双目测距这块我从拿到一对USB工业相机开始到真正在室内走廊上量出物体的距离前前后后也折腾了好几周。网上关于OpenCV双目测距的教程一抓一大把但绝大多数停留在“把示例代码跑通”的阶段真到自己采集标定板、调SGBM参数、处理距离误差的时候才发现坑比想象中多得多。这篇文章就按我实际做的流程整理一份完整记录从相机标定、立体校正、视差计算到深度恢复附带能直接跑的Python源码骨架重点讲清楚每一步为什么这么干以及调试时最容易翻车的几个点。如果你是刚接触计算机视觉、准备做机器人避障、目标测距或者AR测深相关方向的学生和开发者这篇文章可以帮你省掉大量从零摸索的时间。如果你已经跑通过OpenCV官方例程但发现测出来的距离忽远忽近、视差图全是噪点那这篇文章里关于参数调优和误差排查的部分应该正好能解决你的问题。1. 双目测距的核心原理与整体架构1.1 为什么是双目而不是单目或深度相机做测距方案选型的时候大家都会先纠结一下既然有单目、有双目还有RGB-D深度相机为什么偏偏要折腾双目标定和视差计算单目测距最出名的是基于物体已知尺寸的比例法比如车牌检测、人脸检测那套思路。它本质上是利用物体的先验宽度或高度来反推距离一旦目标种类不固定或者目标被部分遮挡这个先验就不成立了。所以说单目在通用场景下只能做估算精度和鲁棒性都比较受限。RGB-D深度相机像Kinect、RealSense在室内短距离0.5米到3米左右确实又准又省事但它对环境光敏感强光下红外散斑容易被淹没室外基本废掉。而且深度相机本身的视场角和基线都是固定的想针对特定距离范围做优化硬件上就已经焊死了。双目立体视觉走的是三角测量路线原理上和人眼感知深度一模一样只需要两个普通RGB相机不依赖主动光源室内室外都能用。基线距离、镜头焦距、分辨率都可以根据实际需求自由选型这是它最大的工程灵活性。缺点自然也有标定步骤繁琐、视差计算耗时、弱纹理和重复纹理区域容易产生错误匹配。但这些问题在纯软件层面都能通过调参和算法优化缓解所以到今天它仍然是低成本通用测距的首选方案之一。1.2 三角测量与深度公式推导双目测距的几何基础可以用一个特别简单的模型描述。假设两个相机水平放置光轴平行焦距相同这就是理想的双目模型也叫标准式配置。当空间中的点P同时被左右相机看到时它在左图成像点坐标为(xL, yL)在右图成像点坐标为(xR, yR)。由于两个相机在水平方向上有位移所以同一个三维点在左右视图里的水平坐标会有一个差值这个差值就是视差d xL - xR注意在OpenCV默认的坐标系里右图里的对应点通常偏左所以xL比xR大视差值为正。把几何关系画成相似三角形可以得到Z (f * B) / dZ就是点到相机基线的垂直距离f是焦距像素单位B是左右相机光心之间的距离也叫基线d是视差像素单位。这个公式是整个双目测距系统的灵魂后面所有标定、校正、视差计算的工程操作本质上都是在精确获取f、B和d这三组值。f和B来自标定d来自视差计算三层各自独立又层层依赖所以前面哪一层做不好最后量出来的距离都会歪。很多人会问焦距f的单位为什么是像素而不是毫米。其实标定得到的f矩阵里fx f_mm / 像素尺寸单位天然就是像素。二维成像之后几何计算只关心像素坐标不再关心物理尺寸所以直接用这个像素焦距算就行。后续标定得到的平移向量单位是毫米这也就是真正能出来“米”这个物理单位的来源之一。1.3 从原始图像到距离的完整链路整个系统如果画成流水线大概是下面这个顺序左右相机拍摄标定板图像提取棋盘格角点分别做单目标定再做双目标定得到两相机相对位姿根据标定参数做立体校正让左右图像行对齐在校正后的图像上计算视差由视差和标定参数恢复深度距离严格来说单目标定可以理解为先解决“一个相机自己的内参和畸变”双目标定是解决“两个相机之间的旋转和平移关系”。立体校正则是把这两个相机的坐标系强行变换到同一个理想平行模型下让左右视图同一行对齐这样后续视差搜索就从二维问题降维成一维问题大幅降低计算量。我在实操里最深的感受是步骤1和2直接决定了整个系统的上限。标定照片拍得不认真后面再怎么调SGBM参数都是白搭。所以下一节我把标定和校正这部分展开讲给出一套我自己用下来比较稳妥的操作流程。2. 相机标定到立体校正的踩坑实录2.1 棋盘格采集的讲究相机标定本质上是一个数值优化过程优化的输入就是多组棋盘格角点在左右图像中的像素坐标和对应的三维物理坐标。理论上棋盘格贴在一个绝对平面上就行但在实际操作中很多细节决定了优化能不能收敛到好结果。打印标定板的时候尽量用激光打印机打印在平整的硬纸板或者铝板上避免热敏纸因为受热卷曲。棋盘格参数中角点数指的是内角点数比如ChArUco板上印的是10x7个格子内角点就是(10-1)x(7-1)9x6这个数字在代码里一定不能填错否则findChessboardCorners会找不到角点。采集图像时的姿势比数量更重要。我的做法是保持标定板在相机景深范围内从正对相机到倾斜约30到45度都拍一些每次改变标定板的角度和位置而不是连续平移拍摄视频然后抽帧左右相机分别拍摄同一时刻的标定板图像确保所有照片覆盖画面的四个角和中心区域每张图里棋盘格最好占画面面积的1/4以上别拍得太小关于图像数量左右各拍15到20对是比较舒适的区间。太多会让标定过程变慢而且如果图像之间差异不够大反而容易过拟合到局部最优解太少则对畸变参数的约束不足。拍20对左右先粗标一次看看平均重投影误差误差大于0.5像素就补拍或者剔除质量差的图。2.2 stereoCalibrate 双目标定与参数意义OpenCV里双目标定的核心函数是cv2.stereoCalibrate它的输入是左右相机各自标定好的内参、畸变系数以及左右图像中所有角点的像素坐标。ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( objectPoints, # 世界坐标系下的棋盘格角点坐标 imagePoints1, # 左图角点像素坐标 imagePoints2, # 右图角点像素坐标 K1, D1, # 左相机内参矩阵和畸变系数 K2, D2, # 右相机内参矩阵和畸变系数 image_size, # 图像尺寸 (width, height) flagscv2.CALIB_FIX_INTRINSIC )这里我用了CALIB_FIX_INTRINSIC标志意思是只优化两个相机之间的旋转矩阵R和平移向量T不重新优化内参。好处是进一步利用之前单目标定的结果避免双目标定时内参与外参相互拉扯导致的数值漂移。R是一个3x3的旋转矩阵表示右相机坐标系相对于左相机坐标系的旋转。T是平移向量单位是毫米T的模长就是基线的距离。这里需要说明一点OpenCV自动标定时T的尺度是依赖标定板实际格子的物理尺寸来恢复的所以如果你在objectPoints里把每个格子写成10平方毫米那T算出来就会和真实毫米有偏差。所以objectPoints一定要按真实棋盘格边长来写单位统一用毫米。标定完成后建议打印一遍重投影误差cv2.calibrateCamera返回的ret就是平均重投影误差stereoCalibrate里也类似。它表示把角点重新投影到图像上与实际提取角点之间的平均像素距离小于0.5像素说明标定质量不错如果大于1.0像素建议回头检查拍摄数据。2.3 立体校正与remap实际配参拿到R和T之后就要做立体校正。OpenCV的Bouguet校正方法通过cv2.stereoRectify函数实现它会把两个相机坐标系重新旋转使得左右图像的对极线变成水平行线。R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K1, D1, K2, D2, image_size, R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0 )P1和P2分别是校正后的投影矩阵Q是一个4x4的重投影矩阵后面从视差图算三维点会用到。将K1、K2、D1、D2作为原始参数输入alpha0表示对校正后的图像做最大程度的裁剪去掉黑色无效区域这样视差图不会有大片黑边如果你想保留完整画面可以设成1但代价是图像边缘会有变形区域。校正之后需要生成映射表map1x, map1y cv2.initUndistortRectifyMap(K1, D1, R1, P1, image_size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(K2, D2, R2, P2, image_size, cv2.CV_32FC1)然后用cv2.remap对左右图做重映射。这一步做完左图和右图的同一行应该对应三维空间的同一行这就是后面视差计算的先决条件。我踩过的一个坑一开始我把initUndistortRectifyMap的map类型写成了CV_16SC2结果OpenCV报错类型不匹配。后来统一用CV_32FC1就稳了。后面所有送入SGBM的图必须是单通道8位灰度图记得在remap之后做一次cv2.cvtColor。标定和校正的效果如何检查我习惯用OpenCV画水平线叠加在左右图上对比把右图按行切几条水平线叠到左图上如果同一物体边缘基本落在同一条水平线上说明校正到位了。这个人工检查步骤虽然土但是肉眼判断极快比看数值直观得多。3. 视差计算与深度恢复SGBM参数才是灵魂3.1 SGBM关键参数详解立体校正做完了下一步就是从两幅图像里找到对应点计算每个像素的视差。OpenCV里最常用的是cv2.StereoSGBM_create它实现了半全局块匹配算法对比传统的BMBlock Matching块匹配SGBM通过能量函数聚合多方向路径的匹配代价能显著减少弱纹理区域的误匹配和条纹状噪声。构造SGBM的时候大家最容易抄到一段参数然后随便填但实际上每个参数都有明确的物理意义。我贴一段我自己调过、效果比较稳定的参数配置然后逐个解释left_matcher cv2.StereoSGBM_create( minDisparity0, numDisparities16*3, blockSize3, P18 * 1 * 3 * 3, P232 * 1 * 3 * 3, disp12MaxDiff1, preFilterCap16, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.STEREO_SGBM_MODE_SGBM_3WAY )minDisparity最小视差值设为0表示最近处对应的视差为0通常取0但如果你的目标区域视差范围偏大比如近景目标距离很近需要适当调成负数。numDisparities最大视差和最小视差的差值必须是16的倍数。这个参数直接决定了测距范围。视差d越大距离Z越近所以numDisparities越大能测的近距离越近但计算量也线性增加。我的经验是在保证覆盖最近目标的前提下尽量取小值算法效率和稳定性都会更好。blockSize匹配窗口大小必须是奇数取值范围一般在3到11之间。窗口越小细节越丰富但是对噪声更敏感窗口越大纹理弱的区域也更容易匹配上但容易平滑掉物体边缘导致前景和背景的边界糊在一起。实际调参时我通常从3开始试遇到弱纹理区域匹配太差再往上加。P1和P2这是SGBM能量函数里的平滑惩罚项。P1是灰度变化相邻像素的惩罚P2是灰度变化更大的惩罚。P2的值一般要远大于P1典型经验公式是P1 8 * 通道数 * blockSize^2P2 32 * 通道数 * blockSize^2。如果你要算的是单通道灰度图通道数直接取1。P2设置太大会导致整个视差图过于平滑颜色突变处被压平P2太小会出现明显的条纹状横纹噪声。disp12MaxDiff左右一致性检查允许的最大差异一般设为1。这个参数用来剔除因遮挡而产生的误匹配如果差异大于1则将该点视差置为无效。preFilterCap预滤波截断值默认值即可一般不用动。uniquenessRatio唯一性比率表示最小匹配代价与次小匹配代价的差值比例一般建议5到15之间。越大越容易变稀疏但误匹配更少。我用10作为起步值。speckleWindowSize和speckleRange用来滤除小的孤立斑点。斑点窗口大小和范围一并配合那些面积小于speckleWindowSize且视差波动小于speckleRange的孤立区域会被直接置为无效视差。3.2 WLS滤波与中值去噪SGBM直接输出的视差图往往带有毛刺和空洞尤其在物体边缘和纹理重复区域表现明显。这时候直接做深度恢复距离数据会非常跳跃体验很差。我实际工程里一定会加两步后处理WLS滤波和中值模糊。WLS全称Weighted Least Squares是基于边缘保持的图像滤波方法。它能借助原始左图像的颜色边缘信息来引导视差图平滑属于典型的引导滤波思路。OpenCV中位于cv2.ximgproc扩展模块里。wls_filter cv2.ximgproc.createDisparityWLSFilter(left_matcher) right_matcher cv2.ximgproc.createRightMatcher(left_matcher) disparity_left left_matcher.compute(imgL, imgR) # 视差图是CV_16S格式 disparity_right right_matcher.compute(imgR, imgL) # 右视差图 filtered_disparity wls_filter.filter( disparity_left, imgL, disparity_map_rightdisparity_right, ROIroi1 )这里有个细节WLS滤波需要输入左右两张视差图所以你需要构造一个right_matcher它和left_matcher参数完全一致但是compute时输入左图和右图的顺序是反的。不传右图也可以运行但滤波效果会明显变差。这个函数是OpenCV扩展模块里的记得pip install opencv-contrib-python才能导入cv2.ximgproc。WLS之后我还会再加一次中值滤波filtered_disparity cv2.medianBlur(filtered_disparity, 3)这里的滤波核不要太大3或者5就足够。太大会把物体的棱角轮廓直接磨平影响测距点的选取。小提示SGBM返回的视差图是CV_16S格式也就是带符号16位整型真实视差值等于数值除以16。所以后面做任何数学运算之前先统一转成float32disp filtered_disparity.astype(np.float32) / 16.0。这是最容易踩的坑很多人把数值直接当成真实视差用最后测出来的距离差了16倍。3.3 深度转距离的工程实现有了真实视差之后距离计算有两种常用方式一种是通过重投影矩阵Q直接把视差图映射成三维点云另一种是直接用公式Z f * B / d对每个像素计算。我自己在做非密集测距时更倾向于直接用公式因为可以只对目标区域计算省掉完整三维点云的内存开销。f和B从标定结果里读取baseline np.linalg.norm(T) # 单位毫米 fx K1[0, 0] # 像素焦距 depth_map (fx * baseline) / (disp 1e-6) # 单位毫米这里加了一个1e-6的极小值防止除以零。注意视差为0或者无效点时深度结果无意义后面需要根据mask过滤。如果确实需要完整的点云用cv2.reprojectImageTo3Dpoints_3d cv2.reprojectImageTo3D(disp, Q)得到的是每个像素对应的(x, y, z)三维坐标其中z就是距离。这个方法适合后面接点云处理或者目标检测框平均距离计算。回到公式本身你可能会发现一个实际的现象近距离物体的小视差变化会导致距离发生剧烈变化。比如视差从40变成41距离差异可能达到几十厘米而视差从200变成201距离差异只有几厘米。这是双曲线特性决定的也是为什么双目系统越远越测不准的根本原因。后面我会专门展开讲这种误差分布特性。4. 工程化落地性能、精度与应用场景4.1 CPU与CUDA的性能取舍视差计算是双目系统里最消耗算力的环节。SGBM算法的计算复杂度与图像分辨率、numDisparities、blockSize直接相关。举几个实测数据供参考在分辨率为640x480灰度图、numDisparities64、blockSize3的条件下纯CPU大概i5-8400级别跑SGBM约80到120ms一帧也就是8到12FPS如果开启OpenCV CUDA版本的cv2.cuda_StereoSGBM在入门级GTX 1050Ti上能到5到8ms一帧直接快一个量级以上所以如果你要做实时机器人避障建议在部署机器上装带CUDA的OpenCV或者直接用Jetson平台。如果只是离线标定、复现实验CPU版本完全够用别为了追求速度把整套流程复杂度先拉满。分辨率上我自己的建议是测距精度要求高且不是实时场景用1280x720实时避障、目标跟踪降到640x480或者更低。因为视差计算的时间复杂度近似与像素量成正比分辨率翻倍耗时接近翻倍但精度提升未必对应用有决定性帮助。4.2 基线长度、焦距与可测距离范围的关系这部分是选型时必须想清楚的也是很多人容易忽略的。双目测距可测的距离范围由编码视差的能力和图像分辨率共同决定。公式Z f * B / d里f和B是硬件常量d是视差。视差可以看成是像素为单位的大整数有效范围最多只有一个图像宽度。在基线固定的情况下目标越远视差越小。当视差小到1个像素以内时距离就完全测不出来。举个例子假设fx约为500像素基线100毫米那么1个像素的视差对应距离是500 * 100 50000毫米 50米。这就是理论上限。但SGBM对视差的分辨率往往不是整像素精度实际精度上限远低于这个理论值所以大家普遍把双目可靠测距范围控制在基线的20倍到30倍以内。100毫米基线可靠范围大概是2到3米。基线长度的选择就是在测量近距离和远距离之间做权衡。基线越长同一个距离对应的视差越大测量精度越高但近距离会被限制因为近距离大视差超出了numDisparities的覆盖范围且两个相机视角差异太大导致共同视野变小。一般桌面级应用基线选60到120毫米室外导航选200到300毫米需要测量更远的场景就要选更大的基线甚至用两个变焦相机加同步触发。4.3 可以直接改的完整测距骨架代码为了方便复用我把前面几节的内容整合成一个精简但完整的骨架覆盖“标定参数加载、图像校正、视差计算、距离测量”全流程。它没有包含标定板的采集代码那个用OpenCV自带的示例脚本即可但从标定结果到距离输出的路径是完整的。import cv2 import numpy as np # ---------- 1. 加载标定结果 ---------- # 这些参数由标定阶段保存保持相同顺序 k1 np.array(...) # 3x3 左内参 d1 np.array(...) # 1x5 左畸变 k2 np.array(...) # 3x3 右内参 d2 np.array(...) # 1x5 右畸变 r np.array(...) # 3x3 旋转矩阵 t np.array(...) # 3x1 平移向量 image_size (640, 480) # ---------- 2. 立体校正 ---------- r1, r2, p1, p2, q, roi1, roi2 cv2.stereoRectify( k1, d1, k2, d2, image_size, r, t, flags0, alpha0 ) map1x, map1y cv2.initUndistortRectifyMap(k1, d1, r1, p1, image_size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(k2, d2, r2, p2, image_size, cv2.CV_32FC1) # ---------- 3. SGBM 参数 ---------- left_matcher cv2.StereoSGBM_create( minDisparity0, numDisparities16 * 4, blockSize3, P18 * 3 * 3, P232 * 3 * 3, disp12MaxDiff1, preFilterCap16, uniquenessRatio10, speckleWindowSize100, speckleRange32 ) wls_filter cv2.ximgproc.createDisparityWLSFilter(left_matcher) right_matcher cv2.ximgproc.createRightMatcher(left_matcher) # ---------- 4. 主循环 ---------- cap_left cv2.VideoCapture(0) cap_right cv2.VideoCapture(1) while True: ret1, frame_left cap_left.read() ret2, frame_right cap_right.read() if not (ret1 and ret2): break # 如相机本身是彩色但只需要灰度就转灰度 imgL cv2.cvtColor(frame_left, cv2.COLOR_BGR2GRAY) imgR cv2.cvtColor(frame_right, cv2.COLOR_BGR2GRAY) # 校正 rectL cv2.remap(imgL, map1x, map1y, cv2.INTER_LINEAR) rectR cv2.remap(imgR, map2x, map2y, cv2.INTER_LINEAR) # 视差计算 dispL left_matcher.compute(rectL, rectR) dispR right_matcher.compute(rectR, rectL) dispWls wls_filter.filter(dispL, rectL, disparity_map_rightdispR, ROIroi1) dispWls cv2.medianBlur(dispWls, 3) # 转成真实视差 disp dispWls.astype(np.float32) / 16.0 # 通过公式计算深度 fx k1[0, 0] baseline np.linalg.norm(t) depth_map (fx * baseline) / (disp 1e-6) # 显示并可视化 disp_vis cv2.normalize(disp, None, 0, 255, cv2.NORM_MINMAX) disp_vis np.uint8(disp_vis) cv2.imshow(disparity, disp_vis) cv2.imshow(depth, depth_map / np.max(depth_map)) if cv2.waitKey(1) 0xFF ord(q): break cap_left.release() cap_right.release() cv2.destroyAllWindows()这个骨架里k1、d1等标定参数需要你根据自己的相机替换。坐标点的单位问题前面提过T的模长就是毫米基线所有深度输出也都是毫米。你可以把上面的depth_map做一次像素采样得到某个目标点的距离值。需要特别提醒的是两个USB摄像头同时读取时不同摄像头之间的帧同步是个大问题。便宜的双目摄像头靠USB带宽不同左右帧可能相差几十毫秒目标一旦运动起来视差计算就会出现明显错误。这种情况下有两个妥协思路一是尽量放慢目标运动速度二是用硬件同步的多目相机模块或者工业相机加外部触发。做静态测距的话基本不用担心。5. 常见问题与排查技巧实录5.1 标定阶段常见问题问题1findChessboardCorners找不到角点优先检查图像格式和棋盘格角点参数是否一致。角点打印尺寸和页面排版很容易搞错比如打印出来的棋盘格实际是9x6格子内角点只有8x5。另外避免在太亮或者太昏暗的光线下拍摄棋盘格黑白色块边缘要锐利目标太小也会导致角点检测失败。问题2重投影误差很大误差大基本都是图片太少或者拍摄姿势不够多样造成的。我的经验是至少保证10对以上图像并且覆盖相机视野的各个区域正对和倾斜都要有。还有一种可能是标定板的物理尺寸填错了objectPoints里的坐标单位不一致会导致T的规模不对但是内参会假装收敛。每次标定完我会多打印一组角点重投影的图逐张看一眼误差大的图像是哪些再把不合格的剔除。问题3双目标定后基线T的模长和实际量出来的差距很远这个一半是标定板尺度有问题另一半是相机位姿抖动造成的。把标定板每个格子实际边长用尺子认真量一遍不要只按照打印尺寸算。另外拍摄标定板时尽量固定相机棋盘格可以在手里动但相机本身不要来回晃。5.2 视差图阶段常见问题问题1整个视差图全黑或者几乎全黑大概率是视差图的取值范围和显示方式有误。SGBM输出的是CV_16S真实视差是数值除以16。如果直接用imshow显示16位数据里的有效值范围很小看起来就会非常暗甚至全黑。正常化再用imshow就能看到内容和层次。问题2视差图上出现大量条纹状噪声多出现在大面积弱纹理区域比如纯色墙面、天空。有几个调节方向调大blockSize、增大uniquenessRatio、适度调大P1和P2让平滑更积极。但这些参数调完往往会牺牲边缘精度所以是一个反复权衡的过程。还可以考虑用SGBM的MODE_HH模式这个模式会做更完整的路径聚合效果好一些但速度明显变慢离线处理时可用。问题3画面左右边缘有大面积黑色无效区域这通常是立体校正时alpha0裁剪掉的无效区域。OpenCV确实会在这部分生成一些空白。如果你介意把stereoRectify的alpha调成1保留所有原始像素但代价是视差图左右边缘会有明显畸变应用时需要裁剪到有效的ROI区域。实际使用时我认为保持alpha0然后对后续目标框区域留出几像素到十几像素的边距比较合理。5.3 测距精度相关排查问题1近距离误差还算能看远距离误差迅速变大这是双目测距的数学特性决定的不是调参能完全解决的。视差固定误差一个像素距离越远相对误差越大。如果需要补远距离精度只能换更长基线或者更高分辨率相机。在工程上我会先画一个“距离-理论误差”曲线然后在应用里按测距范围给结果加置信度标签超出可靠范围直接提示不可用。问题2实测距离和真实距离之间有个接近线性的偏移这种情况常见于标定基线和实际物理基线不一致比如相机外壳厚度导致光心间距和T模长差了一截。解法是先测几组不同距离的真实值拟合一个线性校准曲线real_distance a * measured_distance b。虽然看起来有点土但在工程落地上非常有效很多公司量产差异就是靠这个公式补回来的。问题3不同时间、不同温度下距离漂移工业塑料外壳会随温度发生轻微形变加上相机松动基线和光轴都会变。定期重新标定是必须的。如果项目需要长时间运行建议设计金属固定支架并且每隔一段时间跑一次自动标定流程。我碰到过项目跑了一个月后距离整体偏移5%的情况排查下来就是相机支架在振动环境下松了。实际开发中想完全避免这些误差是不可能的双目系统本来就是多环节误差叠加的模型。标定误差、图像噪声、匹配误差、算法离散化误差最终都会在距离上体现出来。比较好的工作是分层控制先通过对齐的标定保证坐标系统的正确性再用SGBM参数把视差图调到可接受的匹配质量最后用合适的后处理和校准曲线修正系统误差。这套流程走下来即使是入门级双目模组在3米以内的测距精度做到3%到5%的相对误差是完全可行的。最后说一个我自己的习惯每次调试完一组参数我会把左右视图、校正后视图、原始视差图、滤波后视差图和深度图共五张图拼在一起保存并在文件名里标注当时的场景和参数组合。这样做的好处是后续你调整了某个环节翻旧图就能快速判断到底是标定变了还是参数变了导致的效果退化。双目测距调试的时候能同时看到中间结果比看最终数字重要得多因为在很多情况下距离数字看起来对但视差图里的匹配质量已经悄悄崩了只是你还没发现而已。本文还有配套的精品资源点击获取