简介:面向计算机视觉学习者与自动驾驶、机器人导航、无人机避障等场景开发者,这份资源聚焦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 技术栈与依赖清单
| 模块 | 技术选型 | 作用 |
|---|---|---|
| 目标检测 | YOLOV5(PyTorch / 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, flags=cv2.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, flags=cv2.CALIB_ZERO_DISPARITY, alpha=0) 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), Qalpha=0表示校正后只保留有效像素,边缘会被裁掉,换来的是没有黑边干扰匹配。Q是重投影矩阵,后面reprojectImageTo3D直接用它。左右图分别cv2.remap之后,接 SGBM:
def compute_disparity(left_rect, right_rect): block = 5 sgbm = cv2.StereoSGBM_create( minDisparity=0, numDisparities=128, # 必须是 16 的倍数 blockSize=block, P1=8 * block * block, P2=32 * block * block, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2, mode=cv2.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):cy+4, max(cx-3,0):cx+4] valid = patch[patch > 0] if valid.size == 0: return None d = float(np.median(valid)) point = np.array([[cx, cy, d, 1.0]], dtype=np.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_thres=0.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 是除以 16,BM 也是定点,别照搬。
加时序滤波是我在实际项目里必做的一步,单帧深度抖动在机械臂场景里很致命。做法是对每个跟踪目标维护一个深度队列,取滑动中位数:
from collections import deque class DepthSmoother: def __init__(self, window=5): self.buf = {} # key: 目标 id, value: deque self.window = window def update(self, track_id, z): q = self.buf.setdefault(track_id, deque(maxlen=self.window)) q.append(z) return float(np.median(q))window取 5 是延迟和稳定性的折中,产线速度快的场景降到 3,静态测量可以加到 9。track_id需要上游有跟踪器,没有的话可以先用检测框 IoU 做简易关联,或者按类别加位置最近邻匹配。这个类不依赖具体匹配算法,换检测模型也不受影响。
验证改造是否有效,我习惯用固定距离的标定板做基准:把板放在 0.5m、1m、2m 三个位置,每个位置采 100 帧,看深度的均值和标准差。均值偏差超过 2% 说明标定或Q有问题,标准差超过 1cm 说明匹配或滤波需要调。这个验证脚本比肉眼看画面靠谱得多,改完任何一环都跑一遍。
从那以后我每次动双目链路,都强制先跑一遍固定距离基准,再上真实场景,省下来的返工时间远比写脚本的时间多。希望帮到你。
本文还有配套的精品资源,点击获取