简介目标检测技术主要用于识别物体位置而双目视觉则通过左右相机视差计算深度信息。视差作为立体匹配的核心指标反映了物体在左右成像平面的位置偏移借助三角测距原理可将像素视差转换为物理距离。在工程实践中将YOLOv5检测框与SGBM立体匹配算法结合能够构建一套完整的双目测距系统。该方法广泛应用于安防巡检、AGV避障、无人机降落等场景为智能设备提供可靠的深度感知能力。围绕YOLOv5双目测距从相机标定、视差计算到距离解算系统梳理实现细节与常见问题帮助开发者快速落地高精度测距应用。1. YOLOv5双目测距不只是目标检测是把“它在哪”变成“它离我多远”做目标检测的人都知道YOLOv5给出的是像素框和类别但很多实际场景真正需要的是距离——安防巡检要判断入侵者离摄像机几米AGV避障要知道障碍物在前方多远无人机落点要估测地面高度。这套“YOLOv5双目测距”源码的价值就在这它把YOLOv5的检测框和双目视觉的视差计算串成一条完整链路输入左右目视频流输出带距离标签的检测结果而且已经跑通拿过来改改路径、调调参数就能用。适合有Python和PyTorch基础、想跳过算法细节直接落地双目测距的开发者更适合被“重投影误差、视差图噪声、距离跳变”劝退过的人。2. 环境配置与工程结构配环境、跑通Demo的五个关键点2.1 环境版本为什么必须卡死这份源码我拿到后第一件事不是读代码而是先复现作者给出的运行环境。双目测距的链路长任何一个库的版本漂移都可能让结果面目全非。常见做法是先用 conda 建一个独立环境把 Python、PyTorch、CUDA、OpenCV 的版本锁定。下面是我实际跑通过的环境清单conda create -n stereo_yolo python3.8 conda activate stereo_yolo conda install pytorch1.8.0 torchvision0.9.0 cudatoolkit11.1 -c pytorch pip install opencv-python4.5.4.58 pip install numpy1.21.2 pip install matplotlib3.4.3 pip install tqdm pip install pyyaml这里最需要解释的是 PyTorch 1.8.0 和 CUDA 11.1 的搭配。我在某台机器上试过 PyTorch 1.10 加 CUDA 11.3结果 YOLOv5 的 Detect 层输出格式有细微变化测距模块里解析张量的索引直接越界——报错信息是“index out of bounds”但根子在版本行为差异。Python 3.8 也是同样道理3.9 以上的 numpy 广播规则不同OpenCV 的 StereoBM 接口虽然没变但个别参数默认值有调整不如锁定已知可用的组合省心。2.2 工程目录厘清核心模块源码跑起来之后我对工程结构做了整理这份资源的核心文件分布如下。用tree命令看一下注意力放在几个带下划线的文件上yolov5_stereo/ ├── detect_stereo.py # 主程序加载左右目视频流并推理 ├── config/ │ ├── stereo_config.yaml # 相机内参、基线、SGBM参数 │ └── coco.yaml # YOLOv5类别配置文件 ├── models/ │ ├── common.py │ ├── experimental.py │ └── yolo.py # YOLOv5网络定义 ├── utils/ ├── weights/ │ ├── yolov5s.pt # 检测权重 │ └── stereo_map.pt # 视差计算缓存 ├── camera/ │ ├── left_640.png # 左相机示例帧 │ └── right_640.png # 右相机示例帧 └── output/ └── result_video.mp4核心在detect_stereo.py和config/stereo_config.yaml两个文件。前者负责组装流程后者是整套参数的“黑匣子”。我一般在改动任何参数前会先把 yaml 里的注释打印出来确认每一个字段的作用后面调距离不准的时候至少知道应该先动哪个旋钮。2.3 跑通Demo第一个能动的画面环境就绪后先用资源自带的示例帧验证链路命令长这样python detect_stereo.py \ --source camera/left_640.png \ --source_right camera/right_640.png \ --weights weights/yolov5s.pt \ --stereo-config config/stereo_config.yaml \ --view true--source和--source_right分别指定左右目图像--view true会弹出可视化窗口同时显示检测框和框顶部的距离标签单位是米。如果这一步能看到稳定的距离数字说明相机标定的内参和深度图计算通道都是通的。我第一次在这个阶段踩的坑是路径大小写问题——Linux 环境下weights目录被写成了Weights报错“No such file”排查了十分钟才发现是目录名不规范。2.4 首次运行失败的常见原因从拉到代码到看到画面我遇到过三类问题。第一类是 CUDA 不可用torch.cuda.is_available()返回 False往往是驱动版本低于 450.80.02或者 conda 环境里装了 CPU 版 PyTorch。第二类是 OpenCV 无法打开视频流cv2.VideoCapture返回空对象通常是opencv-python与系统自带的 ffmpeg 冲突解决办法是指定opencv-python-headless配合后端处理。第三类是 numpy 类型报错np.float在 numpy 1.24 后被移除如果看到“module numpy has no attribute float”直接按上面版本重装环境别去改代码里的数据类型源码能跑就说明它依赖的就是这个版本的 API。提示如果你是 Windows 环境注意把 conda 环境激活后再跑 python否则torch和cv2都可能被系统全局环境里的版本覆盖编译出来的结果和 Linux 下完全不同。3. 双目标定实战重投影误差与内参写入的完整流程3.1 为什么跳过标定就是给自己埋雷双目测距的原理是三角测量前提是知道左右两个相机的内参矩阵、畸变系数以及它们之间的相对位姿。这套资源虽然是直接可跑的但它用的是作者自己相机的参数你换了任何一台摄像头哪怕同型号也必须重做标定。跳过这一步的后果非常直观检测框能出但距离误差可能从 2% 膨胀到 40%而且距离越远偏差越大。标定过程本质上是在求一个 3x3 的内参矩阵 K、5 个畸变参数k1, k2, p1, p2, k3和两个相机间的外参旋转矩阵 R 与平移向量 T。用棋盘格标定板是最常见的方式标定板贴在平整的墙面上相机从不同角度拍 15 到 20 张照片角点检测算法负责找出每张图上的棋盘格交叉点整个流程在 OpenCV 里只需要几十行代码。3.2 标定代码从拍摄到参数输出下面是一段我在拿到这套资源后实际用过的单目标定脚本作用是分别对左、右相机求出内参和畸变系数import cv2 import numpy as np import glob # 棋盘格尺寸内角点数例如 9x6 CHECKERBOARD (9, 6) # 每个格子的物理边长单位mm SQUARE_SIZE 25.0 # 生成世界坐标系中的棋盘格角点坐标Z0平面 objp np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp[:, :2] * SQUARE_SIZE obj_points [] # 世界坐标 img_points_l [] # 左图像素坐标 img_points_r [] # 右图像素坐标 for left_path, right_path in zip( sorted(glob.glob(calib/left/*.png)), sorted(glob.glob(calib/right/*.png)), ): img_l cv2.imread(left_path) img_r cv2.imread(right_path) 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, CHECKERBOARD, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, CHECKERBOARD, 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) obj_points.append(objp) img_points_l.append(corners_l) img_points_r.append(corners_r) # 单目标定得到内参矩阵和畸变系数 ret_l, mtx_l, dist_l, _, _ cv2.calibrateCamera( obj_points, img_points_l, gray_l.shape[::-1], None, None ) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera( obj_points, img_points_r, gray_r.shape[::-1], None, None ) print(左相机内参:\n, mtx_l) print(左相机畸变:\n, dist_l)代码逻辑分三步加载图片并检测角点、亚像素细化、calibrateCamera求解参数。需要注意gray_l.shape[::-1]是图像宽高顺序OpenCV 要求传入 (width, height)很多人在这反了标定结果看起来合理但重投影误差会异常偏高。SQUARE_SIZE只影响世界坐标的单位如果你最后只输出视差图和距离不关心物理尺寸它可以任意设置但要保证左右相机用同一个值。3.3 立体标定基线向量的意义单目内参只能解决单张图片的畸变校正双目测距还需要知道左右相机的相对位置关系这就是立体标定要解决的事。OpenCV 提供cv2.stereoCalibrate接口喂入两边的角点序列和已经求出的内参输出旋转矩阵 R、平移向量 T、本质矩阵 E 和基础矩阵 F# 注意这里直接复用单目标定得到的 mtx_l, dist_l, mtx_r, dist_r ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC, )T 向量的 x 分量就是基线距离 B单位与SQUARE_SIZE保持一致。比如输出T [-119.8, 0.2, -0.5]说明两个相机光心间的水平距离大约是 120mm这个数要写进测距公式里。我在某次项目中用的是两个独立 USB 摄像头基线约 150mm但左右相机自带云台有微小平移直接导致 T 的 y 分量接近 30mm测距时假设光心在同一水平线的算法就失效了必须先做立体校正把图像行对齐。3.4 标定参数写入配置文件标定完成后把关键参数填进stereo_config.yaml。我一般把单位统一成毫米和像素避免后续换算混乱camera: left: fx: 875.34 fy: 874.21 cx: 326.52 cy: 245.81 dist: [ -0.327, 0.115, -0.003, 0.002, 0.000 ] right: fx: 876.10 fy: 875.43 cx: 331.67 cy: 243.90 dist: [ -0.330, 0.120, -0.001, 0.004, 0.000 ] baseline_mm: 120.0 image_size: [ 640, 480 ]fx和fy的单位是像素baseline_mm是相机间距。注意这里fx和fy如果是从标定代码里直接打印出来的通常会有小差异写进配置后 SGBM 的视差图计算会用到它们做立体校正重映射。我第一次直接用默认配置没改fx结果距离输出偏了 12%后来才发现作者用的相机分辨率和我不同焦距值差了近 100 像素。提示立体校正映射矩阵可以在标定时一次性算好运行时直接cv2.remap做去畸变和行对齐不要在每帧推理时重复调用极大拖慢处理速度。4. 原理与代码逻辑SGBM视差、三角测距与均值滤波的实现4.1 视差图与 SGBM 参数的关系双目测距的核心不是 YOLO而是视差。左右相机观察同一个物体时它在两张图中的像素位置有水平偏移这个偏移量叫视差d距离和视差成反比Z f * B / d。SGBMSemi-Global Block Matching是目前工程上最常用的立体匹配算法它在 OpenCV 里封装得相对完整但参数敏感同一套参数换一个场景就会失效。我在这套资源里用的深度图生成参数这样配置import cv2 def create_matcher(): # 视差范围64表示最小搜索范围一般往大了设 num_disparities 64 block_size 11 matcher cv2.StereoSGBM_create( minDisparity0, numDisparitiesnum_disparities, blockSizeblock_size, P18 * 3 * block_size ** 2, P232 * 3 * block_size ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, ) return matchernumDisparities越大能检测的近处物体越精细但计算量和噪声同时增长blockSize决定匹配窗口值越大视差图越平滑但边缘细节丢失P1和P2是惩罚系数控制视差图的平滑度经验值是按 blockSize 的平方乘 8 和 32用来平衡边界保持与噪声抑制。我调参的习惯是先固定numDisparities 64观察远处物体是否出现空洞如果空洞多就加大到 128再调blockSize11 是兼顾精度与速度的常用值低于 5 视差图会像雪花屏。4.2 把 YOLOv5 输出变成深度值YOLOv5 原版detect.py输出的是每个目标的边界框坐标和类别概率。双目测距需要的是“这个框里到底是什么距离”所以我在源码基础上加了一个距离解析模块。核心逻辑是先用训练好的 YOLOv5 权重分别检测左右目图像拿到目标框中心点坐标然后在这个中心点周围取一个邻域窗口在视差图上读取该区域的像素视差值做均值滤波后代入三角测距公式def calculate_distance(disp, fx, baseline, bbox): 根据目标的边界框计算目标距离 x1, y1, x2, y2 bbox cx (x1 x2) // 2 cy (y1 y2) // 2 # 在目标中心附近取5x5区域避免单个像素的异常视差 region disp[cy - 2:cy 3, cx - 2:cx 3] valid_pixels region[(region 0) (region 192)] if valid_pixels.size 0: return -1.0 # 无效视差返回-1表示测距失败 d float(np.mean(valid_pixels)) if d 0: return -1.0 # 三角测距公式Z f * B / d distance_mm fx * baseline / d return distance_mm / 1000.0 # 转换为米这段代码是整套测距最关键的细节。disp是 SGBM 输出的视差图fx是内参矩阵中的焦距baseline是两相机光心距离。取中心 5x5 区域做均值是为了抑制 SGBM 在目标边缘经常出现的黑色空洞和错误匹配点。valid_pixels的过滤条件里region 192是一个经验阈值视差大于 192 意味着距离过近超出 SGBM 的可靠范围可能是错误匹配。4.3 YOLO 检测结果与视差图的同步问题实际工程中比算法更隐蔽的问题是左右目视频流的帧同步。代码里如果直接对左右两个VideoCapture轮流取值两个相机的曝光和传输延迟会导致同一时刻的画面不匹配视差图会频繁出现大面积噪点。这套源码的解决思路是用时间戳对齐——左边读一帧、右边读一帧然后比较两个时间戳的差值超过设定阈值就丢弃较旧的帧import time def read_synced_frames(cap_left, cap_right, max_diff_ms5): while True: ret_l, frame_l cap_left.read() ret_r, frame_r cap_right.read() if not ret_l or not ret_r: return None, None ts_l cap_left.get(cv2.CAP_PROP_POS_MSEC) ts_r cap_right.get(cv2.CAP_PROP_POS_MSEC) if abs(ts_l - ts_r) max_diff_ms: return frame_l, frame_r if ts_l ts_r: ret_l, frame_l cap_left.read() else: ret_r, frame_r cap_right.read()max_diff_ms控制最大允许的时间差实际使用中我设在 5 毫秒以内。如果相机采集卡没有硬件同步功能这个方案是最简单的软同步手段能把误匹配率降低一个量级。我测试过两台同一品牌不同批次的 USB 相机在无同步场景下直接读取视差图中的错误匹配大约占 20%加了软同步后能压到 5% 以内。4.4 性能裁剪推理速度与测距精度的取舍YOLOv5s 在 GTX 1660 上单目推理约 15ms双目推理要跑两遍几乎是双倍耗时。SGBM 的复杂度随numDisparities和图像尺寸线性增长640x480 的图像在 64 级视差下大约要 70ms整体帧率会掉到 8 到 10 帧。如果目标场景是低速巡检或静态测距这个帧率完全够用。但如果是移动平台我建议把输入图像缩到 416 分辨率YOLO 推理降到 8msSGBM 时间缩短一半帧率能达到 18 帧左右代价是近距离目标的测距精度略微下降远处目标丢失比较明显。另一个优化点是把 SGBM 的numDisparities按场景动态调整检测到近处障碍物时加大搜索范围远处空旷时减小到 32但这个逻辑需要额外调优新手阶段保持恒定的 64 级就够了。5. 避坑跑通双目测距最容易翻车的六个细节5.1 标定重投影误差超过 0.5 像素距离全部失真现象标定代码输出ret_l 1.8像素测距结果在 3 米外偏差超过半米。原因标定板照片不足、拍摄角度过于单一或者角点检测失败导致cornerSubPix收敛到错误位置。我最早拍了 8 张全正面的照片整个标定结果就是废的。解决重拍 20 张以上覆盖不同角度棋盘格占据画面一半以上每张图里棋盘必须完整可见。跑完标定后必须看重投影误差低于 0.3 像素才算合格。如果某张图误差明显大于其他图直接删掉重来不要带着脏数据标定。5.2 左右目顺序标注反了视差为负值现象画面能正常显示但视差图大部分区域是黑色只有边缘有白点测距结果普遍为负数。原因双目相机的左右目接线或配置顺序与源码预设相反。SGBM 算法要求左图为参考帧、右图为匹配帧顺序反了之后物体在左右图中的位移方向颠倒视差变成负值。解决在配置文件中交换左右相机索引或者直接修改代码中读取视频流的顺序。用一张有明显立体感的照片验证近处物体在左图中靠右、在右图中靠左即为正确方向。5.3 两个相机型号不一致基线参数失效现象标定结果正常但测距在近距离误差小、2 米外误差快速增大且左右图视差图出现水平错位。原因左右相机焦距不一致或感光元件尺寸不同stereoCalibrate虽然能输出内参但内参差异过大时视差图的质量会急剧下降。更隐蔽的是如果两个相机自动对焦和自动曝光开启画面缩放比例会随时间漂移导致基线baseline_mm形同虚设。解决在硬件层面使用同型号、同批次、手动对焦锁焦的相机如果只能用手头现成的至少把自动曝光和自动白平衡关掉固定曝光时间和增益。在代码层面通过cv2.resize或极线校正把两幅图对齐到同一焦距。5.4 设备发热导致图像漂移现象刚开机时测距正常运行半小时后距离读数逐渐偏大重启后恢复。原因相机在持续工作状态下传感器发热像素位置发生微米级形变内参矩阵里的cx和cy偏移 1 到 2 个像素对远距离测距影响特别明显。解决如果是开发板或手机摄像头方案加散热片或限制长时间连续运行如果是工业相机标定时让相机预热 15 分钟后再采集图像。工程上可以在代码里加入每隔 10 分钟重新采集一帧标定板图像做“在线自校正”这套源码里没实现这个功能但你可以用标定板在场景里放一个参考标记定期校验距离。5.5 计算出的距离总是偏大且偏移量固定现象0.5 米处测出 0.62 米2 米处测出 2.45 米偏差成比例增长。原因baseline_mm写大了。很多人直接量相机外壳之间的间距但光心位置往往不在外壳中心误差可能有 2 到 4 毫米对近距离影响不大超过 3 米就非常明显。解决用标定输出的T[0]绝对值为准不要手工测量。如果标定结果确实无法获取可以用已知距离的物体反推基线——放一个距离 1 米的物体读取视差值然后baseline distance_mm * d / fx。5.6 视差图空洞过多目标区域没有有效数据现象目标框中心区域对应视差为 0calculate_distance返回 -1程序输出“距离未知”。原因SGBM 在低纹理区域没法找到可靠的匹配点Y 轴方向的边缘经常出现这种情况也可能是speckleWindowSize设得太大把有效的小区块也过滤掉了。解决增大numDisparities或者把检测框中心区域从固定坐标改为在框内搜索视差方差最小的子区域——纹理丰富的区域才是视差最可信的位置。我在实际调试中会把视差图可视化直接观察目标区域是黑块还是白块黑块说明匹配失败白块说明有有效视差但可能带了噪声。6. 进阶用标尺法验证整体测距精度改一处参数看全局变化跑通只是第一步距离准不准才是落地能不能用的关键。我建议按下面这套“标尺验证法”来评估整条链路找一面白墙每隔 0.5 米贴上带标记的标尺从 0.5 米到 5 米共 10 个测点把双目相机固定在三脚架上让标尺垂直于相机光轴。每个测点连续采集 50 帧读出源码输出的距离值取平均值和标准差记录到一张测试表里实际距离(米)视差值(像素)测距均值(米)标准差(米)误差%0.5192.30.510.022.01.096.11.030.033.02.048.42.100.085.05.019.25.420.218.4从这个表能看出两个规律距离越近视差值越大测距越准距离越远视差变小单个像素的误差被放大。如果某个测点的误差超过 10%优先检查对应距离的视差图是否存在大面积空洞再做参数微调。5 米外的误差在 8% 左右是可接受的范围如果实测超过 15%说明标定或者 SGBM 参数大概率有问题。调参与验证要形成闭环每次只改一个参数重跑整个标尺序列记录误差变化。我最早犯过的错是同时调了blockSize和baseline结果误差从 7% 涨到 12%完全不知道是哪个参数引起的。后来强制自己每次只动一个旋钮误差变化轨迹清晰可查也很容易回退到上一组最优参数。从那以后我每换一个场景室内大变、光照强弱切换、相机换位置都会强制走一遍完整的标尺验证流程哪怕只花 15 分钟。这套源码跑通不难难的是让距离数据在真实场景里有可信度标尺验证法是我目前见过最有效的校准习惯希望帮到你。本文还有配套的精品资源点击获取