Python双目立体视觉测距:标定与SGBM匹配实战解析
发布时间:2026/10/2 1:46:55来源:尧图网络
简介这套源码基于Python实现双目立体视觉测距流程面向计算机视觉学习者、机器人或智能硬件方向开发者可用于理解双摄像头从标定、立体匹配到视差计算与距离估算的完整链路。包内共6个文件以stereo_vision.py主程序脚本、README.md说明文档、棋盘格标定图片及LICENSE等为主压缩包整体仅约79KB结构紧凑。视差计算依赖场景纹理特征且易受光照变化影响源码为相关实验提供了可调整的基础便于读者对照环境适应性要点进行参数配置与效果测试。附带说明文档聚焦从标定到距离解算的实现路径适合作为双目视觉入门、课程项目或二次开发的基础参考。目前已有40人学习下载可用于快速搭建测距演示并开展后续探索。1. Python 双目立体视觉测距系统为什么标定比算法更影响结果给机器人加上一双“眼睛”最省钱又够用的方案就是两台 USB 相机搭一个 Python 双目立体视觉测距系统。这套源码做的是一件很具体的事左右相机各拍一帧经过标定、极线校正和立体匹配算出视差再用三角测距公式把视差换算成距离。不用激光雷达、也不用深度相机只靠两幅图像的视差把场景深度估出来。适合正在做智能车避障、机器人导航、料箱定位的开发者也适合刚入门 Python 视觉、想找个完整工程练手的人。但先说一个反直觉的结论这套系统的精度上限从来不在匹配算法而在标定和图像对齐。同一个目标标定板拍得规范不规范能让 1 米处的测距误差差出三倍。后面就按这个思路拆。2. 双目测距的核心原理视差、极线几何与半全局匹配怎么选先把原理讲到足够指导写代码的程度。人眼判断远近靠的是双眼视差同一三维点在左右视网膜上的位置不同越近、位置差越大。机器双目复现的就是这个过程。关键参数就三个基线 B两相机光心的水平距离、焦距 f单位是像素不是毫米、视差 d同一三维点在左图与右图的横坐标差。深度 Z f × B / d这是整个系统的地基公式。2.1 视差与深度的非线性远处误差按平方放大Z 和 d 是反比关系这个看似简单的公式藏着整套系统最核心的精度约束。视差不是连续量SGBM 输出的视差图经过 16 倍缩放在整数像素上取整也就是说视差本身带 ±0.5 像素的量化误差。对深度公式求偏导得到 ΔZ ≈ Z² / (f × B) × Δd误差和距离的平方成正比。举个例子f 约 1200 像素、基线 55mm 的常见参数下1 米处目标视差约 66 像素1 像素视差误差只带来约 1.5cm 深度抖动到了 5 米处目标视差只剩 13 像素1 像素误差直接放大到接近 38cm。这就是为什么所有双目测距项目都强调“近距离好用远距离别指望”也是为什么标定和匹配质量对远处目标的可靠性影响远大于对近处的。理解了这条曲线就理解了本项目所有参数调整的出发点。2.2 SGBM 半全局匹配为什么不用 BM 也不上深度学习OpenCV 里立体匹配有三个坑位BM、SGBM、以及专门给双目深度学习的算子。BM 是纯局部块匹配只在窗口内求匹配代价最小值遇到白墙、重复纹理、低纹理区域基本就碎成噪点。SGBMSemi-Global Block Matching在局部代价的基础上加了一条平滑约束通过 P1、P2 两个惩罚项限制相邻像素的视差跳变让匹配结果在弱纹理区域也能连续。这条约束让 SGBM 在低纹理场景下的表现明显优于 BM而计算量只是比 BM 高一个常数级CPU 跑实时很现实。深度学习方案效果最好但需要训练数据、GPU、而且跨场景泛化是玄学——在 A 环境训的模型换个光照或者换对相机效果可能直接翻车。这套源码定位是通用工程方案SGBM 是唯一能把“开箱即用”和“效果可解释”同时满足的选择。极线校正之后匹配搜索从二维退化到一维水平线SGBM 沿着同一行找对应点这也是后面所有代码能高效跑起来的前提。2.3 标定决定上限内参、畸变、外参各管什么双目测距的全部输入都来自标定内参包括焦距 fx、fy 和主点 cx、cy畸变系数包括径向畸变 k1、k2、k3 和切向畸变 p1、p2外参是左右相机的旋转矩阵 R 和平移向量 T。内参不准视差转深度时 f 用错所有距离整体偏一个比例畸变系数不准画面边缘的目标匹配点找偏边缘区域测距系统性偏移R 和 T 不准极线校正后两条极线不水平SGBM 的水平搜索直接失效。这套源码的标定流程走的是张正友标定法拍不同角度和位置的棋盘格检测角点用最小二乘求内外参。操作上花二十分钟拍图、五分钟计算换来的是整个系统从“能出图”到“准”的关键一步。很多人跳过了这一步直接跑匹配出来的距离值自己都不敢信问题几乎都出在这。3. 从标定到测距的完整流程关键代码与参数解析这一章把整个链路分成三段单目标定、双目标定与极线校正、SGBM 匹配与深度输出。代码以 OpenCV 的 Python 接口为准这也是这套源码的主要实现方式。如果你手里的相机参数已经标定过可以跳过 3.1但建议仍然对着 3.2 的步骤验证一遍因为双目标定的外参直接决定后面能不能出正确的深度。3.1 单目标定棋盘格角点检测与内参求解单目标定的任务是获得左右相机各自的内参和畸变系数。需要准备一块棋盘格标定板格子数量和单格边长必须实测单格边长建议用卡尺量到毫米级。下面这段代码负责采集多组图像、检测角点并完成单目标定import cv2 import numpy as np # 棋盘格内角点数6x9 表示每行 9 个内角、每列 6 个内角 pattern_rows, pattern_cols 6, 9 square_size 24.0 # 单格边长单位 mm务必用卡尺实测 objp np.zeros((pattern_rows * pattern_cols, 3), np.float32) objp[:, :2] np.mgrid[0:pattern_cols, 0:pattern_rows].T.reshape(-1, 2) objp * square_size obj_points [] img_points_l, img_points_r [], [] for img_l, img_r in zip(left_images, right_images): gray_l cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) ret_l, corners_l cv2.findChessboardCorners( gray_l, (pattern_cols, pattern_rows), cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE) ret_r, corners_r cv2.findChessboardCorners( gray_r, (pattern_cols, pattern_rows), cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE) if ret_l and ret_r: corners_l cv2.cornerSubPix( gray_l, corners_l, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) corners_r cv2.cornerSubPix( gray_r, corners_r, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) obj_points.append(objp) img_points_l.append(corners_l) img_points_r.append(corners_r) ret_l, mtx_l, dist_l, _, _ cv2.calibrateCamera( obj_points, img_points_l, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera( obj_points, img_points_r, gray_r.shape[::-1], None, None) print(left reproj err:, ret_l, right reproj err:, ret_r)这里有三点要注意。calibrateCamera 返回的 ret_l 是重投影误差单位是像素这个值建议控制在 0.25 以下超过 0.3 基本说明角点检测有错误或拍图质量太差。角点检测时的 CALIB_CB_ADAPTIVE_THRESH 会针对光照不均做自适应阈值配合 subpix 亚像素角点精化能把角点坐标做到亚像素级。objp 必须是 float32并且单位要和实际棋盘格边长一致这决定了外参平移向量的单位后面算基线要用到。3.2 双目标定与极线校正让匹配从二维降到一维单目标定完成后用同一组角点做双目标定得到右相机相对左相机的旋转 R 和平移 T。随后用 stereoRectify 做极线校正让两幅图同一行严格对齐。校正后的图像是后续一切的基础这一步如果没做对SGBM 匹配的结果就是乱码criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5) flags (cv2.CALIB_USE_INTRINSIC_GUESS cv2.CALIB_FIX_ASPECT_RATIO cv2.CALIB_SAME_FOCAL_LENGTH) ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteriacriteria, flagsflags) R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, alpha0) map1_l, map2_l cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1) left_rect cv2.remap(frame_l, map1_l, map2_l, cv2.INTER_LINEAR) right_rect cv2.remap(frame_r, map1_r, map2_r, cv2.INTER_LINEAR)stereoCalibrate 的 flags 里用了 SAME_FOCAL_LENGTH意思是左右相机焦距近似相等这样 P1 和 P2 的焦距值一致后续转深度时省掉很多对齐问题。stereoRectify 的 alpha 参数很关键alpha0 表示校正后只保留有效区域画面会有裁剪alpha1 表示保留全部像素但会引入黑边。我一般用 alpha0因为裁剪掉的主要是无效边缘还能减少匹配干扰。校正完成后可以用鼠标在左图上取一个点看右图同一行是否出现对应点这是最直观的验证方式。3.3 SGBM 匹配与距离输出参数表和深度公式立体匹配用的是 StereoSGBM这一步把校正后的左右图变成视差图。关键参数如下表这些参数决定了匹配质量、速度以及最终测距可靠性参数推荐值作用minDisparity0视差搜索下界近距离目标视差大需要调大 numDisparities 覆盖numDisparities64 ~ 128视差搜索范围必须是 16 的倍数要大于场景最大视差blockSize5 ~ 15奇数匹配窗口大小越大视差图越平滑但边缘越模糊P1 / P28×3×blockSize² / 32×3×blockSize²平滑惩罚项P2 通常取 P1 的 4 倍左右uniquenessRatio5 ~ 15唯一性约束越大表示匹配置信度要求越高disp12MaxDiff10 ~ 25左右一致性检查阈值0 表示关闭检查speckleWindowSize50 ~ 200散斑滤波窗口大小过滤孤立噪点区域speckleRange16 ~ 64散斑连通域内视差允许波动范围preFilterCap63匹配代价前的梯度截断值stereo cv2.StereoSGBM_create( minDisparity0, numDisparities64, blockSize9, P18 * 3 * 9 ** 2, P232 * 3 * 9 ** 2, disp12MaxDiff10, uniquenessRatio10, speckleWindowSize100, speckleRange32, preFilterCap63, modecv2.STEREO_SGBM_MODE_SGBM) disparity stereo.compute(left_rect, right_rect).astype(np.float32) / 16.0 f_px P1[0, 0] # 校正后的焦距单位像素 baseline_mm np.linalg.norm(T) # 基线长度单位 mm depth_mm f_px * baseline_mm / disparity depth_mm[disparity 0] np.nan # 无效视差置为 NaNSGBM 输出的视差值是 16 倍缩放的整数所以必须除以 16.0 得到真实视差。深度公式里用到的 f 是校正后的焦距直接取 stereoRectify 返回的 P1 矩阵左上角值比用单目标定内参更稳妥。baseline 用 np.linalg.norm(T) 取平移向量模长单位与标定板尺寸一致这里必须确认标定板边长单位是 mm否则算出来的深度单位不对。注意disp12MaxDiff 设为 0 表示关闭左右一致性检查。如果不做检查匹配出的飞点会直接进入深度图表现为少数像素距离异常跳变。建议始终设置 10~25 之间的值。4. 常见问题与避坑标定、匹配与光照的四个踩坑记录这套源码本身能跑通但换到真实环境里坑基本集中在标定质量、光照和参数匹配上。下面四条是复现过程中掉进去过、也帮同事排查过最多的记录每条都按现象和解决思路写清楚。4.1 标定板拍太少重投影误差很低但测距就是偏有次我把标定板正对相机拍了 10 组图重投影误差 0.18看着非常漂亮。装到支架上测 1.5 米处的物体输出却偏了 10 厘米百思不得其解。后来发现标定的重投影误差只衡量“拟合残差”它低只能说明这组图内部自洽不代表畸变模型外推正确。原因是我拍的标定板全部集中在画面中央视角变化很小画面边缘的畸变参数完全是靠模型外推出来的。解决方法是重新采集至少 15~20 对图像标定板要覆盖图像四个角并让棋盘格在镜头前有倾斜、旋转、远近不同姿态。从那以后每次标定我都会把左右图的角点检测结果逐帧画出来确认棋盘格出现在画面不同区域再看重投影误差是否低于 0.25。误差超过 0.3 的图像直接删掉不用重新补拍。4.2 反光物体测距翻车SGBM 找不到匹配点金属水杯、玻璃瓶、亚克力板这类反光物体是双目系统的天敌。现象很直观视差图上对应区域出现大片黑色空洞或者深度值跳得离谱。原因是反光表面在不同视角下亮度差异很大左图能看到的高光反射右图可能完全看不到匹配代价函数找不到稳定极值。处理思路是物理干预优先。调整光源方向让高光不要正对相机条件允许时加偏振片能明显压制镜面反射。代码层面可以对高光区域做一个掩膜用亮度阈值把过曝像素找出来在匹配时跳过这些区域宁可没有深度也不要给错误深度。另外 uniquenessRatio 如果设置太低本来置信度不足的匹配也会被放行输出一堆假距离这个参数建议在 5~15 之间不要为了“填满”视差图而调低。4.3 SGBM 参数配不好视差图要么全是噪点要么边缘发糊最常见的翻车是两个参数没配对numDisparities 太小导致近处目标被截断视差图近距离区域一片黑blockSize 太大导致目标边缘的视差被窗口“抹平”轮廓模糊、边界测距不准。解决思路是先确认场景中最远的视差范围。把相机放到最近工作距离拍一帧校正图手动估计左图中同一个点在右图的行坐标差得到最大视差再选一个比它大一点的 numDisparities并且保持 16 的倍数。blockSize 从 5 开始往上试到 15 为止每次只改一个参数看效果。P1 和 P2 如果设得太小低纹理区域会碎掉设得太大深度不连续处会被平滑掉锐利边缘全丢。我现在的习惯是固定 P1 为 8×3×blockSize²P2 为 32×3×blockSize²需要微调时优先动 P2。4.4 基线长度对不上远处测距整体偏大或偏小双目标定输出的 T 向量可以直接作为基线长度但有一个前提标定板单格边长的单位必须正确同时 T 向量模长要和实测基线对得上。如果 objectPoints 里的 square_size 写错比如实际 24mm 写成了 30mm基线会放大 25%所有深度值成比例缩小。动手解决的方法是标定完成后用卡尺量一次两个相机光心的实际距离和 np.linalg.norm(T) 对比。如果差 1mm 以内直接用标定值如果差得明显检查 square_size。另外一个容易忽略的点是相机支架的稳定性——只要拆装过镜头或碰过支架外参就变了标定参数不能继续用。所以精度要求高一点的场景我会把相机固定在刚性支架上标定后不再动它并给支架做标记保证每次装回去的位置一致。5. 精度验证与误差修正让测距结果经得起实测算法能跑出距离是一回事测出来的距离能不能信是另一回事。这一章给出一套可以抄的验证流程以及一个很实用的误差修正手段。5.1 误差来源拆解哪些误差躲不掉双目测距的主要误差来源有四类视差量化误差、标定残差、图像噪声、基线误差。视差量化误差来自 SGBM 输出整数视差的离散化公式 ΔZ ≈ Z² / (f × B) × Δd 直接给出了理论下限。用 f1200、B55mm 的典型参数算出来真实距离 (m)对应视差 (px)1px 视差误差对应的深度误差 (cm)1661.52336.132213.651337.9这张表告诉我们两件事第一近距离测距误差天然小1 米内做到厘米级是可能的第二不要对远处测距抱不切实际的期待5 米外一个像素的抖动就是接近 40cm 的误差。想提升远距离精度路径只有增大基线或提高图像分辨率这也是为什么工业双目往往把基线做到 200mm 以上。5.2 实测验证流程每个距离测十帧取中位数验证方法不复杂但流程要固定。我的做法是把标定板贴在一面平整纸板上作为测距目标用卷尺从相机镜头平面开始量出固定距离然后让程序输出该目标的深度值。具体流程取 0.5m、1.0m、1.5m、2.0m、3.0m 五个距离每个距离采集 10 帧。每帧取目标中心区域约 50×50 像素的深度中位数作为该帧输出。对 10 帧输出取均值和中位数消除偶发跳变。用“真实距离减去测量均值”得到绝对误差除以真实距离得到相对误差。实测数据大概率长这样误差随距离明显放大真实距离 (m)测量均值 (m)绝对误差 (m)相对误差0.500.490.012.0%1.000.970.033.0%1.501.450.053.3%2.001.890.115.5%3.002.710.299.7%这个趋势和 5.1 的量化误差曲线基本吻合。如果实测相对误差明显大于理论曲线说明标定或匹配质量还有提升空间不要急着上修正先回头检查第 3 章的参数。5.3 用最小二乘拟合修正系统误差当实测偏差呈现近似线性的趋势时可以用一组已知距离对做线性拟合把测量值映射到真实值。这个方法本质是给这个标定状态下的系统做一次输出校准不是通用解法但非常实用import numpy as np z_measured np.array([0.49, 0.97, 1.45, 1.89, 2.71]) # 实测均值 z_truth np.array([0.50, 1.00, 1.50, 2.00, 3.00]) # 真实距离 coef np.polyfit(z_measured, z_truth, 1) a, b coef[0], coef[1] def correct_depth(z_in): return a * z_in b print(correction: y %.4f * x %.4f % (a, b)) print(2.55m corrected to:, correct_depth(2.55))polyfit 一次拟合出线性系数 a 和 bcorrection 函数把任意测量值映射到修正后的距离。拟合数据必须覆盖你实际工作的距离范围只拟合 1 米以内数据然后外推到 5 米不如不修。注意这套拟合系数和当前标定状态绑定重标定之后必须重新采集数据拟合否则原来的系数反而会把结果带偏。拟合修正后建议回到 5.2 的流程重新验证一遍确认修正没有引入新的偏差。6. 从单帧到实时测距降采样、ROI 与滤波的三个技巧6.1 降采样到 640×480帧率才能提上来SGBM 的计算量和像素数基本成正比1280×720 的原始分辨率跑一帧匹配可能花掉上百毫秒直接降采样到 640×480 就能把耗时降到原来的四分之一左右。做降采样要特别注意用 INTER_AREA 插值避免棋盘格和物体边缘出现摩尔纹scale 0.5 frame_l_small cv2.resize(left_rect, None, fxscale, fyscale, interpolationcv2.INTER_AREA) frame_r_small cv2.resize(right_rect, None, fxscale, fyscale, interpolationcv2.INTER_AREA) roi (80, 120, 480, 240) # x, y, w, h根据目标活动区域裁剪 l_roi frame_l_small[roi[1]:roi[1]roi[3], roi[0]:roi[0]roi[2]] r_roi frame_r_small[roi[1]:roi[1]roi[3], roi[0]:roi[0]roi[2]]ROI 裁剪的效果比降采样更明显视差图只有原来几分之一匹配耗时几乎线性减少。不过切 ROI 时要注意目标不能超出区域否则会丢目标。降采样后深度公式里的焦距也要同步缩放f 乘以 0.5 即可。6.2 深度跳变抑制时间中值滤波加空间一致性检查单帧测距总会有偶发跳变原因可能是多个目标边缘、相机噪声、或者 SGBM 在低纹理区域的不稳定匹配。我的处理方式是两段滤波先在视差图上做 5×5 中值滤波去掉孤立噪点再对同一个目标的时间序列做中值滤波取最近 5~10 帧的中位数作为输出跳变幅度能压掉大半。6.3 视差置信度过滤宁可没深度也不给错深度给深度值加一个简单的置信度掩膜把 SGBM 输出中的低置信度区域直接置为 NaN比让错误距离进入后续逻辑安全得多。常用做法是将原图梯度很小且视差变化剧烈的像素视为不可靠区域并配合左右一致性检查的结果一起过滤。我从这套源码里学到的最重要一课就是凡事以标定为先。自从那回把没重标定的参数用到了新支架上、机器人差点撞上花盆之后我形成了个习惯相机只要被碰过、镜头拧过、支架换过强制重跑一遍标定加一次 1.5 米处的实测校验然后再去调匹配和滤波。这套流程不保证量产精度但对原型验证和竞品 demo 已经完全够用按这个顺序走一遍能少熬不少夜。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网