124 lines
5.3 KiB
Python
124 lines
5.3 KiB
Python
"""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}")
|