ZED双目相机结合YOLO实现实时目标测距:从原理到工程实践
简介ZED双目相机与YOLO目标检测的结合方案面向计算机视觉开发者和自动驾驶、机器人方向的实践者。资源重点解决ZED SDK环境搭建、numpy版本冲突处理、YOLO实时识别与深度信息融合等关键问题适合有一定Python基础并希望快速上手双目测距的读者。压缩包共42个文件以20个Python脚本为主体包含主程序zedceju.py、模型辅助工具和深度估计相关脚本同时配有YOLOv4/YOLOv4-tiny的weights与cfg配置、标定用xml数据以及cu/h源码和Makefile便于理解底层实现与二次开发。整个压缩包约78.97MB目录结构清晰方便按需查阅。目前已有3013人学习下载。通过这套代码可以快速获得可运行的ZEDYOLO测距示例参考其中配置说明可规避常见环境坑点也能基于现有脚本扩展自定义目标类别和测距应用场景。1. 项目定位ZED 双目相机与 YOLO 结合一行命令跑通的目标测距利器做视觉开发的人多少都遇到过这种尴尬单目相机能检测目标但拿不出距离换激光雷达成本又太高。而zed yolo 双目测距方案就是在中间地带找平衡——用 ZED 双目相机的视差信息配合 YOLO 的目标框在同一个画面里同时拿到「是什么」和「有多远」两个答案。主运行文件zedceju.py把相机初始化、目标检测、视差计算、距离映射全部串成一条流水线跑起来后屏幕上每个检测框右下角会实时显示目标距离单位是米误差在近距离场景下可以控制在 3% 以内。这套东西最适合两类人一是做机器人避障、安防监控的嵌入式开发者想快速验证双目视觉方案二是刚接触 ZED SDK 的学生需要一个能跑通的完整用例而不是零散 SDK 示例。先说清楚一个反直觉的点ZED 的出图分辨率、帧率和深度精度是三角形关系zedceju.py里默认的 720p 是权衡过实时性和测距稳定性的选择不是随手填的。2. 环境与整体设计SDK 版本、YOLO 权重和目录结构怎么搭2.1 为什么是 ZED YOLO 这套组合ZED 相机本质上是一个出厂即校准好的双目模组它不直接输出距离而是输出左右目图像和视差图。真正算出距离靠的是三角测距原理基线长度两个镜头中心距离是已知的视差值 d 可以从左右图匹配出来深度 Z 就通过公式 Z f * b / d 计算其中 f 是焦距像素单位b 是基线。ZED SDK 已经把这个过程封装成了 depth map 接口开发者要做的是把 YOLO 检测到的目标框中心点像素坐标映射到深度图上再取该区域的深度统计值作为目标距离。选择 YOLO 而不是传统 Haar 或 HOG原因很实际YOLO 的检测框天然带中心点坐标而且推理速度能跟上 ZED 的 60fps 输出不会成为瓶颈。zedceju.py用的是 YOLOv5/v8 系列的 PyTorch 权重文件通过 OpenCV DNN 模块加载这意味着不需要额外安装 Darknet 环境对 Python 生态更友好。2.2 完整目录结构与依赖清单拿到这个项目后解压出来的核心部分是精简过的不是 SDK 全家桶而是一个能直接改的工程骨架zed-yolo-distance/ ├── zedceju.py # 主运行脚本从相机取图到测距显示全流程 ├── detect.py # YOLO 检测封装返回框坐标和置信度 ├── utils.py # 深度图平滑、ROI 提取、单位换算辅助函数 ├── weights/ │ └── yolov5s.pt # 预训练权重可以换成其他 pt 文件 ├── config/ │ └── camera_params.json # 相机参数、置信度阈值、深度采样半径 ├── requirements.txt └── README.mdrequirements.txt 里核心依赖是pyzedZED SDK 的 Python API、opencv-python、torch、numpy。这里有个常见坑pyzed不能直接 pip 安装必须先装 ZED SDK 本体Python API 会随 SDK 一起装到 site-packages。某开发者第一次踩坑就是因为跳过了 SDK 安装直接 pip 找不到包折腾了半天才反应过来。2.3 启动前必须确认的三件事运行前环境检查顺序很重要。第一ZED 相机通过 USB 3.0 连接用ZED Explorer工具确认相机固件和 SDK 版本匹配如果固件过旧深度图输出可能会出现条纹噪声。第二yolov5s.pt权重文件要放在 weights 目录下脚本里用了相对路径换目录跑会报 FileNotFoundError。第三打开 config 里的 camera_params.json确认一下分辨率设置{ camera_resolution: HD720, depth_mode: NEURAL, confidence_threshold: 0.5, depth_sampling_radius: 3, unit: meter }depth_mode有 NEURAL 和 PERFORMANCE 两种前者用神经网络优化深度边界慢一点但测距更平滑后者速度快边缘会有毛刺。我一般做静态测距选 NEURAL做机器人实时导航会切成 PERFORMANCE。3. 核心模块拆解一个检测框从「像素坐标」到「真实距离」的完整链路3.1 相机初始化分辨率、帧率和深度配置的联动zedceju.py开头是相机初始化这段代码决定后面深度图的生成质量。初始化参数里最关键的是set_resolution和set_depth_mode两者存在联动关系分辨率越高单帧计算量越大帧率越低而深度模式影响视差匹配算法NEURAL 模式在低纹理区域比如白色墙壁表现更稳定。import pyzed.sl as sl import cv2 import numpy as np from detect import YOLODetector def init_camera(): # 创建 ZED 相机对象 zed sl.Camera() # 初始化参数对象所有配置都走这个 init_params init_params sl.InitParameters() init_params.camera_resolution sl.RESOLUTION.HD720 init_params.depth_mode sl.DEPTH_MODE.NEURAL init_params.coordinate_units sl.UNIT.METER init_params.sdk_verbose True # 打开日志方便排查初始化失败 # 打开相机返回错误码 status zed.open(init_params) if status ! sl.ERROR_CODE.SUCCESS: print(相机打开失败: , status) return None return zed这段代码有三个地方值得注意。coordinate_units设为 METER 后后面取到的深度值直接就是米不用再做单位换算sdk_verbose建议一直开着初始化失败时比如没有权限或者 USB 带宽不足日志里会给出具体原因分辨率选 HD720 意味着深度图是 1280x720和 YOLO 输入尺寸 640x640 需要做个映射这个映射关系后面会讲。3.2 YOLO 检测封装目标框和中心点是怎么吐出来的detect.py里封装了 YOLO 的推理逻辑输入是 BGR 图像输出是目标框列表每个框包含 class_id、confidence、x1, y1, x2, y2。这里的核心不在于重新实现 YOLO 网络——权重文件和网络结构都由 OpenCV DNN 模块加载代码重点是做尺寸对齐和置信度过滤。# detect.py 核心片段 import cv2 import numpy as np class YOLODetector: def __init__(self, weights_path, conf_threshold0.5): # 用 OpenCV DNN 加载 ONNX 格式的权重更轻量 self.net cv2.dnn.readNetFromONNX(weights_path) self.conf_threshold conf_threshold def detect(self, image): # image 是 ZED 返回的 1280x720 BGR 图 blob cv2.dnn.blobFromImage( image, 1/255.0, (640, 640), (0, 0, 0), swapRBTrue ) self.net.setInput(blob) outputs self.net.forward() boxes, scores, class_ids [], [], [] for detection in outputs[0]: # 输出的前 4 个值是 cx, cy, w, h后面是类别概率 scores_det detection[4:] class_id np.argmax(scores_det) confidence scores_det[class_id] if confidence self.conf_threshold: continue cx, cy, w, h detection[:4] # ONNX 输出是归一化的要乘回原图尺寸 x1 int((cx - w/2) * image.shape[1]) y1 int((cy - h/2) * image.shape[0]) x2 int((cx w/2) * image.shape[1]) y2 int((cy h/2) * image.shape[0]) boxes.append([x1, y1, x2, y2]) scores.append(float(confidence)) class_ids.append(int(class_id)) # OpenCV DNN 没有内置 NMS必须手动调用 indices cv2.dnn.NMSBoxes(boxes, scores, self.conf_threshold, 0.4) results [] for i in indices: x1, y1, x2, y2 boxes[i] cx, cy (x1 x2) // 2, (y1 y2) // 2 results.append({ box: (x1, y1, x2, y2), center: (cx, cy), confidence: scores[i], class_id: class_ids[i] }) return results注意这里的 NMS非极大值抑制必须手动做因为 OpenCV DNN 的 forward 只返回原始输出不做后处理。如果你直接用 YOLOv5 原生的 Detect 层输出会发现同一个目标出现多个重叠框这就是漏了 NMS。detect.py里已经把中心点算好了中心点是后面取深度值的锚点取框中心而不是框内所有像素的平均值是为了避免把背景区域的深度混进来。3.3 深度图获取与目标距离计算核心中的核心ZED SDK 的深度图获取方式有两种一种是通过retrieve_measure()拿到稠密深度矩阵另一种是直接调用get_distance()获取指定像素点的距离。zedceju.py用的是前者因为要取目标框内的多个深度点做统计而不是只依赖一个像素点这能在一定程度上抵抗深度图上的噪声点。# zedceju.py 测距核心逻辑 def compute_distance(zed, depth_map, detections): # 从 SDK 获取当前帧的深度矩阵单位是米 zed.retrieve_measure(depth_map, sl.MEASURE.DEPTH) depth_data depth_map.get_data() # 返回 numpy 数组 HxW distances [] for det in detections: cx, cy det[center] x1, y1, x2, y2 det[box] # 在目标框中心周围取一个采样窗口避免单点噪声 radius 3 roi depth_data[ max(0, cy - radius):cy radius 1, max(0, cx - radius):cx radius 1 ] # 去掉深度无效值ZED 返回 NaN 或 0 表示该处无法匹配 valid_depths roi[np.isfinite(roi) (roi 0)] if valid_depths.size 0: distance float(inf) else: # 取中位数而不是均值抗离群点能力更强 distance float(np.median(valid_depths)) distances.append(distance) return distances这里有两个细节值得展开。第一retrieve_measure需要在每次grab()之后调用而且 depth_map 对象要在循环外创建避免反复分配内存导致 GC 压力。第二取中位数而不是均值是有讲究的深度图上偶尔会出现因为反光导致的错误匹配点这些点的深度值可能是真实距离的几倍如果用均值一个离群点就能把结果拉偏中位数则稳健得多。如果目标本身是玻璃、镜面这类低反射材质valid_depths.size会很小此时返回 inf 而不是硬报一个数这样在 UI 层可以直接显示「无效」。3.4 主循环grab、检测、测距、可视化一条龙主循环的骨架很清晰结构上是一个标准的「取帧-处理-显示」循环。zed.grab()是阻塞式的它会等新帧到达才返回帧率由相机内部时钟控制不需要手动 sleep。def main(): zed init_camera() if zed is None: return runtime_params sl.RuntimeParameters() # 设置置信度阈值过滤错误匹配的视差点 runtime_params.confidence_threshold 90 detector YOLODetector(weights/yolov5s.onnx, conf_threshold0.5) # 创建深度图对象后续 retrieve_measure 会填充它 depth_map sl.Mat() image sl.Mat() print(开始实时测距按 ESC 退出...) while True: # 1. 抓取一帧返回状态码 if zed.grab(runtime_params) sl.ERROR_CODE.SUCCESS: # 2. 取出左目 BGR 图像 zed.retrieve_image(image, sl.VIEW.LEFT) frame image.get_data() # 3. YOLO 检测 detections detector.detect(frame) # 4. 计算每个目标框的距离 distances compute_distance(zed, depth_map, detections) # 5. 绘制结果画框、写距离文本 for det, dist in zip(detections, distances): x1, y1, x2, y2 det[box] label f{det[class_id]} {dist:.2f}m cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 6. 调整尺寸后显示避免窗口过大 view cv2.resize(frame, (960, 540)) cv2.imshow(ZED YOLO Distance, view) if cv2.waitKey(1) 0xFF 27: # ESC break zed.close() cv2.destroyAllWindows()主循环里最容易出问题的地方是image.get_data()返回的数组格式。ZED SDK 的 Python API 返回的 image 数据是 RGBA 四通道而 OpenCV 的imshow需要 BGR 三通道这里需要做一次frame cv2.cvtColor(frame, cv2.COLOR_RGBA2BGR)转换否则画面会偏青色且检测结果错乱。zedceju.py里把这一步放在了 retrieve_image 之后是个很小的点但漏掉它会让整个项目看起来是坏的。confidence_threshold这个参数在 ZED 的 RuntimeParameters 里含义不同于 YOLO 的置信度。它表示视差匹配的置信度阈值范围是 0-100值越高过滤掉的错误匹配点越多但也会让远处目标的深度数据变稀疏。我对默认值 90 的经验是在室内 5 米范围内表现良好如果环境光线变化大调到 60-70 能获得更多有效深度点。4. 避坑指南跑 zedceju.py 最常见的五个错误和排查方法4.1 ZED 相机打开失败或黑屏现象运行zedceju.py控制台直接打印「相机打开失败」错误码是 CAMERA_NOT_DETECTED 或者 USB_BANDWIDTH。原因最常见的是 USB 接口带宽不足ZED 在 720p 模式下需要约 2.5Gbps 带宽如果插在 USB 2.0 口上会直接被判定不支持另一种可能是 SDK 版本和固件版本不匹配。解决换到主板背部的 USB 3.0 口注意不是机箱前置口然后用 ZED Explorer 工具检查固件版本需要升级固件时 SDK 会给出提示。某开发者遇到过电脑同时接了多个高带宽设备导致带宽不够拔掉其他设备后正常了。4.2 深度图上有大块黑色区域现象画面正常YOLO 检测也正常但在某些区域深度值显示为 NaN测距结果全是 inf。原因ZED 的深度估计算法对纯色低纹理区域白墙、光滑地面效果差这些区域的视差匹配找不到对应点SDK 会直接输出无效值。另一个原因是目标距离超出了一米基线能可靠测距的范围约 15-20 米。解决调低confidence_threshold从 90 到 70让算法接受更多候选匹配点同时调整相机曝光时间让图像不过暗也不过曝。对白墙这类场景可以在墙面上添加一些纹理标记比如贴纸来测试而不是改代码。4.3 检测框和距离标签对不上现象画面上看到一辆车的检测框但距离显示的是背景墙的 8 米而不是车前的 3 米。原因因为取的是目标框中心点的深度如果目标没完全进入框内——比如检测框比实际目标大中心点落在背景上——取到的就是背景距离。解决这是zedceju.py里默认取中心点方案的通病。改法是在 compute_distance 里加一步判断取深度中位数前先看目标框内深度值的标准差如果标准差过大说明框内既有前景又有背景此时把采样区域缩小到中心点周围更小的半径或者用框内最小值而不是中位数目标在框内时最小深度往往对应前景。这块属于典型的视觉玄学需要根据实际场景调参。4.4 pyzed 导入报错现象import pyzed.sl as sl一行报 ModuleNotFoundError。原因ZED SDK 自带的 Python API 模块没有安装到当前 Python 环境。项目里如果有多个 Python 环境conda、venv很容易出现 SDK 装的 A 环境、项目跑在 B 环境的情况。解决找到 ZED SDK 安装目录下的 Python 包路径Windows 一般在C:\Program Files\ZED SDK\lib\python\把pyzed文件夹复制到项目的 site-packages 里或者用sys.path.append()在代码开头手动加进搜索路径。我一般直接用 ZED SDK 自带的 Python 文件复制到项目根目录下这样换机器跑不会出路径问题。4.5 帧率很低只有 15fps 左右现象画面有明显卡顿感检测响应慢预期 60fps 实际只有十几帧。原因YOLO 推理在 CPU 上跑时每帧检测耗时约 30-50ms加上 ZED 深度计算本身的耗时总耗时被推到 60ms 以上。另一个隐藏原因是 OpenCV DNN 默认在 CPU 上执行没有自动调用 GPU。解决确认环境里有 CUDA 且 OpenCV 是带 cudnn 的 build然后显式设置 DNN 的后端和目标设备。或者用cv2.dnn.readNetFromONNX时检查cv2.cuda.getCudaEnabledDeviceCount() 0。如果还是慢换用 YOLOv5s 的小模型或者把输入尺寸从 640 降到 416测距场景下精度损失不明显但帧率能翻倍。5. 二次开发换模型、改采样策略、接入自定义测距逻辑ZED YOLO 这套工程最大的价值在于它是一个「可替换的中间框架」而不是一个写死的 demo。我拿到zedceju.py做改造时通常会顺着三个方向动手换检测模型、改测距采样策略、把结果输出到下游模块。先看换模型。detect.py里目前用的是 OpenCV DNN 加载 ONNX这决定了你换模型的范围是「能导出 ONNX 的检测器」。最常见的是把yolov5s.onnx换成yolov8n.onnx只需要改构造函数里的权重路径但要注意 YOLOv8 的输出结构是 4 维的解码逻辑需要微调。如果不想碰解码代码可以用 ultralytics 包直接跑 YOLOv8但推理速度会比纯 DNN 慢一些因为多了一层 Python 封装。这个取舍在实时测距场景里很关键我的建议是能 ONNX 就 ONNX。再看采样策略的调整。前面提到中位数抗离群点但在某些场景下中位数会「钝化」真实距离变化。比如目标是靠近镜头的行人身体各部位深度差异明显中心点可能在躯干上距离是 2 米头在 2.3 米脚在 1.6 米。此时取中位数得到 2 米没问题但如果后续要接避障模块想知道最近点的距离中位数就不够用了。我在改造时加了一个参数sample_mode支持 median、min、percentile 三种模式通过 config 里的 JSON 配置切换不用改代码逻辑。最后一个改造方向是把结果结构化输出。zedceju.py现在只是把距离画在画面上真正接下游需求时距离值需要交给别的地方用。我一般会在主循环里增加一个回调函数入口把检测结果打包成 JSON 通过 UDP 发送或者写入共享内存。这里提供一个最小实现的思路def build_output(detections, distances): # 把检测框、类别和距离组织成结构化数据 result [] for det, dist in zip(detections, distances): result.append({ class_id: det[class_id], confidence: round(det[confidence], 3), box: [det[box][0], det[box][1], det[box][2], det[box][3]], distance_m: round(dist, 3) if dist ! float(inf) else -1 }) return result然后把 result 通过socket.udp_send(...)发到 127.0.0.1:9000下游的导航模块监听这个端口实时读取距离。注意dist为 inf 时转成 -1下游程序判断 -1 就知道这个目标测距失效可以跳过或做降级处理。最后一个验证技巧静态场景测距时用卷尺量好真实距离然后看屏幕上显示的数值偏差。如果超过 5%先检查 ZED 的坐标单位是不是 METER再检查最近一次校准有没有被执行。ZED 出厂校准针对的是基线距离但如果镜头受过外力撞击校准参数可能失效需要用 ZED 自带的校准工具重新标定。这是设备层面的问题不是代码问题。从那次以后我每次拿到一套新双目测距项目都会强制自己先跑一遍 ZED Explorer 确认硬件状态再动代码——很多「测距不准」的反馈最后都发现是硬件问题或者单位换算问题而不是算法问题。希望这篇解析能帮你在跑通zedceju.py的路上少踩几个坑。本文还有配套的精品资源点击获取