双目测距方案重构与其他小修改

This commit is contained in:
keita
2026-09-10 16:16:05 +08:00
parent 3325465359
commit 538ae4c6c7
39 changed files with 2285 additions and 1444 deletions
+90 -49
View File
@@ -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}")