87 lines
4.2 KiB
Python
87 lines
4.2 KiB
Python
"""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}")
|