简介:这份资源面向计算机视觉初学者与需要落地双目测距的开发者,围绕ZED立体相机与YOLO目标检测的联合使用,解决从环境搭建到实际测距的完整链路问题。压缩包共42个文件,约78.97MB,以20个Python脚本为核心,辅以YOLOv4与YOLOv4-tiny的cfg配置、weights权重、xml工程配置及cu/h等编译文件,覆盖检测、相机调用、可视化与标定等模块。已有3015人学习下载,说明其在同类实践中具备一定参考价值。读者可获得可直接运行的zedceju.py主程序,以及相机标定、TensorRT加速、多模型检测与显示工具等配套脚本,便于对照理解ZED深度信息与YOLO识别结果的融合逻辑,并借鉴numpy版本兼容、依赖配置等排错思路,快速复现物体识别与距离计算流程。
1. 从一台双目相机到真实距离:这个项目到底在算什么
很多人第一次拿到双目相机,第一反应是“拍两张图不就行了”,真跑起来才发现,两张图之间的像素差怎么变成米,才是整件事的核心。这个项目做的事情很具体:用 ZED 双目相机采集左右目图像,交给 YOLO 做目标检测,再把检测框内的目标位置换算成相机坐标系下的三维坐标,最终输出目标到相机的直线距离。入口脚本就是zedceju.py,一个文件把采集、推理、测距串起来,适合想快速验证双目测距链路、又不想从零搭工程的人。
它适合两类读者:一类是刚接触双目视觉,想先跑通一条“检测 + 测距”闭环的新手;另一类是手里已经有 ZED 和 YOLO 权重,想把两者接起来做二次开发的老手。项目本身不追求工程化封装,胜在链路短、依赖清晰,改起来不绕弯。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆。
2. 技术栈与目录结构:一个脚本背后的四层依赖
2.1 技术选型:为什么是 ZED + YOLO 的组合
双目测距的经典路线有两条:一条是纯几何,靠双目视差和标定参数直接算深度;另一条是学习路线,用深度估计网络出稠密深度图。这个项目走的是第一条,但把“哪里需要测”交给 YOLO 决定。这样做的原因是:纯几何测距对纹理缺失区域很敏感,整幅图算深度噪声大;而目标检测框天然圈定了有意义的区域,框内取视差中位数或均值,距离就稳得多。
ZED 在这里承担的是“高质量双目图像源 + 内参外参提供者”的角色。它的 SDK 能直接给出左右目校正后的图像和相机基线、焦距等参数,省掉了自己标定和极线校正的一大摊事。YOLO 负责检测,常见做法是用 Ultralytics 的接口加载权重,输出框的类别、置信度和像素坐标。两者之间靠一段坐标换算代码粘合,这也是整个项目最值得看的部分。
依赖上,除了pyzed(ZED SDK 的 Python 绑定)和ultralytics,还需要opencv-python、numpy。ZED SDK 本身要单独装,版本要和相机固件匹配,这是后面避坑章节要重点说的。
2.2 目录结构:短链路项目的典型布局
这类项目通常不会有大目录树,zedceju.py是主入口,旁边配几个辅助文件。一个合理的布局大致如下:
zed_yolo_distance/ ├── zedceju.py # 主入口:采集 + 检测 + 测距 ├── config.yaml # 相机参数、模型路径、测距阈值 ├── models/ │ └── best.pt # YOLO 权重 ├── utils/ │ ├── zed_capture.py # ZED 图像与参数读取封装 │ └── distance.py # 视差转深度、深度转距离 └── requirements.txt主脚本负责编排,utils里放可复用的换算逻辑。这样拆的好处是:换相机型号只动zed_capture.py,换测距公式只动distance.py,主流程不用碰。如果拿到手的版本是单文件,也建议按这个思路先拆一刀,后面调参时省很多来回。
2.3 核心参数表:测距准不准就看这几个数
| 参数 | 含义 | 典型取值 | 影响 |
|---|---|---|---|
baseline | 左右目基线距离(米) | 0.12 左右 | 越大测距越远,但近处盲区变大 |
fx | 校正后焦距(像素) | 由 SDK 给出 | 直接进公式,错一位距离全偏 |
disparity | 目标框内视差(像素) | 中位数 | 取均值易被边缘污染 |
conf_thres | YOLO 置信度阈值 | 0.4~0.5 | 太低框飘,测距跟着飘 |
roi_shrink | 框内缩比例 | 0.2 | 避开框边缘的背景像素 |
这张表建议直接落到config.yaml里,别硬编码在脚本中间。调参时改配置比翻代码快得多,也方便记录哪组参数在什么场景下好用。
3. 运行链路拆解:从打开相机到打印距离
3.1 环境准备与依赖安装
先装 ZED SDK,再装 Python 依赖。ZED SDK 的安装包要从相机厂商渠道拿,装完用python -c "import pyzed"验证。Python 依赖用虚拟环境隔离:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install pyzed ultralytics opencv-python numpy pyyaml注意pyzed这个包名在不同平台下略有差异,有的环境里是随 SDK 一起装的 wheel,直接pip install pyzed可能拉不到,这时要去 SDK 安装目录下的pyzed文件夹手动装。装完跑一句导入测试,能过再往下走。
3.2 相机初始化:拿到左右目图像和内参
ZED 的初始化有一套固定流程:创建Camera对象、配置分辨率与帧率、打开、然后每帧grab再retrieve。关键是要同时取到左目和右目校正后的图像,以及相机内参。
import pyzed.sl as sl def open_zed(): cam = sl.Camera() init = sl.InitParameters() init.camera_resolution = sl.RESOLUTION.HD720 init.camera_fps = 30 init.depth_mode = sl.DEPTH_MODE.NONE # 我们自己做测距,不用 SDK 深度 status = cam.open(init) if status != sl.ERROR_CODE.SUCCESS: raise RuntimeError(f"ZED 打开失败: {status}") # 读取校正后的相机内参 calib = cam.get_camera_information().camera_configuration.calibration_parameters fx = calib.left_cam.fx baseline = calib.get_camera_baseline() return cam, fx, baseline这段代码里有两个点值得说。depth_mode设成NONE是因为项目自己算视差,不需要 SDK 的深度图,关掉能省算力。get_camera_baseline()返回的是左右目物理基线,单位米,直接进后面的公式。fx取左目焦距,因为视差通常以左目为参考计算。
3.3 视差计算:在检测框内做块匹配
拿到左右目图像后,对 YOLO 检出的每个框,在框内做局部块匹配求视差。整幅图做 SGBM 再取框内值也行,但局部匹配更快,也更聚焦。
import cv2 import numpy as np def disparity_in_box(left, right, box, shrink=0.2): x1, y1, x2, y2 = box w, h = x2 - x1, y2 - y1 # 框内缩,避开边缘背景 x1 += int(w * shrink); x2 -= int(w * shrink) y1 += int(h * shrink); y2 -= int(h * shrink) roi_l = left[y1:y2, x1:x2] roi_r = right[y1:y2, x1:x2] stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=64, blockSize=9, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2 ) disp = stereo.compute(roi_l, roi_r).astype(np.float32) / 16.0 valid = disp[disp > 0] return float(np.median(valid)) if valid.size else 0.0numDisparities必须是 16 的倍数,它决定了能测多近;blockSize越大越平滑但边缘越糊。除以 16 是因为 SGBM 输出的是定点数,放大 16 倍存的。取中位数而不是均值,是为了让框内少量错误匹配不把结果带偏,这是血泪经验,均值在纹理复杂处经常翻车。
3.4 视差转距离:一个除法背后的坐标系
拿到视差后,距离公式就是Z = fx * baseline / disparity。这一步看着简单,但单位要对齐:fx是像素,baseline是米,disparity是像素,算出来Z就是米。
def disparity_to_distance(disp, fx, baseline): if disp <= 0: return None return fx * baseline / disp如果还想给出目标在相机坐标系下的 X、Y,就用框中心像素(u, v)反投影:X = (u - cx) * Z / fx,Y = (v - cy) * Z / fy。cx、cy同样从 SDK 内参里取。很多新手只算 Z 就以为完事,做抓取或避障时才发现还需要横向偏移,这一步别省。
3.5 主循环:把上面几步串起来
主循环的逻辑是:抓帧 → 左目图送 YOLO → 遍历框 → 框内求视差 → 转距离 → 画框标注。伪代码结构如下:
cam, fx, baseline = open_zed() model = YOLO("models/best.pt") runtime = sl.RuntimeParameters() while True: if cam.grab(runtime) != sl.ERROR_CODE.SUCCESS: continue left = sl.Mat(); right = sl.Mat() cam.retrieve_image(left, sl.VIEW.LEFT) cam.retrieve_image(right, sl.VIEW.RIGHT) img_l = left.get_data()[:, :, :3] # 去掉 alpha 通道 img_r = right.get_data()[:, :, :3] results = model(img_l, conf=0.45, verbose=False) for box in results[0].boxes.xyxy.cpu().numpy(): disp = disparity_in_box(img_l, img_r, box) dist = disparity_to_distance(disp, fx, baseline) if dist: cv2.putText(img_l, f"{dist:.2f}m", (int(box[0]), int(box[1]) - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("zed-yolo-distance", img_l) if cv2.waitKey(1) & 0xFF == 27: breakretrieve_image拿到的图带 alpha 通道,送 YOLO 前要切掉,否则通道数不对会报错。verbose=False关掉每帧日志,不然控制台刷得看不清测距输出。按 ESC 退出后记得cam.close(),不然下次打开可能占用失败。
4. 避坑与排查:双目测距最容易翻车的五个地方
4.1 距离整体偏大或偏小
现象:所有目标测出来都比实际远一截或近一截,比例大致固定。原因多半是fx或baseline取错,比如用了未校正图像的焦距,或者基线单位是毫米却当米用。解决:打印 SDK 返回的fx和baseline,用卷尺量一个已知距离的目标做对照,反推比例系数,确认是哪个参数错了再改,别直接乘个系数糊弄过去。
4.2 框内视差为 0 或大量无效值
现象:某些目标距离显示为空,或者视差算出来是 0。原因是框内纹理太弱(白墙、纯色物体),SGBM 匹配不上;也可能是numDisparities太小,近处目标视差超出范围。解决:把numDisparities调到 128 试试,框内缩比例调小一点保留更多纹理,实在不行对该框跳过并标注“不可测”,别硬给一个假距离。
4.3 YOLO 框抖动导致距离跳变
现象:同一静止目标,距离在 0.1 米范围内来回跳。原因是检测框每帧有微小位移,框内像素组成变了,视差中位数跟着变。解决:对距离做滑动平均,或者对框做跟踪(如 ByteTrack)稳定框位置;置信度阈值适当提高,低置信框本来就飘。这是玄学问题,滤波比调参管用。
4.4 相机打开失败或帧率骤降
现象:cam.open返回错误,或者跑一会儿帧率掉到个位数。原因是 ZED SDK 版本与相机固件不匹配,或者 USB 带宽不够(接了 Hub、用了 USB 2.0 口)。解决:查 SDK 版本文档确认支持范围,换直连 USB 3.0 口,分辨率从 HD1080 降到 HD720。帧率骤降还可能是每帧都新建StereoSGBM对象,把它提到循环外复用。
4.5 左右目图像对不齐
现象:测距结果随目标在画面中的位置变化很大,边缘处尤其离谱。原因是用了未校正的原始图像,或者左右目取图时序不一致。解决:确认VIEW.LEFT和VIEW.RIGHT取的是校正后图像,同一帧grab后连续retrieve,不要跨帧混用。ZED 的校正图是配准好的,用对了这一步基本不会出问题。
5. 二次开发:把单脚本改成能落地的测距模块
5.1 拆出可复用的测距类
单脚本跑通后,第一件事是把测距逻辑收成一个类,方便在别的项目里调用。核心接口就三个:初始化、单帧测距、释放。
class ZEDYoloRanger: def __init__(self, model_path, conf=0.45): self.cam, self.fx, self.baseline = open_zed() self.model = YOLO(model_path) self.conf = conf self.stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=128, blockSize=9 ) def measure(self): # 返回 [(类别, 距离米, 框), ...] ... def close(self): self.cam.close()把StereoSGBM放进__init__是关键改动,避免每帧重建。measure返回结构化结果而不是直接画图,上层想画框、想发消息、想存日志都行,解耦从这里开始。
5.2 加一层距离滤波与有效性判断
原始距离直接对外用,抖动会传到下游。常见做法是给每个跟踪 ID 维护一个固定长度的队列,取中位数输出;同时设一个有效视差下限,低于它就标记不可测。
| 改造点 | 做法 | 收益 |
|---|---|---|
| 滑动中位数 | 队列长度 5~7 | 抑制单帧跳变 |
| 有效视差下限 | disp > 1.0才计算 | 过滤远距离噪声 |
| 距离上限 | 超过量程标“超限” | 避免给出无意义大数 |
| 跟踪 ID 绑定 | 接 ByteTrack | 同一目标距离连续 |
这套组合拳下来,静止目标的距离波动能压到厘米级,移动目标也不会出现突跳。参数不用一次调到位,先上中位数滤波,不够再加跟踪。
5.3 验证方法:用已知距离做回归
改完代码怎么确认没改坏?我一般会在地上贴几个标记点,量好 1 米、2 米、3 米、5 米,把相机固定好,逐个对准读距离,记录误差。误差在量程 10% 以内算合格,近处应该更准。每次改测距相关代码都跑一遍这组对照,比盯着屏幕感觉靠谱得多。有条件的话把每次的读数存成 CSV,改完对比前后曲线,回归问题一眼能看出来。
5.4 一个容易忽略的改造点:分辨率与量程的权衡
numDisparities和分辨率共同决定量程。分辨率越高,同样物理视差对应的像素视差越大,远处目标才有的测;但高分辨率下 SGBM 计算量涨得快。我的习惯是先用 HD720 跑通,确认量程够用再考虑升分辨率,不够就加大numDisparities而不是盲目上 1080。曾经为了“看得更远”直接把分辨率拉满,结果帧率掉到 5 帧,测距延迟大到没法用,从那以后每次调分辨率都先跑一遍帧率测试,确认实时性再谈精度。希望这些拆解能帮到你,把zedceju.py跑起来只是起点,按上面的思路拆一层、滤一层,它才真正能进你的项目。
本文还有配套的精品资源,点击获取