83 lines
3.5 KiB
Python
83 lines
3.5 KiB
Python
"""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}")
|