双目测距方案重构与其他小修改
This commit is contained in:
@@ -1,50 +1,51 @@
|
||||
"""StereoConfig 与 JSON 配置加载。
|
||||
"""StereoConfig 与 JSON 配置加载(§9 配置参数全量表)。
|
||||
|
||||
规格 docs/双目测距方案.md:全部数值参数均为配置项(§2),默认值取文档
|
||||
示例值。配置校验在构造期一次性完成,热路径不读 JSON。
|
||||
全部参数配置外置、构造期一次加载并校验:缺项、类型错误、非法取值一律拒绝
|
||||
进入运行期(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 pathlib import Path
|
||||
from typing import Any, Dict, Tuple
|
||||
from typing import Any, Dict
|
||||
|
||||
# JSON 加载时的整数型字段(其余按数值校验)
|
||||
_INT_FIELDS = frozenset({"consistency_N_frames", "consistency_M_frames", "filter_reset_frames"})
|
||||
|
||||
|
||||
@dataclass
|
||||
class StereoConfig:
|
||||
# ---- 相机参数(校正后、对应原生分辨率,§3.1 / §4.3)----
|
||||
# ---- 几何组(来自标定 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 # 基线 B
|
||||
baseline_m: float = 0.06
|
||||
|
||||
# ---- 目标距离窗口:关联硬约束(§6),派生视差区间 [FB/z_max, FB/z_min] ----
|
||||
z_min_m: float = 1.0
|
||||
z_max_m: float = 50.0
|
||||
# ---- 匹配组(§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
|
||||
|
||||
# ---- 关联阈值(§6)----
|
||||
vertical_tau_px: float = 2.0 # 垂直残差 tau_y
|
||||
height_ratio_tau: float = 0.25 # 相对高度差 tau_h
|
||||
# ---- 有效性组(§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 的视差跳变门限
|
||||
|
||||
# ---- 视差取点与有效性(§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)
|
||||
# ---- 滤波组(§9.4)----
|
||||
smooth_alpha: float = 0.3
|
||||
jump_reset_m: float = 5.0
|
||||
filter_reset_frames: int = 5
|
||||
|
||||
# ---- 可选时间滤波(§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
|
||||
# ---- 目标隔离(§4:类别 + 框位置最近邻;⚠补充项)----
|
||||
isolation_gate_px: float = 50.0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require(self.fx_px > 0.0, "fx_px", "必须为正")
|
||||
@@ -52,31 +53,71 @@ class StereoConfig:
|
||||
_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", "必须非负")
|
||||
_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 f:
|
||||
data: Dict[str, Any] = json.load(f)
|
||||
known = {field.name for field in fields(StereoConfig)}
|
||||
unknown = set(data) - known
|
||||
"""从 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"未知配置字段: {sorted(unknown)}")
|
||||
missing = known - set(data)
|
||||
raise ValueError(f"未知配置字段: {unknown}")
|
||||
missing = sorted(known - set(data))
|
||||
if missing:
|
||||
raise ValueError(f"缺少配置字段: {sorted(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(ok: bool, field_name: str, message: str) -> None:
|
||||
if not ok:
|
||||
def _require(condition: bool, field_name: str, message: str) -> None:
|
||||
if not condition:
|
||||
raise ValueError(f"配置非法: {field_name} {message}")
|
||||
|
||||
Reference in New Issue
Block a user