资讯动态

OpenCV双目测距实战:从标定到SGBM视差计算与深度图输出

发布时间:2026/10/18 23:55:07 来源:尧图企业网站定制
简介这份资源面向计算机视觉初学者与机器人、自动驾驶方向的开发者提供一套在Ubuntu环境下基于OpenCV与Qt实现双目测距系统的完整工程。内容围绕立体视觉展开涵盖摄像头同步与对齐、SIFT/ORB等特征匹配、半全局立体匹配、基础矩阵与单应性矩阵求解、三角测量、深度图重建及目标距离计算等关键环节并借助Qt搭建图像显示、参数调整与测距结果展示的交互界面。压缩包为rar格式大小约80.48MB包内文件类型与数量上游暂未提供明细下载后可按图像处理、特征匹配、深度计算等模块组织代码便于维护与扩展。目前已有743人学习下载适合希望从原理到工程落地系统掌握双目测距、并用于机器人导航或工业检测场景的读者参考同时可结合多线程加速与光照噪声处理思路进一步优化性能。1. 双目测距到底能干嘛从两个摄像头到真实距离很多人第一次听说双目测距脑子里浮现的是机器人、自动驾驶这些大词其实它最朴素的能力就一句话用两个平行摄像头拍同一场景靠左右图的视差算出每个像素离镜头有多远。我最近拆的这个基于 OpenCV 的双目测距系统就是把这件事做成了一个能跑、能改、能接自己摄像头的完整工程。它适合谁手里有俩 USB 摄像头、想给小车或机械臂加个测距模块的开发者也适合正在学立体视觉、想找个能直接跑通的 OpenCV 实战项目练手的人。整套东西不依赖深度学习权重纯几何加标定跑在普通笔记本上就能出深度图改起来门槛低这也是我愿意花时间拆它的原因。2. 系统架构与目录结构先看清数据怎么流动拿到一个视觉项目我习惯先不急着跑而是把数据流捋一遍。双目测距的链路其实很清晰采集左右图 → 去畸变校正 → 立体校正对齐 → 计算视差 → 视差转深度 → 输出距离。这个项目的目录结构基本就是按这条链路切的每个环节一个模块改哪一步都不会牵一发动全身。2.1 目录结构与模块职责一个典型的工程目录长这样我按自己的理解标注了每个文件的作用stereo_distance/ ├── config/ │ └── stereo_params.yaml # 标定结果内参、畸变、R/T、Q 矩阵 ├── calibration/ │ ├── capture_calib.py # 采集棋盘格图像 │ └── calibrate.py # 执行双目标定输出 yaml ├── core/ │ ├── rectify.py # 立体校正生成映射表 │ ├── disparity.py # SGBM 视差计算 │ └── depth.py # 视差转深度、测距 ├── utils/ │ └── camera.py # 双摄像头同步采集封装 ├── main.py # 主入口串起整条链路 └── requirements.txtconfig放标定产物是整个系统的“地基”所有几何计算都依赖它calibration是离线跑一次的标定工具core是运行时核心utils处理硬件差异。这种分层的好处是标定和运行解耦——你标定一次之后换场景只跑main.py就行。2.2 技术选型为什么是 OpenCV SGBM选型上没什么玄学OpenCV 自带calibrateCamera、stereoCalibrate、stereoRectify、StereoSGBM这一整套从标定到视差一条龙不用自己造轮子。视差算法用的是半全局块匹配SGBM相比 BM块匹配它对弱纹理和光照变化更稳代价是慢一点但普通场景下帧率够用。深度换算走的是标准公式depth (f * baseline) / disparity其中f是校正后的焦距像素单位baseline是两摄像头光心距离毫米disparity是视差像素。这三个量里f和baseline来自标定disparity来自 SGBM缺一不可。理解这个公式后面调参就有方向了——视差算不准深度必然飘。2.3 数据流串讲主流程在main.py里逻辑是读标定参数 → 初始化校正映射 → 打开双摄 → 循环取帧 → 校正 → 算视差 → 转深度 → 显示/输出。每一步的中间结果都可以存图调试这点对新手特别友好哪一步不对一眼就能看出来。下面这张表是我整理的模块与关键函数对应关系方便你按图索骥去读源码模块关键函数/类输入输出rectifyinitUndistortRectifyMap标定参数左右映射表disparityStereoSGBM.compute校正后左右图视差图depthreprojectImageTo3D视差图 Q三维坐标cameraCameraPair.read设备号左右帧把这张表记住读代码时就不会迷路。3. 环境搭建与标定跑通前的必修课环境这关不难但标定这关是新手最容易翻车的地方。我见过太多人代码跑起来了深度图却是一团糊九成问题出在标定上。这一章把环境、标定、参数含义讲透。3.1 依赖安装与摄像头准备依赖很干净核心就三个pip install opencv-python opencv-contrib-python numpy pyyamlopencv-contrib-python建议装上虽然 SGBM 在主包里也有但 contrib 版包含更多立体匹配的扩展实现后续想换算法不用重装。摄像头方面两个同型号 USB 摄像头最省事分辨率尽量一致。物理安装时尽量让两个镜头光轴平行、高度一致基线两镜头间距建议 60120mm——太小深度精度差太大近处盲区大。我一般用一块硬质亚克力板把两个摄像头固定死避免使用中位移因为基线一变标定就白做了。3.2 双目标定流程与代码标定用棋盘格常见的是 9x6 内角点、方格边长 25mm 的板子。先采集图像再跑标定import cv2 import numpy as np import glob # 棋盘格内角点数列, 行注意是内角点不是方格数 PATTERN (9, 6) SQUARE_SIZE 25.0 # 单位 mm必须和实际打印尺寸一致 objp np.zeros((PATTERN[0] * PATTERN[1], 3), np.float32) objp[:, :2] np.mgrid[0:PATTERN[0], 0:PATTERN[1]].T.reshape(-1, 2) * SQUARE_SIZE objpoints, imgpoints_l, imgpoints_r [], [], [] for fname in sorted(glob.glob(calib/left_*.png)): img_l cv2.imread(fname) img_r cv2.imread(fname.replace(left_, right_)) 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, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, PATTERN, None) if ret_l and ret_r: # 亚像素精化标定精度能提升一个档次 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) 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) # 双目标定一次性拿到内外参和相对位姿 ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, None, None, None, None, gray_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC ) print(baseline(mm):, np.linalg.norm(T))这段代码的逻辑是先构造棋盘格在世界坐标下的三维点objp再对每对左右图找角点并做亚像素精化最后交给stereoCalibrate一次性解出两个相机的内参、畸变和它们之间的旋转R、平移T。T的模长就是基线打印出来和你的物理测量值对比一下差太多说明采集质量有问题。CALIB_FIX_INTRINSIC这个 flag 表示固定单目标定得到的内参如果你没单独标过单目去掉它让双目标定一起解也行但需要更多有效图像。3.3 标定结果怎么读、怎么存标定完把参数存成 yaml运行时直接读别每次重标import yaml data { K1: K1.tolist(), D1: D1.tolist(), K2: K2.tolist(), D2: D2.tolist(), R: R.tolist(), T: T.tolist(), image_size: list(gray_l.shape[::-1]) } with open(config/stereo_params.yaml, w) as f: yaml.safe_dump(data, f)参数含义上K1/K2是内参矩阵焦距和主点D1/D2是畸变系数R/T描述右相机相对左相机的位置。这几个量直接决定后面stereoRectify能不能把两幅图对齐到同一极线上。判断标定好坏有个土办法看重投影误差ret一般小于 0.5 像素算合格超过 1 像素建议重采。采集时棋盘格要覆盖画面各个角落倾斜、远近都来几张20 对左右图起步别偷懒。4. 核心模块拆解从视差图到真实距离标定是地基这一章才是真正干活的部分。我把core里三个模块拆开讲每个都给出可抄的代码和参数说明。4.1 立体校正让左右图对齐极线校正的目的是把两个相机的成像平面“摆正”让同一个物点在左右图里只差水平方向一个位移这样找匹配点就从二维搜索降到一维速度和准确率都上来了。import cv2 import numpy as np import yaml with open(config/stereo_params.yaml) as f: p yaml.safe_load(f) K1 np.array(p[K1]); D1 np.array(p[D1]) K2 np.array(p[K2]); D2 np.array(p[D2]) R np.array(p[R]); T np.array(p[T]) size tuple(p[image_size]) # 计算校正变换alpha0 表示裁剪掉无效区域画面更干净 R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( K1, D1, K2, D2, size, R, T, alpha0 ) # 预计算映射表运行时直接 remap比每帧重算快得多 map1x, map1y cv2.initUndistortRectifyMap(K1, D1, R1, P1, size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(K2, D2, R2, P2, size, cv2.CV_32FC1) def rectify(img_l, img_r): l cv2.remap(img_l, map1x, map1y, cv2.INTER_LINEAR) r cv2.remap(img_r, map2x, map2y, cv2.INTER_LINEAR) return l, rstereoRectify输出里最关键的是Q矩阵它是后面视差转三维的桥梁一定要存下来。alpha0会裁掉校正后产生的黑边如果你需要保留全部视野就设成 1代价是边缘有无效像素。initUndistortRectifyMap把去畸变和校正合并成一张查找表运行时remap一次搞定这是性能优化的关键——千万别在循环里反复调stereoRectify。4.2 SGBM 视差计算与参数调优视差图的质量几乎全看 SGBM 参数这块是调参重灾区我把常用参数和调法列出来# numDisparities 必须是 16 的倍数决定能测多近 # blockSize 是匹配块大小必须是奇数 stereo cv2.StereoSGBM_create( minDisparity0, numDisparities128, # 越大能测越近但计算越慢 blockSize5, # 3~11越大越平滑但丢细节 P18 * 3 * 5 ** 2, # 平滑惩罚项控制视差连续性 P232 * 3 * 5 ** 2, disp12MaxDiff1, # 左右一致性检查滤掉错误匹配 uniquenessRatio10, # 唯一性太小容易误匹配 speckleWindowSize100, # 滤除小连通区域噪点 speckleRange2, modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) def compute_disparity(img_l, img_r): gray_l cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) disp stereo.compute(gray_l, gray_r).astype(np.float32) / 16.0 return dispnumDisparities决定搜索范围它和最近可测距离成反比设太小近处物体视差超出范围就测不到设太大远处精度和速度都受影响。blockSize是平滑和细节的权衡纹理丰富的场景用 35弱纹理用 711。P1/P2是平滑惩罚P2一般是P1的 4 倍左右值越大视差图越平滑但边缘越糊。uniquenessRatio我一般设 1015能有效压掉误匹配。调参没有万能值建议先固定其他参数单独动numDisparities和blockSize边看视差图边调。4.3 视差转深度与测距输出拿到视差图用Q矩阵重投影就能得到三维坐标Z 分量就是距离def disparity_to_depth(disp, Q): # 无效视差置零避免产生离谱的深度值 disp_valid disp.copy() disp_valid[disp_valid 0] 0 points_3d cv2.reprojectImageTo3D(disp_valid, Q) depth points_3d[:, :, 2] # 过滤掉无效和超范围的值 depth[(depth 0) | (depth 10000)] 0 return depth def get_distance(depth, x, y): # 取目标像素点周围小窗口的中值比单点稳 win depth[max(0, y-2):y3, max(0, x-2):x3] win win[win 0] return float(np.median(win)) if win.size else -1.0reprojectImageTo3D把每个像素的视差映射成三维点Z 就是相机坐标系下的深度单位跟标定时用的SQUARE_SIZE一致这里是毫米。直接取单点深度容易受噪声影响我习惯取 5x5 窗口的中值稳得多。depth 10000的过滤是经验值超出量程的基本是错误匹配。到这一步你点画面上任意一点就能打印出它离相机的距离了。5. 避坑与常见问题排查这一章是我踩过的坑合集新手照着排查能省大半天。现象一深度图整体偏大或偏小比例不对。原因标定用的棋盘格方格尺寸SQUARE_SIZE和实际打印尺寸不一致或者基线T没标准。 解决拿尺子量一下打印出来的方格实际边长改SQUARE_SIZE重标标定后打印np.linalg.norm(T)和物理测量的镜头间距对比误差超过 5% 就重采图像。现象二视差图大片黑色几乎没数据。原因numDisparities设太小近处物体视差超出搜索范围或者左右图没校正对齐。 解决先把numDisparities调到 128 甚至 256 试再存一张校正后的左右图肉眼看同一物体是否在同一水平线上不在就是校正环节出问题检查Q和映射表是否用了同一套标定参数。现象三深度值跳动厉害同一物体忽远忽近。原因SGBM 参数过于激进uniquenessRatio太小或speckleWindowSize太小噪声没滤掉。 解决uniquenessRatio提到 15speckleWindowSize提到 150blockSize加到 7同时用窗口中值代替单点取值。现象四两个摄像头取帧不同步运动物体测距错乱。原因用两个独立VideoCapture顺序读取中间有时间差。 解决常见做法是用一个支持双路输入的采集卡或者用多线程同时抓帧再配对静态场景影响不大动态场景必须处理同步。现象五换了个场景深度就全错。原因标定参数是在特定分辨率下得到的换了分辨率没重新标。 解决标定和运行必须用同一分辨率换分辨率就重标别想着缩放参数凑合。提示调试时把校正图、视差图、深度伪彩图都存下来哪一步不对一目了然比盯着最终结果猜快得多。6. 二次开发把测距接进你自己的项目跑通之后多数人下一步是想把它接进自己的系统比如让小车根据前方距离避障或者让机械臂定位目标。这里给几个改造方向和我自己的经验。第一个方向是换视差算法。SGBM 是 CPU 上的稳妥选择如果你有 GPU 且追求帧率可以换成 OpenCV 的 CUDA 版cuda::StereoSGBM接口类似把compute换成 GPU 版本即可帧率能翻几倍。第二个方向是加目标检测联动先用 YOLO 之类框出目标再把框中心像素喂给get_distance就得到“目标离我多远”这是最常见的组合玩法。第三个方向是输出结构化数据把距离通过串口或 ROS 话题发出去接下游控制。改造时有个参数表值得单独维护我一般放在 config 里参数作用调整建议numDisparities视差搜索范围按最近测距需求设16 的倍数blockSize匹配块大小弱纹理调大细节场景调小baseline基线长度物理固定标定得到别乱改depth_max有效深度上限按量程设滤错误匹配验证改造是否成功我有个笨但有效的办法在已知距离比如 1 米、2 米、3 米处放个目标看输出值偏差是否在可接受范围一般 5% 以内算好。别只看画面好看数值准才是硬道理。说个我自己的教训。早期我图省事标定完直接把参数写死在代码里后来换了个摄像头位置忘了重标测出来的距离全错排查了一下午才发现是基线变了。从那以后我每次动摄像头机械结构都强制走一遍标定流程并且把标定日期和基线值写进 yaml 注释里谁改了一目了然。双目测距这东西几何是死的参数是活的把标定当回事后面就顺了。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑