"""StereoConfig 与 JSON 配置加载(§9 配置参数全量表)。 全部参数配置外置、构造期一次加载并校验:缺项、类型错误、非法取值一律拒绝 进入运行期(fail-fast)。所有默认值均为 ⚠占位值,未整定前不得用于飞行决策 (§12.3 红线 1/2)。 域权威(实现决策):视差域 disparity_min_px / disparity_max_px 为权威, 距离域 z_min_m / z_max_m 由 Z = F·B/D 派生为只读属性——域判定使用视差 (§11.2 直接观测量),两套阈值不再冗余存储、避免互推舍入不一致(§6.4)。 """ import json import math from dataclasses import dataclass, fields from typing import Any, Dict # JSON 加载时的整数型字段(其余按数值校验) _INT_FIELDS = frozenset({"consistency_N_frames", "consistency_M_frames", "filter_reset_frames"}) @dataclass class StereoConfig: # ---- 几何组(来自标定 P1/P2,§9.1;⚠全部为占位值)---- fx_px: float = 960.0 fy_px: float = 960.0 cx_px: float = 960.0 cy_px: float = 540.0 baseline_m: float = 0.06 # ---- 匹配组(§9.2)---- tau_y_px: float = 2.0 tau_h_ratio: float = 0.25 disparity_min_px: float = 3.0 disparity_max_px: float = 57.6 # ⚠占位:F=960/B=0.06 下 ⇔ 近界 1.0 m # ---- 有效性组(§9.3)---- max_delta_z_m: float = 2.0 # 误差预算 δZ(δD = 1px) 上限 consistency_N_frames: int = 3 # 连续跳变降置信度的帧数 consistency_M_frames: int = 5 # 连续异常判 invalid 的帧数 consistency_jump_px: float = 3.0 # ⚠补充项:§9 未列,§6.7 的视差跳变门限 # ---- 滤波组(§9.4)---- smooth_alpha: float = 0.3 jump_reset_m: float = 5.0 filter_reset_frames: int = 5 # ---- 目标隔离(§4:类别 + 框位置最近邻;⚠补充项)---- isolation_gate_px: float = 50.0 def __post_init__(self) -> None: _require(self.fx_px > 0.0, "fx_px", "必须为正") _require(self.fy_px > 0.0, "fy_px", "必须为正") _require(self.cx_px >= 0.0, "cx_px", "必须非负") _require(self.cy_px >= 0.0, "cy_px", "必须非负") _require(self.baseline_m > 0.0, "baseline_m", "必须为正") _require(self.tau_y_px > 0.0, "tau_y_px", "必须为正") _require(self.tau_h_ratio > 0.0, "tau_h_ratio", "必须为正") _require( 0.0 < self.disparity_min_px < self.disparity_max_px, "disparity_min_px/disparity_max_px", "须满足 0 < 下限 < 上限", ) _require(self.max_delta_z_m > 0.0, "max_delta_z_m", "必须为正") _require(self.consistency_N_frames >= 1, "consistency_N_frames", "必须 ≥ 1") _require( self.consistency_M_frames >= self.consistency_N_frames, "consistency_M_frames", "必须 ≥ consistency_N_frames", ) _require(self.consistency_jump_px > 0.0, "consistency_jump_px", "必须为正") _require(0.0 < self.smooth_alpha <= 1.0, "smooth_alpha", "须落在 (0, 1]") _require(self.jump_reset_m > 0.0, "jump_reset_m", "必须为正") _require(self.filter_reset_frames >= 1, "filter_reset_frames", "必须 ≥ 1") _require(self.isolation_gate_px > 0.0, "isolation_gate_px", "必须为正") @property def focal_baseline_px_m(self) -> float: """F·B:构造期一次算好的常量,热路径只做乘除(§11.1)。""" return self.fx_px * self.baseline_m @property def z_min_m(self) -> float: """近界(由 disparity_max_px 派生)。""" return self.focal_baseline_px_m / self.disparity_max_px @property def z_max_m(self) -> float: """远界(由 disparity_min_px 派生)。""" return self.focal_baseline_px_m / self.disparity_min_px @property def z_error_budget_max_m(self) -> float: """误差预算门限对应的最远距离 sqrt(δZ_max·F·B)(δD = 1px,§7.2)。""" return math.sqrt(self.max_delta_z_m * self.focal_baseline_px_m) def load_from_json(path: str) -> StereoConfig: """从 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(StereoConfig)} 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 isinstance(value, bool): raise ValueError(f"配置类型非法: {name} 须为数值") if name in _INT_FIELDS: if not isinstance(value, int): raise ValueError(f"配置类型非法: {name} 须为整数") elif not isinstance(value, (int, float)): raise ValueError(f"配置类型非法: {name} 须为数值") return StereoConfig(**data) def _require(condition: bool, field_name: str, message: str) -> None: if not condition: raise ValueError(f"配置非法: {field_name} {message}")