
简介面向计算机视觉学习者与自动驾驶、机器人导航、无人机避障等场景开发者这份资源聚焦YOLOV5与双目相机结合的三维测距实现属于进阶实战型项目代码包。压缩包共141个文件约40.38MB以37个py源码、40个yaml配置、22个pyc编译文件为主辅以yml环境配置、sh脚本、md说明文档及少量xml、jpg样例与pt权重、mp4演示覆盖数据预处理、模型训练、目标检测与深度计算等环节。已有2420人学习下载。读者可从中获得整合YOLOV5与双目相机算法的完整源码、预训练权重与配置文件借助样例双目图像对快速跑通立体匹配与深度估计流程并通过文档理解基线距离、特征匹配与三维定位的实现细节为复杂环境下的实时测距与感知系统开发提供可复用的工程参考。1. 双目测距这件事为什么值得自己跑一遍去年帮一个做分拣设备的团队看现场问题产线上机械臂抓取总是偏几厘米视觉那边给的是单目加标定板深度信息全靠假设平面。后来换成双目相机配 YOLOV5检测框中心直接反投影出三维坐标抓取精度立刻稳住了。这套组合的价值就在这儿YOLOV5 负责告诉你“这是什么、在图像哪个位置”双目负责告诉你“它离我多远”两者拼起来才是可用的三维测距。这份资源适合已经能跑通 YOLOV5 推理、想把手里的单目检测升级成带深度输出的开发者也适合做机器人抓取、AGV 避障、体积测量这类需要真实距离的场景。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆代码和参数都会落到具体位置。2. 项目定位与技术选型为什么是 YOLOV5 加双目而不是别的组合2.1 这套组合解决的是什么问题单目相机拿到的是二维像素坐标想得到距离必须依赖已知物体尺寸或者地面平面假设一旦物体尺寸变化或者姿态倾斜误差就失控。双目相机通过左右两个视角的视差直接算出深度不依赖先验尺寸这是几何上的硬解。YOLOV5 在这里的角色是提供稳定的目标框框的中心点作为测距的锚点避免整幅图逐像素匹配带来的噪声。两者结合后的输出是一条结构化数据类别、置信度、像素坐标 (u, v)、相机坐标系下的三维坐标 (X, Y, Z)。这条数据往下可以喂给机械臂做抓取位姿解算也可以喂给避障模块做距离阈值判断。选 YOLOV5 而不是更新的检测器原因很实际它的工程化程度高导出 ONNX、TensorRT 的路径成熟推理速度在边缘设备上可控而且后处理逻辑简单框中心点提取不需要额外解析。双目部分用的是标准立体校正加 SGBM 或者 BM 匹配OpenCV 原生支持不需要引入额外的深度学习深度估计模型。整套链路没有黑匣子每一环都能单独调试这对现场排查太重要了。2.2 技术栈与依赖清单模块技术选型作用目标检测YOLOV5PyTorch / ONNX输出类别与像素框双目校正OpenCV stereoRectify把左右图拉到同一极线立体匹配OpenCV SGBM / BM生成视差图三维反投影OpenCV reprojectImageTo3D视差转三维坐标相机标定OpenCV calibrateCamera stereoCalibrate内外参与畸变系数运行环境Python 3.8、PyTorch、OpenCV推理与几何计算依赖安装这块我一般会先把 OpenCV 的 contrib 版本确认好因为 SGBM 在标准包里就有但如果你后面想换其他匹配算法contrib 会更省事。PyTorch 版本跟 YOLOV5 的 requirements 对齐就行不要盲目追新CUDA 版本和驱动对不上是新手最容易翻车的地方。# 基础环境按自己显卡的 CUDA 版本调整 torch 安装源 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python opencv-contrib-python pip install numpy pyyaml这段命令里cu118是 CUDA 11.8 对应的轮子如果你机器是 12.x换成对应版本即可。opencv-contrib-python和opencv-python不要同时装两个版本冲突的包装之前先pip list | grep opencv看一眼有旧的就卸掉。2.3 目录结构长什么样一个能跑的双目测距工程目录通常是这样组织的你拿到源码后先对照这个结构找文件比盲目翻代码快得多stereo_yolo/ ├── configs/ │ ├── stereo_params.yaml # 双目内外参、基线、图像尺寸 │ └── detect_params.yaml # 置信度阈值、NMS 阈值、类别名 ├── weights/ │ └── yolov5s.onnx # 检测模型权重 ├── calibration/ │ ├── left/ # 左目标定图 │ └── right/ # 右目标定图 ├── core/ │ ├── stereo_calib.py # 标定与校正矩阵计算 │ ├── detector.py # YOLOV5 推理封装 │ ├── depth.py # 视差图与三维反投影 │ └── pipeline.py # 主流程串联 ├── tools/ │ └── calibrate.py # 标定脚本入口 └── main.py # 实时运行入口configs放参数是为了改阈值不用动代码calibration放标定原图是为了随时重标core里四个文件各管一件事pipeline.py只做编排。这种拆法在二次开发时很关键你要换检测模型只动detector.py要换匹配算法只动depth.py互不影响。3. 从标定到测距核心链路的代码拆解3.1 双目标定先把内外参算对测距准不准七成看标定。标定分两步单目标定拿每个相机的内参和畸变系数双目标定拿两个相机之间的旋转和平移。下面这段是标定脚本的核心逻辑棋盘格参数按你实际用的格子数和方格边长改。import cv2 import numpy as np # 棋盘格内角点数注意是内角点不是格子数 PATTERN (9, 6) SQUARE_SIZE 0.025 # 方格边长单位米 def calibrate_single(image_paths): 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 obj_points, img_points [], [] for p in image_paths: img cv2.imread(p, cv2.IMREAD_GRAYSCALE) ok, corners cv2.findChessboardCorners(img, PATTERN, None) if not ok: continue # 亚像素精化这一步不做重投影误差会明显偏大 corners cv2.cornerSubPix(img, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) obj_points.append(objp) img_points.append(corners) ret, K, dist, _, _ cv2.calibrateCamera(obj_points, img_points, img.shape[::-1], None, None) return K, dist, obj_points, img_pointsPATTERN写错是标定失败的头号原因9x6 指的是内部角点不是黑白格数量数错一格整组数据全废。SQUARE_SIZE单位必须和后续三维坐标的单位一致用米就全程用米别中途混用毫米。cornerSubPix的窗口 (11,11) 是经验值图像分辨率高可以适当放大。单目标定返回的ret是重投影误差正常应该在 0.5 像素以内超过 1 像素说明标定图质量有问题别急着往下走。双目标定在单目基础上做把左右目的角点对应起来def calibrate_stereo(left_data, right_data, image_size): _, K1, d1, op1, ip1 left_data _, K2, d2, op2, ip2 right_data ret, K1, d1, K2, d2, R, T, E, F cv2.stereoCalibrate( op1, ip1, ip2, K1, d1, K2, d2, image_size, flagscv2.CALIB_FIX_INTRINSIC, criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5)) return K1, d1, K2, d2, R, TCALIB_FIX_INTRINSIC表示固定单目已经算好的内参只优化外参这样更稳。T是平移向量它的模长就是基线长度这个值直接决定测距量程基线越大量程越远但近处盲区也越大。标定完把K1, d1, K2, d2, R, T存进stereo_params.yaml后面校正和反投影都要用。3.2 立体校正与视差计算校正的目的是让左右图的极线水平对齐这样同一个物点在两幅图里只差水平方向一个位移匹配搜索从二维降到一维。校正映射表算一次就能复用不要每帧重算。def build_rectify_map(K1, d1, K2, d2, R, T, image_size): R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( K1, d1, K2, d2, image_size, R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0) map1x, map1y cv2.initUndistortRectifyMap(K1, d1, R1, P1, image_size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(K2, d2, R2, P2, image_size, cv2.CV_32FC1) return (map1x, map1y), (map2x, map2y), Qalpha0表示校正后只保留有效像素边缘会被裁掉换来的是没有黑边干扰匹配。Q是重投影矩阵后面reprojectImageTo3D直接用它。左右图分别cv2.remap之后接 SGBMdef compute_disparity(left_rect, right_rect): block 5 sgbm cv2.StereoSGBM_create( minDisparity0, numDisparities128, # 必须是 16 的倍数 blockSizeblock, P18 * block * block, P232 * block * block, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange2, modecv2.STEREO_SGBM_MODE_SGBM_3WAY) disp sgbm.compute(left_rect, right_rect).astype(np.float32) / 16.0 return dispnumDisparities决定能测多近值越大近处覆盖越好但计算量上升128 是 640 宽度下的常用起点。blockSize越小边缘越锐但噪声越多5 或 7 之间调。uniquenessRatio调高能压掉误匹配代价是弱纹理区域视差缺失变多。SGBM 输出的视差是放大 16 倍的定点数必须除以 16 才是真实像素视差这一步漏了深度会差一个数量级是血泪经验。3.3 YOLOV5 检测框与三维坐标的对接检测部分用 ONNX 推理输出框之后取中心点去视差图里查对应位置的视差值再用Q矩阵反投影。这里有个细节检测框中心可能落在无效视差区域值为 0 或负数直接查会得到离谱的深度所以要在框内做一次有效值筛选。def box_center_to_3d(box, disp, Q): x1, y1, x2, y2 map(int, box) cx, cy (x1 x2) // 2, (y1 y2) // 2 # 在中心附近取小窗口过滤无效视差后取中位数比单点稳 patch disp[max(cy-3,0):cy4, max(cx-3,0):cx4] valid patch[patch 0] if valid.size 0: return None d float(np.median(valid)) point np.array([[cx, cy, d, 1.0]], dtypenp.float32).T xyz Q point xyz / xyz[3] return xyz[:3].flatten() # 单位与标定一致米窗口取 7x7 是折中太小受噪声影响太大跨越物体边缘会把背景视差混进来。取中位数而不是均值是为了抗个别跳变点。Q point之后要除以第四维做齐次归一化漏了这步坐标全错。返回的xyz里 Z 就是深度X、Y 是相机坐标系下的横向和纵向偏移机械臂抓取时三个值都要用。3.4 主流程串联pipeline.py把上面几步按帧串起来顺序不能乱先校正再匹配再检测最后反投影。检测和匹配可以并行但校正必须在最前。def run_frame(left_raw, right_raw, maps, Q, detector, conf_thres0.4): left_rect cv2.remap(left_raw, *maps[0], cv2.INTER_LINEAR) right_rect cv2.remap(right_raw, *maps[1], cv2.INTER_LINEAR) disp compute_disparity(left_rect, right_rect) boxes, scores, classes detector.infer(left_rect, conf_thres) results [] for box, score, cls in zip(boxes, scores, classes): xyz box_center_to_3d(box, disp, Q) if xyz is not None: results.append({cls: cls, score: float(score), xyz: xyz.tolist()}) return resultsconf_thres默认 0.4现场光照差可以降到 0.3但误检会增多要配合类别过滤用。返回的results就是最终结构化输出往下接业务逻辑即可。4. 跑起来之前必须知道的避坑清单4.1 标定重投影误差偏大现象是单目标定ret超过 1 像素双目标定后测距整体偏移。原因多半是标定图数量不够或者姿态太单一全部正对相机拍角点约束不足。解决是每个相机至少 15 组图覆盖不同角度和画面边缘棋盘格要占画面三分之一以上拍的时候避免反光和运动模糊。重标之后对比ret降不下来就换一组图别硬调参数。4.2 视差图大面积空洞现象是弱纹理区域白墙、纯色桌面视差为 0检测框中心查不到深度。这是 SGBM 的固有局限不是代码错。解决有两个方向一是给场景加纹理工业场景可以打散斑投影二是对无效区域做后处理用周围有效视差做插值或者退化成用框内有效像素的均值。我一般会在box_center_to_3d里先试中心窗口失败再扩到整个框还失败就标记该目标深度不可用交给上层决策。4.3 深度值随距离线性漂移现象是近处准、远处偏或者反过来。原因通常是基线T和标定时的实际基线不一致或者Q矩阵用的图像尺寸和运行时不一致。检查stereo_params.yaml里的图像宽高是否和实际采集分辨率完全一致分辨率改了必须重算校正映射表不能复用旧表。另外确认numDisparities覆盖了最远距离对应的最小视差覆盖不到的部分深度是错的。4.4 检测框和视差图对不齐现象是三维坐标明显偏到物体旁边。原因是检测跑在原图、视差算在校正图上两幅图有裁切偏移。解决是统一在left_rect上做检测不要拿原始左图检测再去查校正后的视差图。如果检测模型必须用原图那就把框坐标用校正映射反向变换但这条路径容易出错不如直接在校正图上检测YOLOV5 对裁切不敏感。4.5 帧率掉到个位数现象是实时性不达标。排查顺序先看 SGBM 的numDisparities和blockSize是不是设太大再看检测是不是跑在 CPU 上最后看有没有每帧重算校正映射表。映射表必须初始化时算一次stereoRectify和initUndistortRectifyMap都不要放进帧循环。检测换 ONNX Runtime 的 GPU 推理通常能把整条链路拉到 15 FPS 以上。5. 二次开发换模型、换匹配、加滤波的改造路径拿到这套代码之后最常见的改造需求有三个换更轻或更准的检测模型、换立体匹配算法、给深度输出加时序滤波。换检测模型只动detector.py保持输入输出契约不变——输入一张 BGR 图输出 boxes、scores、classes 三个数组框格式统一成 xyxy 像素坐标。这样pipeline.py一行不用改。换匹配算法动depth.py把compute_disparity换成 BM 或者自己实现的块匹配只要返回单位是像素的浮点视差图即可注意不同算法的视差符号和缩放约定不一样SGBM 是除以 16BM 也是定点别照搬。加时序滤波是我在实际项目里必做的一步单帧深度抖动在机械臂场景里很致命。做法是对每个跟踪目标维护一个深度队列取滑动中位数from collections import deque class DepthSmoother: def __init__(self, window5): self.buf {} # key: 目标 id, value: deque self.window window def update(self, track_id, z): q self.buf.setdefault(track_id, deque(maxlenself.window)) q.append(z) return float(np.median(q))window取 5 是延迟和稳定性的折中产线速度快的场景降到 3静态测量可以加到 9。track_id需要上游有跟踪器没有的话可以先用检测框 IoU 做简易关联或者按类别加位置最近邻匹配。这个类不依赖具体匹配算法换检测模型也不受影响。验证改造是否有效我习惯用固定距离的标定板做基准把板放在 0.5m、1m、2m 三个位置每个位置采 100 帧看深度的均值和标准差。均值偏差超过 2% 说明标定或Q有问题标准差超过 1cm 说明匹配或滤波需要调。这个验证脚本比肉眼看画面靠谱得多改完任何一环都跑一遍。从那以后我每次动双目链路都强制先跑一遍固定距离基准再上真实场景省下来的返工时间远比写脚本的时间多。希望帮到你。本文还有配套的精品资源点击获取