# stereo_yolo(Python 版) 双目 YOLO 检测前端:在左右目图像上**独立运行** YOLO 检测器,输出符合 `stereo_ranger` 输入契约的检测框(每目一个 `DetectionFrame`、共用 frame_id)。 不做测距、左右目关联与立体校正。 规格:`docs/双目YOLO目标识别需求说明.md`(接口契约以其为准;几何语义权威 `docs/双目测距技术路线.md`)。 ⚠ 全部参数为占位值、demo 素材为 AI 合成图:标定与地面真值验证完成前, 仅限台架/回放验证,不得接入实飞控制回路(规格 §7 红线同源)。 ## 接口清单 | 名称 | 说明 | | --- | --- | | `StereoYoloFrontend(config, detector=None)` | 主入口;`detector` 可注入(接口 `detect(img) -> list[Detection]`,单测用假检测器) | | `.detect(left_img, right_img, frame_id) -> (DetectionFrame, DetectionFrame)` | 单帧检测;BGR ndarray(cv2 约定),输出框位于输入图原生坐标系 | | `.close()` | 释放并发线程池 | | `.config` | 只读配置访问 | | `StereoYoloConfig` / `load_from_json(path)` | 7 项配置;缺项/未知字段/类型错误/非法取值一律 `ValueError`(fail-fast) | | `UltralyticsDetector` | ultralytics YOLO `.pt` 后端(双实例池;`ultralytics` 延迟导入) | | `split_sbs(image) -> (left, right)` | side-by-side 单流裁切(宽须为偶数) | | `Detection` / `DetectionFrame` | 直接复用 `stereo_ranger.types`(纯值语义,§2.1 推荐) | | `WarnThrottle` | 告警节流(第 1 次 + 每满 100 次,带累计计数) | `logger` 名称为 `stereo_yolo`,走标准 `logging`。 ## 单帧流程(`detect` 四步) 1. **形状一致性**:左右图形状不一致 → 整帧拒收输出空 + WARN 节流 (§2.2 第 2 条:禁止左右各自缩放/填充); 2. **左右推理**:`parallel`(默认,ThreadPoolExecutor(2),§5 硬性要求)或 `sequential`(基准对照);单目异常被隔离,该目输出空帧列表(§5 失败处理); 3. **出口自检 + 白名单**:丢弃非有限值 / `x2<=x1` / `y2<=y1` 的框 (§2.2 第 6 条);`class_whitelist` 非空时过滤类别; 4. **封装**:每目一个 `DetectionFrame(frame_id, detections)`,`frame_id` 左右共用(§2.3 同帧契约)。 ## 实现决策(v1.0 冻结) 1. **ultralytics 直跑 `.pt`**(项目拍板口径):letterbox、输出解码、NMS、 框映射回原生坐标系全部由库内完成;识别端只负责阈值外置、出口自检与 契约封装。 2. **双实例池**:ultralytics 单实例非线程安全,左右并发各持一份模型 (§4 并发策略的"双实例"形态)。 3. **无跨帧状态**:前端不持有任何历史(无追踪、无平滑、无 Coast)—— 结构性满足"只送本帧原生实测框"(§2.5 第 1 条)。 4. **形状不一致不 raise**:按"拒收 + 空帧 + 节流 WARN"处理,与 stereo_ranger 非同帧拒收同哲学(不阻塞整体)。 5. **几何参数单一来源**:`stereo_yolo.json` 不存内参/原分辨率(输出框天然在 输入图坐标系),F/B/cx/cy 唯一来源是 `utils/config/stereo_ranger.json`, 避免两处漂移(§4 配置一致性)。 6. **`track_id` 留 `None`**:外部追踪器注入属链路后段(§2.1;v1.1 可选)。 ## 移植注记(C++ 侧需自行复现的库内行为) 本次为 ultralytics 直跑口径,以下行为在库内完成、C++ 实现必须逐项复现 (不一致会在视差取点上引入系统性偏差): | 库内行为 | C++ 需复现要点 | | --- | --- | | letterbox | 等比缩放到 `imgsz`、灰边填充(ultralytics 预测默认填充值 114);左右同一套参数(形状一致时天然满足) | | 输出解码 | YOLO26 检测头输出布局与解码(建议导出一份 ONNX 作为参照物;本仓环境:ultralytics 8.4 / torch 2.11 / onnx 1.18) | | NMS | `conf` 先过滤、NMS 用配置 IoU 门限;`max_det` 默认 300 | | 坐标回映 | letterbox 逆变换(scale/pad)到原生分辨率;输出 `x1,y1,x2,y2` 浮点 | | 并发 | 双实例双线程(单实例并发调用不安全) | ## 配置项(全部外置,占位值) | 键 | 占位值 | 说明 | | --- | --- | --- | | `model_path` | `models/yolo26n-drone.pt` | 相对路径按启动工作目录解析(demo 内按仓库根兜底) | | `imgsz` | 640 | 须为 32 的整数倍(YOLO 步长约束) | | `conf_threshold` | 0.25 | 识别端自身过滤口径(测距端不消费检测置信度,§2.4) | | `iou_threshold` | 0.7 | NMS IoU 门限 | | `class_whitelist` | `null` | 可选白名单;同一目标左右目类别必须一致(§2.2 第 5 条) | | `device` | `null` | null = 自动选择(`cpu` / `cuda:0` / …) | | `concurrency` | `parallel` | `parallel` / `sequential` | ## 测试与演示 ```bash cd utils "D:/program file/conda/envs/pytorch/python.exe" -m pytest tests/stereo_yolo -q # 36 条 python utils/demo_stereo_yolo.py # 真模型 + 占位素材 ``` 单测全部用注入的假检测器驱动(无模型、无素材、无硬件):frame_id 左右共用、 出口自检丢弃退化框、白名单过滤、单目失败隔离、形状不一致拒收、双人屏障 断言真并发(串行实现会屏障超时)。 demo(真模型 + AI 合成素材 `utils/assets/stereo_yolo/drone_sky.png`): ① 单图冒烟 → ② 合成双目对(同一 patch 羽化粘贴、已知视差 12 px,实测 |ΔD|≈0.3 px、|Δy|≈0.2 px)→ 真检测框喂 `StereoRanger.update()`,记录值与 解析值 `Z=F·B/D` 同量级 → ③ 空场景(正常空帧不告警)→ ④ 形状不一致拒收 → ⑤ 帧周期实测(台架开发机 ≈34 ms/帧,占位)。替换素材文件即可换真实图片。 ## 验收对照(规格 §6) | 项 | 状态 | | --- | --- | | 坐标回映误差 <0.5 px | 由库内完成,无法构件级单测;以合成对整体校验(|ΔD|≈0.3 px @1080p,含检测边缘噪声);C++ 移植后需实测 | | 垂直对齐 P95 ≤1 px | 合成对实测 |Δy|≈0.2 px(合成口径,非真实校正质量);待真实素材 | | 类别一致性 | 模型单类 `Drone`,合成对天然一致;待真实素材 | | 输出合法性 | ✓ 单测(非有限值/退化框构造用例) | | 输入纯净性 | ✓ 结构性保证(前端无跨帧状态) | | 下游对接 | ✓ demo 场景②;δZ ∝ Z² 趋势由测距端单测覆盖 | | 帧周期 | △ 台架开发机实测 ≈34 ms/帧(30 fps 预算 33.3 ms);待平台实测(P7) |