"""StereoConfig 与 JSON 配置加载。 规格 docs/双目测距方案.md:全部数值参数均为配置项(§2),默认值取文档 示例值。配置校验在构造期一次性完成,热路径不读 JSON。 """ import json from dataclasses import dataclass, fields from pathlib import Path from typing import Any, Dict, Tuple @dataclass class StereoConfig: # ---- 相机参数(校正后、对应原生分辨率,§3.1 / §4.3)---- 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 # 基线 B # ---- 目标距离窗口:关联硬约束(§6),派生视差区间 [FB/z_max, FB/z_min] ---- z_min_m: float = 1.0 z_max_m: float = 50.0 # ---- 关联阈值(§6)---- vertical_tau_px: float = 2.0 # 垂直残差 tau_y height_ratio_tau: float = 0.25 # 相对高度差 tau_h # ---- 视差取点与有效性(§5)---- disparity_min_px: float = 3.0 # D_min:D 低于此值直接无效 range_tol_m: float = 2.0 # Z_tol:δZ <= Z_tol 才算有效 disp_noise_px: float = 1.0 # δD(误差模型中的边缘定位噪声) use_center_fallback: bool = True # 左边缘视差越窗时回退中心(§5.1) # ---- 可选时间滤波(§7,一阶低通 + 跳变重置)---- filter_enabled: bool = False filter_alpha: float = 0.3 filter_jump_ratio: float = 0.2 # 相对跳变超此门限则重置滤波器 # ---- 诊断日志节流 ---- warn_interval_s: float = 5.0 def disparity_bounds_px(self) -> Tuple[float, float]: """关联用的视差区间 (lo, hi):由 z_min/z_max 派生(§6)。""" fb = self.fx_px * self.baseline_m return fb / self.z_max_m, fb / self.z_min_m 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(0.0 < self.z_min_m < self.z_max_m, "z_min_m/z_max_m", "须满足 0 < z_min_m < z_max_m") _require(self.vertical_tau_px > 0.0, "vertical_tau_px", "必须为正") _require(self.height_ratio_tau > 0.0, "height_ratio_tau", "必须为正") _require(self.disparity_min_px > 0.0, "disparity_min_px", "必须为正") _require(self.range_tol_m > 0.0, "range_tol_m", "必须为正") _require(self.disp_noise_px > 0.0, "disp_noise_px", "必须为正") _require(0.0 < self.filter_alpha <= 1.0, "filter_alpha", "须落在 (0, 1]") _require(self.filter_jump_ratio >= 0.0, "filter_jump_ratio", "必须非负") _require(self.warn_interval_s >= 0.0, "warn_interval_s", "必须非负") def load_from_json(path: str) -> StereoConfig: """从 JSON 文件构建配置。字段缺失或非法取值抛 ValueError(携带字段名)。""" with open(path, encoding="utf-8") as f: data: Dict[str, Any] = json.load(f) known = {field.name for field in fields(StereoConfig)} unknown = set(data) - known if unknown: raise ValueError(f"未知配置字段: {sorted(unknown)}") missing = known - set(data) if missing: raise ValueError(f"缺少配置字段: {sorted(missing)}") return StereoConfig(**data) def _require(ok: bool, field_name: str, message: str) -> None: if not ok: raise ValueError(f"配置非法: {field_name} {message}")