简介这份资源面向计算机视觉初学者与需要落地双目测距的开发者围绕ZED立体相机与YOLO目标检测的联合使用解决从环境搭建到距离计算的完整链路问题。包内共42个文件以20个Python脚本为核心配合YOLOv4与YOLOv4-tiny的cfg配置、weights权重、xml工程配置及cu/h等编译文件另有说明文档与演示动图压缩包约78.97MB覆盖检测、标定、可视化与TensorRT加速等模块。已有3015人学习下载说明其在同类实践中具备一定参考价值。读者可据此获得ZED SDK安装与numpy版本冲突的排错思路、YOLO模型接入深度信息的调用方式以及zedceju.py主流程的完整实现便于快速复现物体识别与测距效果并在此基础上调整相机参数或微调模型以适配自动驾驶、机器人导航等场景。1. 从一台双目相机说起这个项目到底能测出什么手头有一台 ZED 双目相机想让它告诉你画面里某个物体离你多远——这件事听起来简单真做起来坑不少。这个项目的核心就是一份能直接跑起来的zedceju.py它把 ZED 相机的左右目图像取出来配合 YOLO 做目标检测再对检测框内的目标做双目测距最终在画面上标出类别和距离。换句话说它解决的是「看见 测距」这一条链路YOLO 负责告诉你这是什么、在哪双目视差负责告诉你它有多远。适合谁手里有 ZED 相机、想快速验证双目测距效果、或者想在此基础上做避障、抓取、跟随的开发者。整份代码是单文件脚本没有复杂工程结构新手照着装依赖就能跑熟手可以直接拆开改检测模型或测距策略。下面从环境、代码结构、核心算法到二次开发一层层拆。2. 环境与依赖让 zedceju.py 在本地跑起来2.1 硬件与软件前置条件ZED 相机不是普通 USB 摄像头它依赖厂商 SDK 才能拿到左右目图像和深度信息。所以跑这个脚本之前必须先装好 ZED SDK并且确认相机能被 SDK 识别。常见做法是先跑 SDK 自带的示例程序能出图再往下走否则后面所有报错都会指向同一个根因——相机根本没连上。软件层面需要的东西不多Python 3.8 左右、pyzedZED SDK 的 Python 绑定随 SDK 安装、ultralyticsYOLO 的现代实现、opencv-python、numpy。这里有个血泪经验pyzed不要用 pip 单独装它和 SDK 版本强绑定装错版本会出现 import 成功但调用即崩的情况正确做法是装完 SDK 后用它自带的 Python API 路径。# 确认 ZED SDK 已安装并能识别相机 # 先跑 SDK 自带示例能出图再继续 python /usr/local/zed/tools/ZED_Explorer # 图形化确认相机状态 # Python 依赖pyzed 随 SDK 提供不在此列 pip install ultralytics opencv-python numpy上面命令里ZED_Explorer是 SDK 自带的诊断工具用来确认相机固件、USB 带宽、左右目是否正常。ultralytics装好后会自动拉取 YOLO 权重第一次运行需要联网下载模型文件。参数上没什么可调的重点是版本对齐SDK 大版本和pyzed必须一致YOLO 权重和ultralytics版本尽量匹配否则会出现推理输出解析失败。2.2 目录与文件组织这个项目是单文件脚本结构极简但理解它的组织方式对改造很重要。典型布局如下project/ ├── zedceju.py # 主脚本取图 检测 测距 显示 ├── yolov8n.pt # YOLO 权重首次运行自动下载 └── requirements.txt # 依赖清单可选自己补zedceju.py内部大致分四块相机初始化、YOLO 模型加载、主循环取帧→检测→测距→绘制、资源释放。这种「一个文件跑通全流程」的写法适合验证但不适合长期维护后面二次开发章节会讲怎么拆。权重文件放在脚本同级目录最省事YOLO 默认会去当前工作目录找找不到再下载。提示运行脚本时的工作目录要和权重文件一致否则会出现「模型加载成功但路径找不到」的迷惑现象本质是相对路径问题。2.3 第一次运行与预期输出装好依赖、连好相机后直接python zedceju.py。正常情况会弹出一个 OpenCV 窗口画面里每个被 YOLO 检出的目标上有一个框框上方标注类别名和估算距离单位通常是米。如果窗口一闪而过或者报错先看终端第一行异常八成是相机没打开或pyzed导入失败。python zedceju.py # 预期弹出实时画面检测框带类别 距离标注 # 退出按 q 或 Esc取决于脚本里的按键判断这里要注意ZED 相机的分辨率设置会直接影响帧率和测距精度。脚本里如果用的是默认 HD720帧率能到 30 左右如果改成 HD1080帧率会掉但远处目标的视差更稳。第一次跑建议先用默认参数确认链路通再调分辨率。3. 核心链路拆解从取帧到距离数字的每一步3.1 相机初始化与左右目图像获取ZED 的 Python API 用Camera对象管理一切。初始化时要指定分辨率、帧率、深度模式等参数然后grab()取一帧再分别拿左目图像和测量数据。测距本质上用的是视差而视差来自左右目图像的匹配所以要么让 SDK 直接算深度图要么自己拿左右目做匹配。这个脚本走的是「用 SDK 的测量接口拿距离」的路线省去自己写立体匹配的麻烦。import pyzed.sl as sl # 初始化相机参数 zed sl.Camera() init_params sl.InitParameters() init_params.camera_resolution sl.RESOLUTION.HD720 # 分辨率影响帧率与精度 init_params.camera_fps 30 # 帧率 init_params.depth_mode sl.DEPTH_MODE.PERFORMANCE # 深度模式PERFORMANCE 偏速度 init_params.coordinate_units sl.UNIT.METER # 距离单位统一为米 err zed.open(init_params) if err ! sl.ERROR_CODE.SUCCESS: print(相机打开失败:, err) exit(1) # 运行时参数测量用的矩阵与坐标系 runtime_params sl.RuntimeParameters() image_left sl.Mat() # 左目图像喂给 YOLO depth_map sl.Mat() # 深度图用于取距离这段代码的关键在depth_mode和coordinate_units。PERFORMANCE模式牺牲一点精度换帧率适合实时检测如果做静态高精度测量可以换成QUALITY。coordinate_units设成米之后后面所有距离数字都不用再换算少一层出错的可能。image_left是给 YOLO 用的depth_map是给测距用的两者在同一帧里对齐这是能测准的前提。3.2 YOLO 检测与检测框坐标提取YOLO 这一侧用ultralytics的接口最省事加载模型、推理、拿结果三步。检测结果里每个框有xyxy坐标、类别 id、置信度。测距只需要框的中心点或框内区域所以要把坐标取出来传给测距逻辑。from ultralytics import YOLO model YOLO(yolov8n.pt) # 轻量模型实时性优先 # 主循环内左目图像转成 numpy 后推理 results model(frame_left_np, verboseFalse) for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() # 框的左上、右下坐标 cls_id int(box.cls[0]) # 类别 id conf float(box.conf[0]) # 置信度 cx, cy int((x1 x2) / 2), int((y1 y2) / 2) # 框中心用于取深度xyxy是像素坐标cx, cy是框中心。为什么用中心而不是整个框因为框边缘常常包含背景深度值会被背景污染中心点相对更接近目标本体。verboseFalse关掉每帧日志否则终端会被刷屏。置信度阈值默认 0.25如果误检多可以调高漏检多就调低这个参数在model()调用里传conf0.4即可。3.3 视差转距离深度图取值与距离计算拿到框中心坐标后去深度图里取对应位置的值就是该点到相机的距离。ZED 的Mat对象用get_value(x, y)取值返回一个包含 x、y、z 的数组其中 z 就是深度方向的距离。这里有个细节如果该点深度无效比如纹理缺失、遮挡返回值会是 NaN 或一个极大值必须过滤否则画面上会出现「距离 0」或「距离 inf」的鬼畜标注。point_cloud_value depth_map.get_value(cx, cy) distance point_cloud_value[1] # 索引 1 对应 z 方向单位米 # 过滤无效深度 if distance 0 and distance 20: # 20 米外视为不可靠 label f{model.names[cls_id]} {distance:.2f}m cv2.rectangle(frame_left_np, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame_left_np, label, (int(x1), int(y1) - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)point_cloud_value[1]取的是 z 分量这是 ZED 坐标系约定x 向右、y 向下、z 向前。距离过滤上限设 20 米是经验值ZED 在远距离的视差精度下降明显超过这个范围数字参考价值不大。绘制部分用绿色框加文字文字位置在框上方 8 像素避免和框线重叠。如果目标密集文字会互相遮挡可以改成只标最近的目标或者用不同颜色区分距离区间。3.4 主循环与资源释放整个流程包在一个while循环里每轮grab()取帧、推理、测距、绘制、imshow按键退出后close()释放相机。资源释放这一步很多人会漏导致下次运行相机被占用。while True: if zed.grab(runtime_params) sl.ERROR_CODE.SUCCESS: zed.retrieve_image(image_left, sl.VIEW.LEFT) # 取左目 zed.retrieve_measure(depth_map, sl.MEASURE.DEPTH) # 取深度 frame_left_np image_left.get_data()[:, :, :3] # BGRA 转 BGR # ... 检测与测距逻辑 ... cv2.imshow(ZED YOLO, frame_left_np) if cv2.waitKey(1) 0xFF in (ord(q), 27): break zed.close() cv2.destroyAllWindows()get_data()返回的是 BGRA 四通道取前三个通道转成 BGR 才能给 OpenCV 和 YOLO 用这一步漏了会出现颜色异常或推理报错。waitKey(1)里的 1 是毫秒太小窗口响应慢太大帧率掉1 到 10 之间都行。退出判断同时支持q和 Esc照顾不同习惯。4. 避坑与排查跑不通时先看这几条4.1 相机打不开或频繁断连现象是zed.open()返回非 SUCCESS或者跑几秒后画面卡死。原因通常是 USB 带宽不足或供电不稳ZED 对 USB3.0 要求高接在 USB2.0 口或经过劣质扩展坞就会出问题。解决方法是直连主板后置 USB3.0 口换一根质量好的线关掉其他占用相机的程序。如果还不行把分辨率降到 HD720、帧率降到 15 再试能跑说明是带宽问题。4.2 距离数字乱跳或恒为 0现象是同一个静止目标距离在 0.5 米和 8 米之间反复跳或者一直显示 0。原因有两个一是取深度时用了框边缘坐标背景深度混进来二是该点深度无效但没过滤。解决方法是改用框中心取深度并加上distance 0 and distance 20的过滤。另外确认coordinate_units设的是米设成毫米的话数字会大 1000 倍看起来像乱跳。4.3 YOLO 推理报错或类别名对不上现象是model.names[cls_id]抛 KeyError或者框画出来了但没文字。原因是权重文件和代码里假设的类别数不一致或者cls_id越界。解决方法是打印model.names确认类别表用model.names.get(cls_id, unknown)兜底。如果换了自己训练的权重类别表会变代码里不能硬编码类别名。4.4 画面颜色发蓝或发绿现象是显示出来的图像颜色明显不对。原因是get_data()返回 BGRA直接喂给 OpenCV 会多一个 alpha 通道或者通道顺序没转。解决方法是[:, :, :3]取前三通道确认是 BGR 顺序。如果还是不对检查是不是把VIEW.LEFT和VIEW.RIGHT搞混了右目图像在某些模式下通道排列不同。4.5 帧率低到无法实时现象是画面卡顿帧率个位数。原因是分辨率太高、YOLO 模型太大、或者每帧都做了多余的数据拷贝。解决方法是分辨率降到 HD720、换yolov8n这类轻量模型、把retrieve_image和推理放在必要的位置避免重复转换。如果还慢可以把 YOLO 推理改成隔帧检测中间帧沿用上一帧的框实时性会明显改善。5. 二次开发把单文件脚本改成能长期维护的测距模块单文件脚本跑通之后真正要落地时问题就来了想换检测模型、想加跟踪、想把距离发给下游控制程序全挤在一个文件里改起来很痛苦。我一般会做三件事拆模块、抽配置、加接口。第一步拆模块。把相机封装成一个类对外只暴露get_frame()和get_depth(x, y)把检测封装成另一个类暴露detect(frame)返回框列表主循环只负责编排。这样换 YOLO 版本或换测距策略时改动范围可控。class ZedCamera: def __init__(self, resolutionsl.RESOLUTION.HD720, fps30): self.zed sl.Camera() params sl.InitParameters() params.camera_resolution resolution params.camera_fps fps params.coordinate_units sl.UNIT.METER assert self.zed.open(params) sl.ERROR_CODE.SUCCESS self.image sl.Mat() self.depth sl.Mat() self.runtime sl.RuntimeParameters() def get_frame(self): if self.zed.grab(self.runtime) ! sl.ERROR_CODE.SUCCESS: return None, None self.zed.retrieve_image(self.image, sl.VIEW.LEFT) self.zed.retrieve_measure(self.depth, sl.MEASURE.DEPTH) return self.image.get_data()[:, :, :3], self.depth def get_distance(self, x, y): val self.depth.get_value(x, y)[1] return val if 0 val 20 else None def close(self): self.zed.close()这个类把相机生命周期和取数逻辑收在一起get_distance内置了有效性过滤调用方不用再判断 NaN。参数上resolution和fps做成构造参数不同场景传不同值不用改源码。第二步抽配置。分辨率、置信度阈值、距离上限、模型路径这些全部提到一个字典或配置文件里主循环读配置。这样调参不用碰逻辑代码也方便做多组实验对比。第三步加接口。如果下游要拿距离做控制别在主循环里直接调控制函数而是把「类别 距离 框坐标」打包成一个结构通过队列或回调发出去。这样测距和控制在时间上解耦控制侧卡顿不会拖慢画面。验证改造是否成功我习惯用固定场景回归把一个已知距离的物体比如 2 米外的箱子放在画面里跑改造前后的代码看距离读数是否一致、帧率是否没掉。改造不是重写行为不变是底线。有个教训我记了很久早期图省事把测距和串口发送写在一个循环里结果串口一阻塞整个画面就冻住排查了半天以为是相机问题。从那以后我每次做实时链路都强制把「取数」和「消费」拆到不同线程或队列哪怕多写几十行。希望这个习惯也能帮到你少走一次那种「以为是硬件、其实是自己代码堵住」的弯路。本文还有配套的精品资源点击获取