"""StereoYoloConfig 与 JSON 配置加载(需求说明 §4 配置项需求)。 全部配置外置、构造期一次加载并校验:缺项、未知字段、类型错误、非法取值 一律拒绝进入运行期(fail-fast)。几何参数(F/B/cx/cy/原分辨率)不在此 重复存储——输出框位于**输入图像自身的原生坐标系**,几何参数的唯一来源 是 utils/config/stereo_ranger.json(§4 配置一致性要求:同一配置源驱动, 杜绝两处漂移)。 """ import json from dataclasses import dataclass, fields from typing import Any, Dict, Optional, Tuple _CONCURRENCY_MODES = ("parallel", "sequential") _INT_FIELDS = frozenset({"imgsz"}) _STR_FIELDS = frozenset({"model_path", "concurrency"}) @dataclass class StereoYoloConfig: model_path: str = "models/yolo26n-drone.pt" imgsz: int = 640 # 左右共用(§2.2 第 2 条:同一缩放、同一填充) conf_threshold: float = 0.25 # ⚠占位:识别端自身过滤口径(§2.4) iou_threshold: float = 0.7 # ⚠占位:NMS IoU 门限 class_whitelist: Optional[Tuple[int, ...]] = None # None = 不过滤(§4 可选白名单) device: Optional[str] = None # None = 自动选择(cpu / cuda:0 / …) concurrency: str = "parallel" # parallel = 左右并发(§5 硬性要求,串行会吃掉全部预算) def __post_init__(self) -> None: _require(bool(self.model_path.strip()), "model_path", "不得为空") _require( self.imgsz >= 32 and self.imgsz % 32 == 0, "imgsz", "须为 32 的整数倍且 ≥ 32(YOLO 步长约束)", ) _require(0.0 <= self.conf_threshold <= 1.0, "conf_threshold", "须落在 [0, 1]") _require(0.0 < self.iou_threshold <= 1.0, "iou_threshold", "须落在 (0, 1]") _require(self.concurrency in _CONCURRENCY_MODES, "concurrency", f"须为 {_CONCURRENCY_MODES} 之一") _require(self.device is None or bool(self.device.strip()), "device", "须为非空字符串或 null") if self.class_whitelist is not None: ids = tuple(self.class_whitelist) _require( len(ids) > 0 and all(isinstance(item, int) and not isinstance(item, bool) and item >= 0 for item in ids), "class_whitelist", "须为非空非负整数列表或 null", ) self.class_whitelist = ids def load_from_json(path: str) -> StereoYoloConfig: """从 JSON 文件构建配置;缺项/未知字段/类型错误/非法取值一律抛 ValueError。""" with open(path, encoding="utf-8") as handle: data: Dict[str, Any] = json.load(handle) if not isinstance(data, dict): raise ValueError("配置非法: 顶层须为 JSON 对象") known = {item.name for item in fields(StereoYoloConfig)} unknown = sorted(set(data) - known) if unknown: raise ValueError(f"未知配置字段: {unknown}") missing = sorted(known - set(data)) if missing: raise ValueError(f"缺少配置字段: {missing}") for name, value in data.items(): if name == "class_whitelist": if value is not None and not ( isinstance(value, list) and all(isinstance(item, int) and not isinstance(item, bool) for item in value) ): raise ValueError("配置类型非法: class_whitelist 须为整数数组或 null") elif name == "device": if value is not None and not isinstance(value, str): raise ValueError("配置类型非法: device 须为字符串或 null") elif name in _STR_FIELDS: if not isinstance(value, str): raise ValueError(f"配置类型非法: {name} 须为字符串") elif name in _INT_FIELDS: if not isinstance(value, int) or isinstance(value, bool): raise ValueError(f"配置类型非法: {name} 须为整数") elif isinstance(value, bool) or not isinstance(value, (int, float)): raise ValueError(f"配置类型非法: {name} 须为数值") return StereoYoloConfig(**data) def _require(condition: bool, field_name: str, message: str) -> None: if not condition: raise ValueError(f"配置非法: {field_name} {message}")