资讯动态

Python双目立体视觉测距系统源码:标定、极线校正与SGBM实战

发布时间:2026/10/2 3:11:10 来源:尧图企业网站定制
简介Python双目立体视觉测距系统源码为具备一定Python基础的开发者与高校机器视觉学习者提供一套可运行的参考实现覆盖双摄像头标定、立体匹配、视差计算与距离测量等核心环节适用于课程设计、入门实践或小型项目原型验证。资源共6个文件以stereo_vision.py源码为主体配套Markdown说明文档、9x6棋盘格标定图像、license许可声明另含zip与zbak格式备份文件便于查看初始版本与修改版本差异压缩包整体仅79KB结构精简适合快速下载并直接开展调试。目前已有40人学习下载说明该工程具备一定的参考价值。使用时可结合棋盘格图像完成相机内参标定通过源码理清立体匹配与视差计算逻辑再根据说明文档中的参数配置和环境限制提示进一步验证光照、纹理特征等因素对测距结果的影响并据此调整算法或硬件方案形成完整的双目测距调优思路。1. 双目立体视觉测距系统源码先跑通视差再谈测距精度把两个摄像头摆在一条直线上同时拍同一个目标左右两幅图里的物体会出现横向位置差这个差值就是视差。“Python实现双目立体视觉测距系统源码”要解决的核心问题就是把视差换算成物理距离目标越近视差越大目标越远视差越小。它适合机器人避障、车辆预瞄、货架体积粗测这类不想依赖激光雷达的测距需求。要做的事包括相机标定、极线校正、SGBM立体匹配以及最后的距离输出。难点不在Python语法而在标定是否收敛、视差参数是否贴合你的实际场景标定错了后面所有代码都白写。2. 为什么用Python搭双目测距标定、极线校正、SGBM三根支柱2.1 双目测距不是“两个摄像头测角度”这么简单很多人一提双目测距第一反应是“根据两个摄像头各自的朝向算夹角”好像两个相机做三角测量就完事了。真实情况要麻烦得多你并不知道左右图像素之间谁和谁对应。同一个空间点在左图是第1024列在右图可能是第1018列也可能因为纹理重复被认成第1030列。这个像素匹配问题不解决夹角算得再准也没有意义。计算机双目视觉的做法是先找到左图某个像素在右图中的匹配位置算两者列坐标之差这个差值就是视差disparity。如果两个相机光轴平行、成像平面严格对齐一个空间点在左右图上的投影只存在水平方向位移匹配搜索被压缩到同一行这就是极线约束。极线约束的价值是把二维搜索变成一维搜索既降低计算量也减少误匹配。反直觉的一点是测距误差不随距离均匀增加而是近似按距离的平方放大。同一个1像素视差误差在1米处可能只差十几毫米到10米处能差出一米多。所以双目测距通常只在近中距离几米内有实用价值想靠它测20米开外的目标必须先接受这个物理上限再谈算法优化。2.2 Python生态下的标定与立体匹配模块分工在Python里搭一个双目测距系统本质上是在调度OpenCV里已经用C优化好的几段算法。角点检测findChessboardCorners、立体标定stereoCalibrate、极线校正stereoRectify都在calib3d模块立体匹配的StereoSGBM_create也在calib3d图像重映射remap属于imgproc。外层用NumPy做数组操作用matplotlib或OpenCV窗口做可视化。这套组合适合做技术验证不推荐在大规模量产时裸用。Python层的调用开销对整体耗时影响很小真正吃性能的是SGBM匹配和remap。我一般把分辨率控制在640×480用VGA尺寸跑通全流程确认算法逻辑没毛病之后再决定要不要为了帧率降到320×240或者把匹配范围裁剪成感兴趣区域ROI。用Python开发还有一个好处调试自由度大。标定结果存在npz文件里SGBM参数可以做成配置文件热加载换一组相机不用改代码替换标定文件就行。这对做设备选型对比特别有用。2.3 从视差到深度焦距、基线和像素单位必须统一最核心的深度公式是 depth f × b / d。f是焦距单位是像素b是双目基线即左右相机光心之间的物理距离d是视差单位是像素。代进公式之后depth的单位和b保持一致。标定得到基线是120mm输出深度就是120mm量级如果把基线当成0.12m深度也会变成0.12m量级差着1000倍。这个单位坑几乎每个新手都会踩一次。在OpenCV里更省事的做法是让stereoRectify生成一个Q矩阵再用cv2.reprojectImageTo3D(disparity, Q)一次算出每个像素的三维坐标。它把焦距、基线、主点偏移全部打包在Q中免去手工换算。但你要知道背后的物理意义否则调试时出了NaN都不知道去哪查。单位换算之外还有一对概念容易被忽略立体标定输出的旋转矩阵R和平移向量T描述的是右相机相对左相机的位姿。T的模长就是基线长度但只有当你的标定棋盘格尺寸单位设成mm时T才以mm为单位。如果标定时把棋盘格边长传成0.028而不是28基线会变成0.000几深度输出就会大得离谱。标定之前先确认棋盘格世界坐标单位与后续输出单位一致。3. 最小可跑通实现从标定板到距离输出的五段代码3.1 采集标定板照片1015组够用但视角要按这三条来标定是整条链路的地基但我不建议一上来拍三五十组浪费体力也没必要。常见做法是打印一张9×6内角点的棋盘格贴平整左右相机同时抓拍1015组。每组画面里棋盘格必须完整出现在左右两图的公共视野中这是硬条件。三条拍摄经验直接影响标定质量。第一棋盘格要有前后移动、倾斜、旋转不要始终正对着相机否则内参解算会退化焦距和主点互相“补偿”看起来RMS很低实际测距偏差很大。第二让棋盘格频繁出现在图像四角和边缘畸变系数才能被充分激励只拍画面中央畸变模型根本拟合不出来。第三棋盘格不能有明显的弯曲手拿着拍容易抖动和轻微变形最好贴在亚克力板或泡沫板上。拍完立刻抽查几组角点检测失败的直接删掉。那种一侧暗角严重、棋盘格有一半在阴影里的照片留着只会拉低标定质量。我宁可用10张高质量图也不用25张凑数的图。3.2 立体标定脚本与重投影误差自检下面是立体标定的最小脚本。目录结构为calib/left/.jpg和calib/right/.jpg左右文件名按相同顺序排序。import numpy as np import cv2 import glob # 棋盘格内角点(9,6) 指横向9个、纵向6个内角点 pattern (9, 6) objp np.zeros((pattern[0] * pattern[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) left_files sorted(glob.glob(calib/left/*.jpg)) right_files sorted(glob.glob(calib/right/*.jpg)) objpoints [] imgpoints_l [] imgpoints_r [] criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for lf, rf in zip(left_files, right_files): left cv2.imread(lf) right cv2.imread(rf) gray_l cv2.cvtColor(left, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(right, cv2.COLOR_BGR2GRAY) found_l, corners_l cv2.findChessboardCorners(gray_l, pattern, None) found_r, corners_r cv2.findChessboardCorners(gray_r, pattern, None) if found_l and found_r: # 亚像素角点细化能压掉一部分标定误差 corners_l cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints_l.append(corners_l) imgpoints_r.append(corners_r) # None表示让OpenCV从角点自行估计初始内参 ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, None, None, None, None, gray_l.shape[::-1], # 输入必须是(W,H)这里gray_l.shape是(H,W) flagscv2.CALIB_FIX_ASPECT_RATIO ) print(RMS , ret) np.savez(stereo_calib.npz, K1K1, D1D1, K2K2, D2D2, RR, TT)逻辑说明整个脚本分三步先把棋盘格真实世界坐标展开成三维点再检测左右图像上的角点最后用立体标定同时解出左右内参、畸变、相对旋转和平移。cornerSubPix是对初步角点做亚像素级位置修正能明显降低RMS。参数说明pattern是内角点个数不是棋盘格方块数打印9×6棋盘格时这里有54个内角点。criteria里的30是迭代次数0.001是亚像素迭代精度。stereoCalibrate返回的ret是整体重投影误差单位是像素经验上低于1.0能用低于0.3很理想。如果ret超过1.5优先检查是不是有模糊照片或棋盘格没拍全。npz文件保存的是左右内参、畸变系数和两相机相对位姿后续校正和Q矩阵都要用到。3.3 极线校正与remap让左右图严格对齐到同一行标定拿到R和T之后下一步是极线校正。这一步把左右图重投影到一个共同的平面让同名点落在同一行。h, w gray_l.shape[:2] # 用CALIB_ZERO_DISPARITY让两幅图的主点保持相同便于后续Q矩阵使用 R1, R2, P1, P2, Q, valid1, valid2 cv2.stereoRectify( K1, D1, K2, D2, (w, h), R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0 ) # 提前生成映射表之后每帧只用remap不要重复计算 map1_l, map2_l cv2.initUndistortRectifyMap(K1, D1, R1, P1, (w, h), cv2.CV_16SC2) map1_r, map2_r cv2.initUndistortRectifyMap(K2, D2, R2, P2, (w, h), cv2.CV_16SC2) def rectify(img, map1, map2): return cv2.remap(img, map1, map2, cv2.INTER_LINEAR) rect_left rectify(left, map1_l, map2_l) rect_right rectify(right, map1_l, map2_r) # 注意这里应该用map1_r/map2_r上面rect_right这一行的写法是刻意演示一个最常见的错误右图必须用右图的映射表map1_r/map2_r用左表会导致图像交叉错乱很多人查半天发现是这里贴错了。参数说明stereoRectify的alpha控制输出图像裁剪范围alpha0会把黑色边框裁掉图像利用率高alpha1保留全部像素边缘有较多无效区域。一般选0即可。Q矩阵是视差转三维坐标的关键后面click测距会用到。initUndistortRectifyMap生成的是像素映射表CV_16SC2格式保证映射精确度注意不能改成CV_32FC1否则remap速度会下降且容易出现插值锯齿。3.4 SGBM生成视差图参数含义一次说清OpenCV自带的StereoSGBM是目前社区里最常用的均衡方案。最小复现代码如下。def create_sgbm(): block 11 matcher cv2.StereoSGBM_create( minDisparity0, numDisparities16 * 5, # 必须是16的倍数越大越能覆盖近处物体 blockSizeblock, # 奇数3~11越大视差越平滑但细节越模糊 P18 * 3 * block ** 2, # 相邻像素小梯度平滑惩罚 P232 * 3 * block ** 2, # 相邻像素大梯度惩罚通常取P1的4倍 disp12MaxDiff1, # 左右一致性检查容差越大越允许左右视差不一致 preFilterCap63, # 图像预处理截断值光照差异大时调大到100 uniquenessRatio10, # 匹配唯一性比例越大误匹配越少但空洞也越多 speckleWindowSize100, # 滤波器窗口0表示关闭100能去掉小噪点 speckleRange1, # 斑点和周围允许的最大视差差异 modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) return matcher disparity matcher.compute(rect_left, rect_right).astype(np.float32) / 16.0逻辑说明SGBM对左右校正图逐像素搜索并计算匹配代价再用半全局路径聚合平滑。返回的原始视差是定点数放大16倍存储所以必须除以16还原成真视差这是出场率最高的“误用”点。除以16之后无效像素通常是0或负值后续通过掩码过滤。参数说明numDisparities决定最大搜索范围越小近处物体越容易出现黑洞但也不是越大越好过大会引入远处噪声和额外耗时。P1和P2控制视差图的平滑程度P2数值过大会把物体边界抹平过小会让视差图出现大量跳变。preFilterCap处理左右图亮度差两张摄像头曝光不一致时优先调它。uniquenessRatio和speckleWindowSize是“去噪”和“保细节”之间的天平处理弱纹理场景时我会先把uniquenessRatio降到8同时把speckleWindowSize提到200。3.5 鼠标点击测距深度分量与三维模长的两种读数有了Q矩阵和视差图距离输出只差一步。points3d cv2.reprojectImageTo3D(disparity, Q) def mouse_callback(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: # reprojectImageTo3D返回的三维坐标在z方向和视觉意义上要分辨清楚 z points3d[y, x, 2] norm np.linalg.norm(points3d[y, x]) print(fpixel({x},{y}) 深度z{z:.2f}mm 相机到点距离{norm:.2f}mm) cv2.namedWindow(left, cv2.WINDOW_NORMAL) cv2.setMouseCallback(left, mouse_callback) cv2.imshow(left, rect_left) cv2.waitKey(0)逻辑说明reprojectImageTo3D把每个有效视差像素换算成以左相机光心为原点的三维坐标。z是沿光轴方向的深度分量norm是空间点到光心的欧氏距离。测量前方障碍物距离时通常用norm测量高度或地形起伏时用z。代码中points3d[y, x]取了三个分量使用之前最好再判断points3d[y, x, 2]是否有限大避免无效视差带来的极端值污染输出。用鼠标逐点抠测距只是调试手段。真正工程化时应该把目标检测框的中心点坐标传进来自动取该点周围若干像素的深度中位数作为最终距离输出。逐点取深度很容易踩中椒盐噪声取中位数能直接把单个离群点屏蔽掉。4. 双目测距常见问题与避坑记录误差突然变大的5个原因4.1 RMS只有0.2测距还是偏标定板角度太单一现象stereoCalibrate返回的RMS低到0.2极线校正后左右图也基本对齐但实测一段距离误差从几十毫米跳到几百毫米完全不能用。原因RMS低只说明算法对这组标定图的拟合残差小不代表内外参数逼近物理真实。如果所有标定图都把棋盘格放在画面中央姿态始终正对相机内参里焦距、主点之间存在相互补偿容易出现“看标定板很准换真实物体就飘”的情况。解决重新采集标定图。前后移动距离要拉开倾斜角度至少超过30度棋盘格要有上下左右四个象限的覆盖。重标定后看两个指标RMS小于1.0同时单张重投影误差没有超过1.5像素的突刺。两者都满足再谈精度。4.2 近处还行远处就飘光圈和对焦没有锁死现象上午标定完测距误差很小到了下午同一套参数误差明显变大或者连续工作半小时后近距离误差还好远距离越测越离谱。原因大多数USB摄像头带自动对焦和自动曝光。标定时镜头停在某个对焦距离上运行时镜头看到远方目标自动拉焦焦距变了标定参数立刻失效。自动曝光变化还会让左右图亮度不一致SGBM匹配质量跟着下降。解决在摄像头驱动里把自动对焦和自动曝光关掉改成手动固定值对焦环用胶带锁死。选型时优先挑没有自动对焦的定焦模组或者能用UVC协议锁定参数的型号。远距离监测场景我还会用一张黑白棋盘格放在固定距离每隔一段时间跑一次标定确认内参没有漂移。4.3 视差图一片黑洞numDisparities与blockSize互相打架现象近距离物体边缘有视差但物体中间区域大片黑色或者整幅图都有类似“挖洞”的黑色。原因numDisparities太小匹配只搜了很小一段视差范围近处目标的真实视差超出搜索上界blockSize太大物体内部作为平滑区域无法产生足够明显的匹配代价差异代价曲线找不出唯一谷值。解决先确认场景最小距离估算最大视差。比如基线120mm、焦距500像素、最近目标0.8m最大视差大概是75像素numDisparities就得设96或128。同时把blockSize从11降到7或5细节恢复能力会明显增强。黑色变少之后噪声会增加再用speckleWindowSize和不一致检查来压。4.4 深度图像撒了雪花speckle滤波没开或窗口太小现象物体表面到处是极远或极近的闪烁噪点深度图看起来像老式电视机雪花屏。原因SGBM本质上是逐像素匹配弱纹理区域、反光区域、重复纹理区域都可能给出错误匹配。单个误匹配点在空间上是孤岛状分布speckleWindowSize默认100本就是为了去除这种斑点但误匹配连成片时窗口不够大就清不干净。解决把speckleWindowSize提高到200500speckleRange保持12。同时检查曝光白色墙、白色瓷砖、天空这类大面积弱纹理区域不要指望纯靠算法硬扛。如果场景里大量出现这类表面更实际的方法是调整机位或补光让纹理信息多起来。4.5 分辨率一高就卡顿SGBM复杂度与分辨率、搜索范围成正比现象640×480能跑到20 FPS切到1280×720直接掉到5 FPS再上1080p几乎无法实时推理。原因SGBM对每个像素都要在numDisparities层做代价聚合复杂度近似O(W×H×D)。分辨率翻倍计算量不只是翻一倍搜索范围一起增加时耗时按立方级别往上走。解决先降分辨率到640×480这是效率和精度的平衡点。如果还需要高清细节就把检测框的ROI扩大2030像素后单独做SGBM其余区域直接忽略。另一个常用做法是低分辨率层次出视差值再结合原始高清边缘图做深度细化但那是性能优化的高级玩法基础版先把ROI用明白。5. 精度验证与进阶把测距系统变成能交付的测量结果5.1 用重投影误差和极线误差量化标定质量stereoCalibrate返回的整体ret能反映全局标定质量但不能定位是哪张图出了问题。我习惯对每对图像单独重投影找到误差最大的图删掉再重跑。代码逻辑是把标定输出结果施加到该图的角点上投影回像素坐标再和实际检测到的角点坐标比。# 假设某张图的rvec/tvec来自cv2.solvePnP或立体标定中间结果 proj_points, _ cv2.projectPoints(objp, rvec, tvec, K1, D1) err np.mean(np.linalg.norm(corners_l.reshape(-1, 2) - proj_points.reshape(-1, 2), axis1)) print(fper-view RMS {err:.3f} px)单张重投影误差超过1.5像素优先查模糊和反光删掉重拍。我一般保留误差在1.0以内的图片参与最终标定这样整体RMS通常能压进0.3以内。极线误差的验证更贴近实测。校正后的左右图同一个物理点应该落在完全相同的行上。在画面上任意框几个目标画一条水平参考线观察左右图目标是否处于同一行。批量做法是写个脚本对多张实拍图提取同步点计算平均绝对行差低于0.5像素说明极线校正合格超过1像素需要检查R1/R2或者成像是否发生畸变。这一步是SGBM能否信任的前提。5.2 用已知距离对照表做实测验证标定指标再好看最终都要上卷尺说话。验证方法相机固定目标分别放在0.5m、1.0m、1.5m每个距离采50帧取深度值的中位数作为输出结果统计误差。实测距离(mm)视差中位数(px)输出深度(mm)绝对误差(mm)备注50062.351212近处噪声小100031.5101818正常区间150020.8154646误差明显放大为什么用中位数双目误差不是高斯分布一个误匹配的离群点就能把平均值拉到没意义。中位数能真实反映系统在“大多数情况下”测得准还是不准。从表里能看出误差随距离增大不是线性增长而是近似二次增长因为深度对视差求导是负二次项关系。所以不要只测一个距离就宣布系统可用至少测“最近、中间、最远”三个点画出误差曲线才能决定盲区和有效量程。如果近处误差大优先查曝光低亮度下SGBM的代价响应不够尖锐如果远处误差大优先查标定时对焦位置或者加大numDisparities。5.3 升级路径从SGBM到深度学习立体匹配当SGBM的所有参数都调过一遍误差仍不满足要求就该考虑换掉立体匹配阶段而不是推倒整条标定链路。当前社区常见的深度学习方案包括RAFT-Stereo系列、IGEV系列和CREStereo它们输入依旧是校正后的左右图输出依旧是视差图前面的标定结果和Q矩阵完全复用替换成本很低。深度学习立体匹配在弱纹理、重复纹理、遮挡区域的表现通常明显好于SGBM代价是推理速度。桌面级GPU下处理720p图像一帧从几十毫秒到几百毫秒不等普通嵌入式平台很难实时。我一般建议把这个方案用在离线处理和远距离精度修复场景先离线生成场景的稠密深度图前端只做查表和插值。线上实时避障则继续用SGBM降分辨率运行两边各取所长。选型前先想清楚三个问题测距范围是几米还是几十米允许一帧匹配耗时是多少目标表面纹理是丰富还是贫瘠。这三条决定了SGBM够用还是必须上深度学习。盲选深度学习模型不仅算力吃紧还会因为训练集和真实场景分布差异带来新的偏见。6. 后处理与亚像素把视差图做干净再输出精度和观感都能上一个台阶6.1 先做有效性掩码和中值滤波很多教程直接拿SGBM输出就去做三维坐标换算结果点云里全是离群点。正确习惯是先做一步掩码把无效视差清零再补中值滤波valid (disparity 0) np.isfinite(disparity) disp_clean np.where(valid, disparity, 0) disp_clean cv2.medianBlur(disp_clean.astype(np.float32), 5)中值滤波窗口用5×5能清掉大部分孤立噪点但代价是物体边缘会被轻微圆化。如果后面要做尺寸测量改成3×3边缘保真度好很多。6.2 用抛物线拟合做亚像素视差SGBM默认输出整像素视差直接用于测距会导致深度值在很近的距离上出现阶梯跳变。低成本做法是对最优视差附近三个点的匹配代价做二次曲线拟合用抛物线顶点作为亚像素视差。实现上需要从SGBM内部拿到代价量OpenCV没有直接接口实操中可以从视差图对附近像素做差分近似或者改用带亚像素输出的匹配器。这一步能把近距离误差削减10%到20%对远距离帮助有限因为远距离视差本身就很小亚像素修正量按比例也变小。6.3 用ROI把算力花在刀刃上实际场景里整个画面的有效匹配区域往往只占30%。把ROI限制在目标区域并外扩2030像素SGBM计算量几乎线性下降目标区域内部的匹配质量反而提升因为远处杂波不再参与代价聚合。这套流程跑通之后最大的感受是双目测距里80%的问题出在标定和镜头锁焦而不是立体匹配算法。不要一上来就把SGBM参数调到激进先拿卷尺测三个距离看重复测量误差是否控制在5%以内再谈绝对精度。希望这些经验帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑