简介:本资源面向计算机视觉初学者与机器人、自动驾驶方向开发者,提供一套在Ubuntu环境下基于OpenCV与Qt实现双目测距系统的完整工程。内容围绕立体视觉核心流程展开,涵盖摄像头同步与对齐、SIFT/ORB特征提取与匹配、半全局立体匹配、基础矩阵与单应性矩阵求解、三角测量、深度图重建及目标距离计算等关键环节,并借助Qt搭建图像显示、参数调整与测距结果展示的交互界面。压缩包为rar格式,大小约80.48MB,包内文件类型以源码、工程配置及说明文档为主,便于直接编译运行与二次开发。目前已有743人学习下载,适合希望掌握双目视觉测距原理并落地实践的读者参考,可帮助快速理解从图像采集到三维深度估计的完整链路,同时为后续性能优化与光照噪声处理提供可扩展的代码基础。
1. 双目测距到底靠不靠谱:从两个摄像头到真实距离的那条链路
很多人第一次听说双目测距,脑子里浮现的是两个摄像头并排一放,距离就自动出来了。真上手才发现,标定、校正、匹配、重投影,每一步都能让结果飘得离谱。这份基于 OpenCV 的双目测距系统,解决的正是从「两张图」到「一个可信距离值」的完整链路问题。它适合做机器人避障、AGV 测距、体积测量预研的工程师,也适合想搞懂立体视觉底层原理的学生。核心逻辑不复杂:左右相机同时拍同一场景,利用视差和三角测量算出深度。但真正决定精度的,是标定参数准不准、极线校正到不到位、匹配算法选得对不对。下面按实际复现顺序拆开讲。
2. 标定与校正:把两个相机变成「一只眼睛」
2.1 为什么标定是双目系统的命门
单目相机标定求内参和畸变系数,双目还要多一层:两个相机之间的旋转和平移关系,也就是外参。没有这组外参,左右图像的极线不平行,匹配时就得在整幅图里二维搜索,既慢又容易错。标定质量直接决定后续测距的上限,标定不准,后面算法再花哨也是白搭。
常见做法是用棋盘格标定板,左右相机同时拍摄十几到二十组不同角度的图像。OpenCV 提供cv2.stereoCalibrate一次性求内参、畸变、旋转矩阵 R 和平移向量 T。这里有个血泪经验:标定板要覆盖画面各个区域,尤其是四角和边缘,否则畸变系数在边缘区域拟合很差,测距时画面边上的物体会明显偏。
标定完成后用cv2.stereoRectify计算校正映射,再用cv2.initUndistortRectifyMap生成映射表。之后每帧图像通过cv2.remap做校正,左右图的对应点就落在同一行上,匹配从二维降到一维。
import cv2 import numpy as np # 棋盘格内角点数量,按实际标定板修改 chessboard_size = (9, 6) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 准备左右相机各自的内参、畸变、旋转、平移 # 这里假设已通过单目标定拿到 mtx_l, dist_l, mtx_r, dist_r # 实际项目中这些值从标定脚本输出读取 image_size = (640, 480) ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( object_points, # 世界坐标系下的棋盘格角点,单位可以是毫米 img_points_l, # 左图角点 img_points_r, # 右图角点 mtx_l, dist_l, mtx_r, dist_r, image_size, flags=cv2.CALIB_FIX_INTRINSIC, # 若单目标定已足够准,固定内参 criteria=criteria ) # 立体校正,得到校正后的投影矩阵和重投影矩阵 Q R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, image_size, R, T, alpha=0 # alpha=0 裁剪掉无效区域,alpha=1 保留全部像素 ) # 生成映射表,后续每帧直接 remap map_lx, map_ly = cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, image_size, cv2.CV_32FC1) map_rx, map_ry = cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, image_size, cv2.CV_32FC1)这段代码的关键参数有三个。flags控制是否固定内参,如果单目标定已经做得很扎实,固定内参能减少双目优化的自由度,结果更稳。alpha决定校正后是否裁剪黑边,做测距建议用 0,避免无效像素干扰匹配。Q是重投影矩阵,后面把视差图转成三维点云或深度值就靠它。
2.2 校正效果的肉眼验证方法
校正完别急着跑测距,先做一步验证:在左右校正图上画水平线,看同一物体在左右图里是否落在同一行。如果明显错位,说明标定或校正有问题。我一般会写个小脚本,把左右图上下拼接,每隔 40 像素画一条横线,保存下来看一眼。这个习惯帮我省过很多次返工。
# 读取一帧左右原图并校正 img_l = cv2.imread('left.png') img_r = cv2.imread('right.png') rect_l = cv2.remap(img_l, map_lx, map_ly, cv2.INTER_LINEAR) rect_r = cv2.remap(img_r, map_rx, map_ry, cv2.INTER_LINEAR) # 拼接并画水平参考线 h, w = rect_l.shape[:2] canvas = np.hstack((rect_l, rect_r)) for y in range(0, h, 40): cv2.line(canvas, (0, y), (2 * w, y), (0, 255, 0), 1) cv2.imwrite('rectify_check.png', canvas)如果水平线穿过左右图的同一物体时基本对齐,说明校正合格。若偏差超过几个像素,回头检查标定图像是否覆盖足够角度,或者棋盘格尺寸是否填错。
3. 视差计算与深度映射:从像素差到毫米
3.1 StereoSGBM 参数怎么调才不飘
视差图的质量直接决定测距精度。OpenCV 里常用StereoSGBM,它比 BM 算法在弱纹理区域更稳,但参数多,调不好就是一片噪点。核心参数有这几个:minDisparity和numDisparities决定搜索范围,blockSize是匹配窗口大小,uniquenessRatio过滤误匹配,speckleWindowSize和speckleRange做连通域去噪。
numDisparities必须是 16 的整数倍,它决定了能测的最近距离。视差越大表示物体越近,如果物体离得近但numDisparities设小了,视差超出范围就会测不到。我一般先设 64 或 128,看视差图里最近物体的视差值再调整。
# 创建 SGBM 匹配器 stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=128, # 必须是 16 的倍数 blockSize=5, # 匹配窗口,奇数,3~11 之间调 P1=8 * 3 * 5 ** 2, # 控制视差平滑度,一般 8*通道数*blockSize^2 P2=32 * 3 * 5 ** 2, # 一般是 P1 的 4 倍 disp12MaxDiff=1, # 左右一致性检查最大差异 uniquenessRatio=10, # 视差唯一性百分比,10~15 较稳 speckleWindowSize=100, # 斑点滤波窗口,0 表示关闭 speckleRange=2, # 斑点滤波视差变化阈值 mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY ) # 计算视差,返回的是定点数,需除以 16 得到真实视差 disparity = stereo.compute(rect_l, rect_r).astype(np.float32) / 16.0blockSize越小,细节保留越好但噪点越多;越大越平滑但边缘会糊。做测距时如果目标是大块物体,可以适当加大到 7 或 9。uniquenessRatio设太低会引入大量误匹配,设太高又会让弱纹理区域大片空洞,10 到 15 是常见平衡点。
3.2 视差转距离的公式与代码落地
拿到视差图后,距离计算公式是Z = f * B / d,其中 f 是校正后相机的焦距(像素单位),B 是基线距离(左右相机光心距离,单位毫米),d 是视差。f 和 B 可以从校正后的投影矩阵 P1 和重投影矩阵 Q 里取。
# 从 Q 矩阵提取焦距和基线 # Q 是 4x4 矩阵,Q[2,3] 和 Q[3,2] 与基线和焦距相关 f = Q[2, 3] # 焦距,像素单位 B = 1.0 / Q[3, 2] # 基线,单位与标定一致 # 逐像素计算深度 h, w = disparity.shape depth = np.zeros_like(disparity) mask = disparity > 0 # 视差为 0 或负值的区域无效 depth[mask] = f * B / disparity[mask] # 取画面中心区域的中值距离,避免个别噪点干扰 center = depth[h//4:3*h//4, w//4:3*w//4] valid = center[center > 0] if len(valid) > 0: print(f"中心区域估计距离: {np.median(valid):.1f} mm")这里有个容易翻车的点:Q矩阵里的基线单位取决于标定时棋盘格尺寸的单位。如果标定时棋盘格方格边长填的是毫米,那 B 就是毫米,算出来的 Z 也是毫米。如果填的是米,结果就是米。单位不统一是新手最常见的错误之一。
提示:视差图里大片黑色区域表示匹配失败,通常出现在弱纹理、反光或遮挡区域。测距时不要直接对全图取平均,用中值或对特定区域取值更可靠。
4. 避坑与排查:双目测距最常见的五个翻车现场
4.1 测距值整体偏大或偏小
现象:所有距离都乘以了一个固定比例,比如实际 1 米测出来 1.2 米。原因:基线 B 或焦距 f 的单位不一致,或者标定板方格尺寸填错。解决:检查标定脚本里棋盘格尺寸的单位,确认和实际物理尺寸一致;再核对 Q 矩阵提取的 B 是否等于卷尺量出来的相机间距。
4.2 视差图大面积空洞
现象:视差图里目标区域几乎全黑,只有零星亮点。原因:numDisparities太小,物体视差超出搜索范围;或者uniquenessRatio太高,弱纹理区域被全部过滤。解决:先增大numDisparities到 256 试一次,如果空洞消失说明是范围问题;如果仍有空洞,把uniquenessRatio降到 5 到 10 之间,同时适当增大blockSize。
4.3 校正后左右图不对齐
现象:画了水平线后发现同一物体在左右图里高度差明显。原因:标定图像数量不足或角度覆盖不够,导致外参不准;也可能是stereoRectify的alpha参数设成了 1,保留了畸变严重的边缘区域。解决:重新采集 15 组以上标定图,覆盖画面四角和远近不同距离;把alpha改为 0 再验证。
4.4 近距离测不准,远距离反而还行
现象:1 米以内误差很大,2 米以上反而稳定。原因:近距离视差大,对匹配窗口和标定误差更敏感;blockSize太小导致近距离纹理重复区域误匹配。解决:适当增大blockSize到 7 或 9,并开启disp12MaxDiff做左右一致性检查,把误匹配点剔除。
4.5 运行速度慢,达不到实时
现象:SGBM 在 640x480 上跑一帧要几百毫秒。原因:numDisparities和blockSize过大,或者用了SGBM_3WAY模式。解决:如果场景对精度要求不高,换STEREO_BM算法;或者把图像缩放到 320x240 再计算视差,最后把距离值按比例还原。实测缩小一半分辨率,速度能提升三到四倍,精度损失在可接受范围内。
5. 进阶技巧:用重投影矩阵把视差变成三维点云
5.1 从深度图到点云的完整转换
如果只是测一个距离值,前面的深度图就够了。但很多场景需要知道物体的三维坐标,比如机械臂抓取、体积测量。OpenCV 提供了cv2.reprojectImageTo3D,直接利用 Q 矩阵把视差图转成三维点云,每个像素对应一个 (X, Y, Z) 坐标。
# 将视差图转为三维点云 points_3d = cv2.reprojectImageTo3D(disparity, Q) # 过滤无效点,只保留视差大于 0 的区域 mask = disparity > disparity.min() cloud = points_3d[mask] # 取画面中心区域的三维坐标中值,作为目标位置估计 h, w = disparity.shape center_region = points_3d[h//4:3*h//4, w//4:3*w//4] center_valid = center_region[center_region[:, :, 2] > 0] if len(center_valid) > 0: xyz = np.median(center_valid, axis=0) print(f"目标三维坐标: X={xyz[0]:.1f}, Y={xyz[1]:.1f}, Z={xyz[2]:.1f} mm")这段代码里points_3d的 shape 是 (h, w, 3),每个像素存 X、Y、Z。Z 就是深度,X 和 Y 是横向和纵向的物理坐标,单位与标定一致。用中值而不是均值,是为了避开个别飞点。如果要做抓取,通常还会对点云做直通滤波,把背景和前景分开。
5.2 精度验证的一个笨办法但很管用
调完参数后怎么知道测距准不准?我的习惯是拿一把卷尺,在相机前方不同距离放一个有明显纹理的物体,从 500 毫米到 3000 毫米,每隔 250 毫米测一次,记录系统输出值和卷尺读数。然后算相对误差,一般要求在 2% 以内算合格。如果某个距离段误差突然变大,回头检查该距离对应的视差值是否接近numDisparities的边界。
| 实际距离 (mm) | 测量距离 (mm) | 相对误差 |
|---|---|---|
| 500 | 512 | 2.4% |
| 1000 | 1018 | 1.8% |
| 1500 | 1523 | 1.5% |
| 2000 | 2035 | 1.8% |
| 2500 | 2560 | 2.4% |
| 3000 | 3082 | 2.7% |
这张表是我在某次模拟项目里实测的典型结果。可以看到近距离和远距离误差略大,中间段最稳。如果远距离误差持续偏大,通常是基线太短,可以考虑增大两个相机的间距,但基线变大后近距离盲区也会变大,需要根据实际场景权衡。
5.3 一个容易被忽略的细节:图像同步
双目测距假设左右相机在同一时刻曝光。如果用两个独立相机分别读取,运动物体会产生视差误差。常见做法是用硬件触发同步,或者选支持同步曝光的双目模组。如果只是静态场景测距,软件上依次读取影响不大。但做动态避障时,同步问题不解决,测出来的距离会跳得厉害。我一般会在代码里加时间戳检查,如果左右帧时间差超过 5 毫秒就丢弃该帧,宁可少测也不测错。
从那以后我每次搭双目系统,都先把标定验证和同步检查跑一遍,再动测距逻辑。希望帮到你。
本文还有配套的精品资源,点击获取