
简介这份源码包围绕 Python 实现的双目立体视觉深度图生成任务面向计算机视觉学习者、相机标定与三维重建相关开发者旨在通过左右视图的视差计算输出深度图。资源共 59 个文件压缩包约 2.95MB主要包含 Python 脚本、JPEG/PNG 左右相机图像样本、XML/JSON 配置文件、pyc 编译文件与 Git 忽略文件等。其中 Python 脚本覆盖图像采集、单目/双目相机标定、视差计算与深度图生成等环节图像与配置文件则分别用于算法验证和参数调整。已有 346 人学习下载。压缩包中还附带 requirements.txt 环境依赖、README/readme.txt 说明以及左右场景样例集与深度图/视差图结果示例便于对照复现。目录按 image、result、配置等区分适合在此基础上扩展标定工程或理解双目视觉全流程。1. 为什么 Stereo_DepthMap 值得动手做一遍双目立体视觉解决的核心问题就一个单目相机拍出来的画面没有尺度信息你不知道画面里那辆车距离你 5 米还是 50 米。而双目方案通过两个固定间距的相机同时拍摄同一场景利用视差disparity反推深度是机器人避障、自动驾驶、三维重建领域最经典也最可落地的深度获取手段。这里要讲的 Stereo_DepthMap本质上是把「从双图到深度图」的完整链路打通标定、极线校正、立体匹配、视差到深度的转换。对 Python 开发者来说OpenCV 把这条链路的绝大多数环节都封装好了你不需要从零实现特征匹配或块匹配算法但你必须理解每个环节的输入输出和参数含义否则出来的深度图会是一张噪声图。这篇内容按从业者的实操路径展开目标是你拿到两个相机哪怕是两个 USB 摄像头就能跑通并调出可用的深度图。2. 相机标定Stereo_DepthMap 的第一块基石2.1 为什么标定结果直接决定深度图质量双目深度估计的数学基础是三角测量而三角测量的前提是知道两个相机的内参焦距、主点、畸变系数和外参两相机之间的旋转和平移。很多人第一次跑双目代码直接拿两张图像进立体匹配出来的视差图完全是噪声——问题几乎都出在没标定或标定板图像数量不足上。内参错了极线校正就歪了立体匹配的搜索范围也随之失效这是整条链路里最不能偷懒的一步。OpenCV 提供cv2.calibrateCamera做单目标定cv2.stereoCalibrate做双目标定。常见做法是打印一张 9×6 的棋盘格用两个相机同时采集 20~30 组不同姿态的图片对。注意棋盘格必须完全在画面内而且姿态要覆盖倾斜、远近、上下左右偏移避免只在一个平面内平移。2.2 单目标定的最小代码import cv2 import numpy as np # 棋盘格内角点数 pattern_size (9, 6) # 棋盘格每个格子的实际边长单位 mm square_size 25.0 # 准备对象点所有角点的三维坐标z0 objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size obj_points [] # 真实世界坐标 img_points [] # 图像像素坐标 images [...] # 你的标定图片路径列表 for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) found, corners cv2.findChessboardCorners(gray, pattern_size, None) if found: obj_points.append(objp) # 亚像素精细化角点坐标 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) img_points.append(corners2) else: print(f{fname} 未检出角点) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) np.savez(calib_left.npz, mtxmtx, distdist)cornerSubPix做亚像素细化是因为findChessboardCorners返回的角点精度只有像素级而标定对精度极其敏感。objp里的坐标单位是 mm后续立体匹配出的视差也在这个尺度下换算深度这里写错单位深度图的数值整体就会偏差一个数量级。np.savez保存内参矩阵和畸变系数建议左右相机各存一份。2.3 双目标定与极线校正的前置参数单目标定完成后用cv2.stereoCalibrate求两相机的外参ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( obj_points, # 同一组对象点 img_points_left, # 左相机图像角点 img_points_right, # 右相机图像角点 mtx_left, dist_left, mtx_right, dist_right, gray_size, # 图像尺寸 (width, height) flagscv2.CALIB_FIX_INTRINSIC # 内参已标定只优化外参 )R是右相机相对左相机的旋转矩阵T是平移向量。这两个矩阵直接决定了后续stereoRectify的投影矩阵 Q而 Q 是把视差图转成三维坐标的关键。CALIB_FIX_INTRINSIC标志位意味着只优化外参这要求前面单目标定足够准如果单目标定质量一般可以去掉这个 flag 让双目标定同时微调内参代价是计算更慢且可能过拟合。3. 极线校正把二维搜索降成一维搜索3.1 为什么要校正对极约束的工程意义立体匹配最朴素的想法是左图某个像素在右图同一行上去找最相似的点行差就是视差。但实际安装的两个相机几乎不可能做到光轴完全平行、成像平面完全共面。如果不对图像做校正匹配点根本不在同一行上你就得做二维搜索计算量爆炸且错误匹配率极高。极线校正通过stereoRectify计算两个单应性变换把左右图像重投影到同一个理想平面上使对应点落在同一水平线上。这一步做对了立体匹配就从二维搜索退化成沿极线的一维搜索效率和质量都是质的飞跃。3.2 stereoRectify 与 initUndistortRectifyMap 的完整流程# 图像原始尺寸 h, w gray_size[1], gray_size[0] # 校正参数alpha 控制裁剪范围0 表示黑色区域最少 R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( K1, D1, K2, D2, gray_size, R, T, alpha0, flagscv2.CALIB_ZERO_DISPARITY ) # 生成校正映射表 map1_left, map2_left cv2.initUndistortRectifyMap( K1, D1, R1, P1, gray_size, cv2.CV_32FC1 ) map1_right, map2_right cv2.initUndistortRectifyMap( K2, D2, R2, P2, gray_size, cv2.CV_32FC1 ) # 对每帧图像做重映射 rect_left cv2.remap(img_left, map1_left, map2_left, cv2.INTER_LINEAR) rect_right cv2.remap(img_right, map1_right, map2_right, cv2.INTER_LINEAR)P1和P2是校正后的投影矩阵Q是视差到三维坐标的转换矩阵最后一步从视差图生成深度图时直接用它做矩阵乘法。alpha0意味着校正后的图像会裁剪掉黑色无效区域缺点是丢失一部分视野如果做三维重建需要完整图像可以调大到alpha1但边缘区域的立体匹配质量会变差。校正完成后务必做一步验证在左右校正图上手动画几条水平线肉眼确认对应特征点在同一条水平线上。这一步是排除标定和校正参数错误的最快手段不要省。4. 立体匹配核心SGBM 的最小可用实现与参数调法4.1 SGBM 的原理认知与选型理由为什么这里不用 Python 直接写块匹配立体匹配算法分三类局部方法如 BM、半全局方法SGBM和全局方法如 Graph Cuts。纯 Python 写逐像素块匹配循环一多就卡成 PPT而 OpenCV 的 SGBM 是基于 C 实现的半全局匹配用动态规划在多个方向上进行代价聚合效果接近全局方法但速度能跑实时。SGBM 的基本思想是对每个像素的候选视差计算匹配代价然后在多个路径上聚合代价最后通过胜者为王WTA选出最优视差。之所以不选 BM是因为 BM 只考虑局部窗口在纹理稀疏区域和深度不连续区域容易出现大面积错误SGBM 的代价聚合机制让它在这些区域明显更稳。在 Python 生态里做立体匹配SGBM 不是唯一的选项但它是精度和速度平衡得最好的默认选择。深度学习方案如 RAFT-Stereo精度更高但需要 GPU 和预训练模型落地成本高SGBM 在 CPU 上跑 VGA 分辨率能达到实时帧率这是它至今仍是工程首选的原因。4.2 StereoSGBM_create 的关键参数与推荐值import cv2 # 创建 SGBM 匹配器 sgbm cv2.StereoSGBM_create( minDisparity0, numDisparities16 * 6, # 必须能被 16 整除 blockSize5, # 奇数3~11 之间 P18 * 3 * blockSize**2, # 平滑惩罚参数控制视差变化小的情况 P232 * 3 * blockSize**2, # 平滑惩罚参数控制视差变化大的情况 disp12MaxDiff1, # 左右一致性检查的最大容许差异 preFilterCap63, # 预处理滤波器的截断值 uniquenessRatio10, # 视差唯一性百分比阈值 speckleWindowSize100, # 滤除散斑噪声的窗口大小 speckleRange2, # 散斑滤波的视差范围 modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) # 计算视差图输入必须是校正后的灰度图 disparity sgbm.compute(rect_left_gray, rect_right_gray).astype(np.float32) / 16.0numDisparities决定了深度探测范围视差越大表示物体越近这个值越小算法越快但近距离物体测不到。P1和P2是 SGBM 最核心的平滑项P2 吸收视差突变P2 太小会让深度边缘产生裂缝太大则会让细小物体被平滑掉。常用经验是 P2 ≈ 4~5 × P1。disp12MaxDiff1开启左右一致性检查能剔除遮挡区域的错误匹配代价是这些像素会被标记为无效。speckleWindowSize用于滤除视差图上的小斑点如果物体边缘出现大量小黑点把这个值调大。4.3 从视差图到深度图的几何转换# 使用 stereoRectify 得到的 Q 矩阵把视差转成三维坐标 points_3d cv2.reprojectImageTo3D(disparity, Q) # 深度 Z 坐标 depth points_3d[:, :, 2] # 将无效视差0的深度置为 0 或者一个最大距离 depth[disparity 0] 0.0Q矩阵的数学含义是[X, Y, Z, W]^T Q * [x, y, disparity, 1]^T其中 Z W 的倒数取负。这里有个常见坑stereoRectify返回的离散视差图存的是整数左移了 4 位需要用disparity.astype(np.float32) / 16.0还原真实视差很多初学者忘了这步导致深度值整体放大 16 倍。reprojectImageTo3D输出的是 N×M×3 的数组直接用切片取 Z 通道就是深度图。提示disparity[disparity 0] 0.0这行不能省因为disp12MaxDiff检查会把遮挡区域的视差置为无效负值这些点直接参与计算会产生极大的错误深度。5. 从标定到深度图的完整工作流一个能跑通的 Python 代码骨架import cv2 import numpy as np class StereoDepth: def __init__(self, calib_file): data np.load(calib_file) self.K1, self.D1 data[K1], data[D1] self.K2, self.D2 data[K2], data[D2] self.R, self.T data[R], data[T] self.img_size (int(data[width]), int(data[height])) # 极线校正参数 self.R1, self.R2, self.P1, self.P2, self.Q, _, _ cv2.stereoRectify( self.K1, self.D1, self.K2, self.D2, self.img_size, self.R, self.T, alpha0, flagscv2.CALIB_ZERO_DISPARITY ) # 生成映射表 self.map1_l, self.map2_l cv2.initUndistortRectifyMap( self.K1, self.D1, self.R1, self.P1, self.img_size, cv2.CV_32FC1) self.map1_r, self.map2_r cv2.initUndistortRectifyMap( self.K2, self.D2, self.R2, self.P2, self.img_size, cv2.CV_32FC1) # SGBM 参数 self.sgbm cv2.StereoSGBM_create( minDisparity0, numDisparities16 * 6, blockSize5, P18 * 3 * 5**2, P232 * 3 * 5**2, disp12MaxDiff1, preFilterCap63, uniquenessRatio10, speckleWindowSize100, speckleRange2 ) def process_frame(self, img_left, img_right): # 校正 rect_l cv2.remap(img_left, self.map1_l, self.map2_l, cv2.INTER_LINEAR) rect_r cv2.remap(img_right, self.map1_r, self.map2_r, cv2.INTER_LINEAR) # 转灰度 gray_l cv2.cvtColor(rect_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(rect_r, cv2.COLOR_BGR2GRAY) # 立体匹配 disp self.sgbm.compute(gray_l, gray_r).astype(np.float32) / 16.0 # 深度图 points cv2.reprojectImageTo3D(disp, self.Q) depth points[:, :, 2] depth[disp 0] 0.0 return rect_l, rect_r, disp, depth # 使用示例 stereo StereoDepth(calib_result.npz) cap_l cv2.VideoCapture(0) cap_r cv2.VideoCapture(1) while True: ret_l, frame_l cap_l.read() ret_r, frame_r cap_r.read() if not (ret_l and ret_r): break rect_l, rect_r, disp, depth stereo.process_frame(frame_l, frame_r) # 可视化深度图归一化到 0~255 便于显示 depth_viz cv2.normalize(depth, None, 0, 255, cv2.NORM_MINMAX) depth_viz cv2.applyColorMap(depth_viz.astype(np.uint8), cv2.COLORMAP_JET) cv2.imshow(Depth, depth_viz) if cv2.waitKey(1) 0xFF ord(q): breakStereoDepth类把标定参数加载、校正映射表生成和 SGBM 匹配器初始化都放在构造函数里避免每帧重复计算。process_frame的四个步骤校正-灰度-匹配-转换就是整个 Stereo_DepthMap 设计的核心链路。注意normalize和applyColorMap只是为了可视化如果后续要做目标检测或测距直接用原始depth数据不要用归一化后的图像。cv2.VideoCapture打开两个摄像头时有些 USB 摄像头不支持同时被两个进程打开常见的做法是把两个摄像头接在不同的 USB 控制器上或者在打开前先设置分辨率一致cap_l.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap_l.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap_r.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap_r.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)分辨率不一致会导致两个图视角范围不同极线校正失效。如果遇到帧率不匹配最简单的方案是同步读取后比对时间戳丢弃不配对的那一帧。6. 深度图质量验证与三个实用排错技巧6.1 用 WLS 滤波提升深度图平滑度SGBM 输出的原始视差图在纹理稀疏区域会有大量空洞和毛刺一个低成本的改进是使用cv2.ximgproc模块的 WLSWeighted Least Squares滤波。WLS 利用左图的颜色边缘作为引导在不破坏深度边缘的前提下平滑内部区域import cv2.ximgproc as xip # 创建 WLS 滤波器需要右图的 SGBM 视差用于一致性检查 sgbm_right cv2. StereoSGBM_create( minDisparity0, numDisparities16 * 6, blockSize5, P18 * 3 * 5**2, P232 * 3 * 5**2, disp12MaxDiff1, preFilterCap63, uniquenessRatio10, speckleWindowSize100, speckleRange2 ) disp_left sgbm.compute(gray_l, gray_r).astype(np.float32) / 16.0 disp_right sgbm_right.compute(gray_r, gray_l).astype(np.float32) / 16.0 wls xip.createDisparityWLSFilter(sgbm) wls.setLambda(8000.0) # 平滑强度越大越平滑 wls.setSigmaColor(1.5) # 颜色相似度权重 filtered_disp wls.filter(disp_left, gray_l, None, disp_right) depth_filtered cv2.reprojectImageTo3D(filtered_disp, self.Q)[:, :, 2]setLambda控制平滑程度setSigmaColor控制颜色边缘的保持力度。lambda 太大会把真实的深度边缘也抹平太小则滤波等于白做一般从 8000 开始调。这个滤波通常能给深度图带来肉眼可见的质量提升而且计算开销不高值得加到正式流程里。6.2 验证深度准确度的相机到标定板距离法深度图做出来不是看颜色好看就完事必须量化验证。最直接的方法把标定棋盘格放在相机前方已知距离 d 处运行你的深度图程序取棋盘格中心区域的深度均值和 d 对比。误差在 3% 以内说明标定和匹配参数基本可靠。如果偏差大于 5%优先怀疑标定板的square_size单位写错其次是numDisparities太小导致近处视差被截断。# 假设棋盘格中心深度均值 measured_depth真实距离 real_dist error_percent abs(measured_depth - real_dist) / real_dist * 100 print(f深度误差: {error_percent:.2f}%)这个方法不需要额外传感器几行代码就能把整个链路的精度测出来比肉眼看深度图可靠得多。6.3 SGBM 参数速调参考表参数现象调整方向numDisparities过小近距离物体深度值失效或为 0增大到能覆盖最近探测距离P1/P2过大物体边界模糊细小物体丢失减小 P1/P2或同时调小 blockSizeP1/P2过小深度图噪声多表面粗糙增大 P2让平滑项起效uniquenessRatio过低纹理重复区域出现条纹状错误增大到 15~20speckleWindowSize过小深度图上散落大量孤立噪点增大到 150~200并同时调大speckleRangeblockSize过大深度边缘偏移严重减到 3~5代价是噪声略增调参的顺序有讲究先调numDisparities保证动态范围再调P1/P2控制平滑最后调uniquenessRatio和speckle清理噪声。一次只动一个参数看它对整张深度图的影响而不是盲目组合。一个容易被忽略的细节SGBM 对输入图像的亮度一致性敏感。左右相机如果是自动曝光同一场景下的亮度差异会导致匹配代价偏高。工程上通常在标定前就把两个相机的曝光和增益设为固定值而不是自动模式。如果做不到至少要在进入 SGBM 前对左右图做直方图均衡化cv2.equalizeHist一行代码就能缓解大部分亮度不一致的问题。这不算理论上的最优解但在实际项目中往往比换更复杂的匹配算法更管用。本文还有配套的精品资源点击获取