☰
OpenCV双目立体视觉测距:标定、校正、SGBM匹配与深度计算全流程
2026/10/7 5:35:35 网站建设 项目流程

简介:本资源为基于Python与双目立体视觉的图像匹配与测距毕业设计项目源码及配套资料,面向计算机视觉、人工智能方向的高校学生与开发者,可用于毕业设计、期末大作业或课程设计场景。项目围绕双目相机标定、立体匹配、视差计算与深度测距等核心环节展开,代码注释完整,新手也能理解整体流程。压缩包共164个文件,约96.22MB,包含38个py源码文件、10个ui界面文件、49个bmp与23个jpg图像样本、13个caj文献及pdf、docx说明文档等,覆盖程序主体、界面资源、测试图像与参考资料。已有70人学习下载。项目经严格调试,部署后即可运行,界面美观、功能完善,读者可据此掌握双目视觉测距的完整实现思路,并直接用于毕设答辩或课程设计提交。

1. 双目立体视觉测距:从毕业设计到能跑通的完整方案

双目立体视觉测距这件事,很多同学第一次接触是在毕业设计选题表上看到「基于双目立体视觉的图像匹配与测距」这几个字,觉得比纯图像分类有意思,又不像深度学习那样吃显卡,于是选了。真正动手才发现,两个摄像头拍出来的图怎么变成距离数字,中间隔着一整套标定、校正、匹配、三角测量的流程,任何一步参数没调好,测出来的深度图就是一片雪花。

这套方案要解决的问题很具体:用两个平行放置的摄像头,通过左右视图的视差计算出画面中每个像素点到相机的距离。它适合做机器人避障、AGV 测距、体积测量、手势交互这类需要「知道东西有多远」的场景。Python 生态里 OpenCV 把标定、校正、SGBM 匹配这些算子都封装好了,一台普通笔记本加两个 USB 摄像头就能跑通全流程,不需要昂贵的深度相机。下面按标定、校正、匹配、测距、避坑的顺序,把每一步的参数和代码讲清楚。

2. 相机标定与双目校正:把两个镜头变成一把尺子

2.1 为什么必须先标定,不标定会怎样

双目测距的数学基础是三角测量:同一个物点在左右两个相机成像平面上的横坐标差(视差 d)和距离 Z 成反比,公式是 Z = f × B / d,其中 f 是焦距(像素单位),B 是两个相机光心之间的距离(基线)。这个公式成立的前提是:两个相机的光轴严格平行、成像平面共面、且我们知道每个相机的内参(焦距、主点、畸变系数)。

现实中的两个 USB 摄像头不可能装得绝对平行,镜头还有径向畸变和切向畸变。如果不标定,直接用厂家给的标称焦距去算,误差可能到几十厘米甚至完全不可用。标定的目的就是求出每个相机的内参矩阵和畸变系数,以及两个相机之间的旋转和平移关系(外参),然后通过校正把左右图重投影到同一个理想平面上,让极线变成水平线——这样匹配时只需要在同一行上找对应点,搜索空间从二维降到一维。

常见做法是用棋盘格标定板,OpenCV 的calibrateCamera和stereoCalibrate两个函数就能完成。棋盘格建议用 9×6 或 11×8 的内角点,方格边长 25mm 到 30mm,打印后贴在硬板上保证平整。

2.2 采集标定图与单目标定

先分别对左右相机采集 15 到 25 张不同角度、不同距离的棋盘格图片。图片要覆盖画面各个区域,棋盘格要有倾斜、旋转,不能全是正对镜头的。采集时保持标定板静止、移动相机,或者固定相机移动标定板,两种方式都可以,但同一组标定图里不要混用。

import cv2 import numpy as np import glob # 棋盘格内角点数,注意是内角点不是方格数 CHESSBOARD = (9, 6) # 方格实际边长,单位毫米 SQUARE_SIZE = 25.0 # 生成棋盘格三维坐标:z=0平面上的网格点 objp = np.zeros((CHESSBOARD[0] * CHESSBOARD[1], 3), np.float32) objp[:, :2] = np.mgrid[0:CHESSBOARD[0], 0:CHESSBOARD[1]].T.reshape(-1, 2) objp *= SQUARE_SIZE def calibrate_single(image_paths): objpoints = [] # 三维点 imgpoints = [] # 二维图像点 gray_shape = None for fname in image_paths: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray_shape = gray.shape[::-1] # 查找棋盘格角点,带亚像素优化 ret, corners = cv2.findChessboardCorners(gray, CHESSBOARD, None) if ret: criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners2) # 单目标定,返回内参矩阵、畸变系数 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray_shape, None, None) return ret, mtx, dist, objpoints, imgpoints # 左右相机分别标定 retL, mtxL, distL, objpL, imgpL = calibrate_single(glob.glob('left/*.jpg')) retR, mtxR, distR, objpR, imgpR = calibrate_single(glob.glob('right/*.jpg')) print('左相机内参:\n', mtxL) print('左相机畸变:', distL.ravel())

这段代码的逻辑是:对每张图找棋盘格角点,把角点的三维世界坐标和二维图像坐标配对,最后用calibrateCamera解出内参和畸变。findChessboardCorners返回 False 的图要剔除,通常是棋盘格不完整或被遮挡。cornerSubPix做亚像素细化,能把角点精度提到 0.1 像素级别,这一步不能省。ret是重投影误差,正常应该在 0.5 像素以内,超过 1 像素说明有图质量太差,要重新采集。

2.3 双目标定与立体校正

单目标定拿到的是各自的内参,双目标定要在此基础上求两个相机之间的旋转矩阵 R 和平移向量 T,然后用stereoRectify计算校正变换,最后用initUndistortRectifyMap生成映射表,对每一帧做remap校正。

# 双目标定:要求左右标定图一一对应,角点检测结果数量一致 ret, mtxL, distL, mtxR, distR, R, T, E, F = cv2.stereoCalibrate( objpL, imgpL, imgpR, mtxL, distL, mtxR, distR, gray_shape, flags=cv2.CALIB_FIX_INTRINSIC) print('基线长度(mm):', np.linalg.norm(T)) # 立体校正:让左右图极线水平对齐 R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtxL, distL, mtxR, distR, gray_shape, R, T, alpha=0, flags=cv2.CALIB_ZERO_DISPARITY) # 生成校正映射表,后续每帧直接remap mapLx, mapLy = cv2.initUndistortRectifyMap( mtxL, distL, R1, P1, gray_shape, cv2.CV_32FC1) mapRx, mapRy = cv2.initUndistortRectifyMap( mtxR, distR, R2, P2, gray_shape, cv2.CV_32FC1) def rectify(imgL, imgR): left = cv2.remap(imgL, mapLx, mapLy, cv2.INTER_LINEAR) right = cv2.remap(imgR, mapRx, mapRy, cv2.INTER_LINEAR) return left, right

stereoCalibrate里CALIB_FIX_INTRINSIC表示固定单目标定得到的内参,只优化外参,这样更稳定。alpha=0表示校正后只保留有效像素区域,画面会裁掉黑边;如果希望保留更多视野可以设alpha=1,但会引入黑边。Q是视差到三维坐标的重投影矩阵,后面测距要用。校正效果可以用cv2.drawChessboardCorners或者直接画水平线检查:校正后同一物点在左右图的纵坐标应该基本一致,偏差超过 1 像素说明校正没做好。

提示:标定板要平整,打印后贴板子时避免气泡。采集时让棋盘格在画面中占比 1/3 到 1/2,太小角点检测不准,太大容易出画面。

3. 立体匹配:SGBM 参数怎么调才能出干净视差图

3.1 视差图是怎么算出来的

校正之后,左右图的极线已经水平对齐,匹配就变成:对左图每个像素,在右图同一行上找最相似的像素,横坐标差就是视差。OpenCV 提供了 BM(块匹配)和 SGBM(半全局块匹配)两种。BM 速度快但视差图噪声大,SGBM 慢一些但边缘和弱纹理区域效果好得多,毕业设计里一般用 SGBM。

SGBM 的核心思路是:先做块匹配算代价,再沿多个方向做动态规划累积代价,最后用唯一性检查和左右一致性剔除错误匹配。参数多,但真正影响结果的就那么几个。

3.2 SGBM 关键参数与可抄作业的配置

# 创建SGBM匹配器 window_size = 5 min_disp = 0 # numDisparities必须是16的整数倍,决定最大可测视差 num_disp = 16 * 6 stereo = cv2.StereoSGBM_create( minDisparity=min_disp, numDisparities=num_disp, blockSize=window_size, P1=8 * 3 * window_size ** 2, # 视差平滑惩罚项1 P2=32 * 3 * window_size ** 2, # 视差平滑惩罚项2 disp12MaxDiff=1, # 左右一致性检查最大差异 uniquenessRatio=10, # 唯一性检查阈值 speckleWindowSize=100, # 散斑过滤窗口 speckleRange=32, # 散斑过滤视差范围 preFilterCap=63, # 预处理滤波截断值 mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY ) def compute_disparity(left, right): # 转灰度 grayL = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY) grayR = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY) disp = stereo.compute(grayL, grayR).astype(np.float32) / 16.0 return disp

参数逐个说清楚。numDisparities决定能测多近:视差越大距离越近,如果物体离相机很近但视差图右侧一片黑,说明这个值设小了,要加大。blockSize是匹配块大小,奇数,3 到 11 之间。块越大越平滑但边缘越糊,块越小细节多但噪声大,一般从 5 开始试。P1和P2是平滑惩罚,控制视差图的连续程度,P2通常是P1的 4 倍左右,值越大视差图越平滑。uniquenessRatio是唯一性检查,值越大越严格,能去掉误匹配但也会让视差图变稀疏,5 到 15 之间比较合适。disp12MaxDiff做左右一致性检查,设 1 到 2 能有效剔除遮挡区域的错误视差。speckleWindowSize和speckleRange用来过滤小面积噪点,如果视差图里有很多孤立小斑点,把speckleWindowSize加大到 150 或 200。

3.3 从视差图到深度图

拿到视差后,用校正阶段得到的Q矩阵做reprojectImageTo3D,就能得到每个像素的三维坐标,Z 分量就是距离。

# 视差转三维点云 points_3d = cv2.reprojectImageTo3D(disp, Q) # 提取深度Z depth = points_3d[:, :, 2] # 或者直接用公式算:Z = f * B / d f = P1[0, 0] # 校正后左相机焦距 B = np.linalg.norm(T) # 基线 # 避免除零 valid = disp > min_disp + 0.5 depth_formula = np.zeros_like(disp) depth_formula[valid] = f * B / disp[valid]

reprojectImageTo3D内部就是用Q矩阵做变换,结果和公式一致。Q矩阵里包含了焦距、主点和基线信息,比手算更省事。注意视差为 0 或负值的像素要剔除,这些是匹配失败的区域,算出来的深度是无穷大或负数。实际使用时通常把深度图做归一化再用伪彩色显示,方便观察。

注意:SGBM 输出的视差是 16 倍定点数,compute之后要除以 16 才是真实视差。忘了除会导致距离算出来差 16 倍,这是新手最常见的翻车点。

4. 测距精度验证与常见问题排查

4.1 用已知距离验证测距结果

标定和匹配都跑通后,必须做精度验证。方法很简单:把标定板或一个特征明显的物体放在卷尺量好的距离上,比如 500mm、1000mm、1500mm,看程序算出来的深度值偏差多少。

def measure_at(disp_map, x, y, f, B): """取视差图某点的视差,计算距离""" d = disp_map[y, x] if d <= 0: return None return f * B / d # 在视差图上选取物体中心点,读取距离 # 建议取物体区域的中位数,比单点更稳 def region_distance(disp_map, x1, y1, x2, y2, f, B): region = disp_map[y1:y2, x1:x2] valid = region[region > 0] if len(valid) == 0: return None d = np.median(valid) return f * B / d

取单点容易受噪声影响,取一个区域的中位数更稳。验证时记录每个距离点的误差,正常在 1 米内误差能控制在 1% 到 3%,距离越远误差越大,因为视差精度随距离下降。如果误差超过 5%,先检查标定重投影误差,再检查基线B是否和实际测量一致。

4.2 避坑与常见问题排查

现象一:视差图大面积黑色,只有零星亮点。原因通常是numDisparities设得太小,或者左右图没有校正对齐。先确认rectify之后左右图同一物点在同一水平线上,用画线工具检查。如果校正没问题,把numDisparities加大到 16×8 或 16×10 再试。

现象二:测距结果整体偏大或偏小,但线性度还行。多半是基线B或焦距f的单位不一致。T的单位跟标定板方格边长单位一致,如果用毫米,B就是毫米,f是像素,算出来Z也是毫米。如果标定时方格边长填了 25 但实际是 25mm,没问题;如果填了 0.025(米),那B就是米,结果也是米。单位混用是血泪经验里排第一的坑。

现象三:视差图边缘有大量错误匹配,物体轮廓处视差跳变。这是 SGBM 在遮挡区域的固有缺陷,左右相机看到的角度不同,物体边缘一侧在另一个相机里被挡住。解决办法是加左右一致性检查,把disp12MaxDiff设为 1,或者后处理时用cv2.filterSpeckles过滤。要求高的话可以上 WLS 滤波或者深度学习匹配网络,但毕业设计用 SGBM 加后处理足够了。

现象四:标定重投影误差很大,超过 1 像素。检查标定图里棋盘格是否完整、是否模糊、是否有反光。另外findChessboardCorners对棋盘格尺寸很敏感,如果实际是 9×6 内角点但传了 8×5,角点检测会失败或错位。打印棋盘格时用实际尺寸,不要缩放。

现象五:实时运行时帧率很低。SGBM 在 640×480 分辨率下大概 10 到 20 帧,1080p 会掉到个位数。优化方向:降分辨率、把mode改成STEREO_SGBM_MODE_HH(精度略低但快)、或者只在 ROI 区域做匹配。如果只是测距不需要全图视差,可以只对目标区域计算。

5. 把测距做成能用的工具:几个实战技巧

5.1 用 ROI 和降采样换实时性

全图 SGBM 在嵌入式设备上跑不动,实际项目里我一般先做目标检测或者手动框选 ROI,只对 ROI 区域做匹配。这样计算量能降一个数量级。另一个技巧是把图像缩放到一半再做匹配,视差也减半,最后距离公式里焦距和基线按缩放比例修正即可。

def fast_disparity(left, right, scale=0.5): h, w = left.shape[:2] smallL = cv2.resize(left, (int(w*scale), int(h*scale))) smallR = cv2.resize(right, (int(w*scale), int(h*scale))) disp = stereo.compute( cv2.cvtColor(smallL, cv2.COLOR_BGR2GRAY), cv2.cvtColor(smallR, cv2.COLOR_BGR2GRAY) ).astype(np.float32) / 16.0 # 视差放大回原尺寸 disp = cv2.resize(disp, (w, h)) / scale return disp

缩放后视差要除以scale才是原图视差,因为视差和图像尺寸成正比。这个操作会损失一些精度,但换来的帧率提升很明显,适合对精度要求不苛刻的避障场景。

5.2 深度图后处理:中值滤波与空洞填充

SGBM 出来的视差图有空洞和噪点,直接算距离会跳变。我习惯先做一次中值滤波,再用左右一致性掩膜把无效区域标出来,最后用邻域有效值填充。

def postprocess(disp): # 中值滤波去噪 disp = cv2.medianBlur(disp, 5) # 无效区域掩膜 mask = disp <= 0 # 用 inpaint 填充空洞 disp_filled = cv2.inpaint( disp.astype(np.float32), mask.astype(np.uint8), 3, cv2.INPAINT_NS) return disp_filled

inpaint原本是给图像修复用的,这里拿来填视差空洞效果不错。INPAINT_NS是 Navier-Stokes 方法,比INPAINT_TELEA在平滑区域更自然。填充后深度图会连续很多,但要注意填充区域的距离是估计值,不能当精确测量用。

5.3 参数速查与调参顺序

调 SGBM 不要一次改所有参数,按下面顺序来:先定numDisparities保证最近距离能测到,再调blockSize平衡噪声和边缘,然后调uniquenessRatio和disp12MaxDiff去误匹配,最后用speckleWindowSize清噪点。每次只改一个参数,观察视差图变化。

参数作用常用范围调大效果
numDisparities最大视差16的倍数,64-160能测更近,计算变慢
blockSize匹配块大小3-11奇数更平滑,边缘变糊
uniquenessRatio唯一性检查5-15误匹配减少,视差变稀疏
disp12MaxDiff左右一致性1-2遮挡区错误减少
speckleWindowSize散斑过滤50-200小噪点减少
P1/P2平滑惩罚P2≈4×P1视差图更连续

这套参数在 640×480、基线 60mm、焦距约 600 像素的配置下,1 米内测距误差能控制在 2% 以内。换相机或换分辨率后,numDisparities和blockSize要重新试,其他参数可以沿用。

5.4 一个容易忽略的细节:温度与机械稳定性

双目相机装好后,基线B会随温度变化和机械应力漂移。我吃过一次亏:标定完放了一晚上,第二天测距整体偏了 3 厘米,查了半天发现是固定支架的塑料件受热变形,基线变了。后来换成铝型材支架,并且每次开机先拍一张标定板做快速校验,偏差大就重新标定。如果做产品,基线要选热膨胀系数小的材料,或者加温度补偿。

毕业设计里如果只是演示,标定一次能用很久;但如果要连续跑几个小时,建议每隔一段时间用已知距离的参照物校验一下。这个习惯帮我省了很多后悔药。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询