归档 legacy/:stereo_yolo/stereo_ranger/target_tracker 三包 + 175 条测试 + demo/配置/权重/三份规格;运行链路不再依赖,默认 pytest 446 条,归档测试按需运行。 目标有效性第二判据改为 LOS 运动学一致性(pod_bridge/los_consistency.py),删除 require_vision;pod_locked() 叠加该判据,状态机随之后退重搜。 自定义返航(task_runner/return_home.py):保持转场高度回起飞点 → 恢复初始航向 → 垂直降落,不再爬 RTL_RETURN_ALT;未起飞/无起飞点/超时/异常仍退回 PX4 原生 RTL。 任务链安全兜底:主循环异常→紧急返航、stop 幂等、landing_stop 保留遥测、in_air 流复用+超时、解锁/起飞与吊舱识别仅执行一次、rtl_timeout 上限。 入口共享件拆到 utils/sitl_support.py;pod_sitl_test.py→l1_sitl_test.py;删除旧架构 test.py;相机脚本移入 tools/。 新增 .gitignore 并剔除历史误跟踪的 59 个 .pyc;requirements 按实际依赖重写;文档全面对齐,新增 legacy/README.md 与 tools/README.md。
87 lines
4.2 KiB
Python
87 lines
4.2 KiB
Python
"""StereoYoloConfig 与 JSON 配置加载(需求说明 §4 配置项需求)。
|
||
|
||
全部配置外置、构造期一次加载并校验:缺项、未知字段、类型错误、非法取值
|
||
一律拒绝进入运行期(fail-fast)。几何参数(F/B/cx/cy/原分辨率)不在此
|
||
重复存储——输出框位于**输入图像自身的原生坐标系**,几何参数的唯一来源
|
||
是 utils/config/stereo_ranger.json(§4 配置一致性要求:同一配置源驱动,
|
||
杜绝两处漂移)。
|
||
"""
|
||
|
||
import json
|
||
from dataclasses import dataclass, fields
|
||
from typing import Any, Dict, Optional, Tuple
|
||
|
||
_CONCURRENCY_MODES = ("parallel", "sequential")
|
||
_INT_FIELDS = frozenset({"imgsz"})
|
||
_STR_FIELDS = frozenset({"model_path", "concurrency"})
|
||
|
||
|
||
@dataclass
|
||
class StereoYoloConfig:
|
||
model_path: str = "models/yolo26n-drone.pt"
|
||
imgsz: int = 640 # 左右共用(§2.2 第 2 条:同一缩放、同一填充)
|
||
conf_threshold: float = 0.25 # ⚠占位:识别端自身过滤口径(§2.4)
|
||
iou_threshold: float = 0.7 # ⚠占位:NMS IoU 门限
|
||
class_whitelist: Optional[Tuple[int, ...]] = None # None = 不过滤(§4 可选白名单)
|
||
device: Optional[str] = None # None = 自动选择(cpu / cuda:0 / …)
|
||
concurrency: str = "parallel" # parallel = 左右并发(§5 硬性要求,串行会吃掉全部预算)
|
||
|
||
def __post_init__(self) -> None:
|
||
_require(bool(self.model_path.strip()), "model_path", "不得为空")
|
||
_require(
|
||
self.imgsz >= 32 and self.imgsz % 32 == 0,
|
||
"imgsz",
|
||
"须为 32 的整数倍且 ≥ 32(YOLO 步长约束)",
|
||
)
|
||
_require(0.0 <= self.conf_threshold <= 1.0, "conf_threshold", "须落在 [0, 1]")
|
||
_require(0.0 < self.iou_threshold <= 1.0, "iou_threshold", "须落在 (0, 1]")
|
||
_require(self.concurrency in _CONCURRENCY_MODES, "concurrency", f"须为 {_CONCURRENCY_MODES} 之一")
|
||
_require(self.device is None or bool(self.device.strip()), "device", "须为非空字符串或 null")
|
||
if self.class_whitelist is not None:
|
||
ids = tuple(self.class_whitelist)
|
||
_require(
|
||
len(ids) > 0 and all(isinstance(item, int) and not isinstance(item, bool) and item >= 0 for item in ids),
|
||
"class_whitelist",
|
||
"须为非空非负整数列表或 null",
|
||
)
|
||
self.class_whitelist = ids
|
||
|
||
|
||
def load_from_json(path: str) -> StereoYoloConfig:
|
||
"""从 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(StereoYoloConfig)}
|
||
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 name == "class_whitelist":
|
||
if value is not None and not (
|
||
isinstance(value, list)
|
||
and all(isinstance(item, int) and not isinstance(item, bool) for item in value)
|
||
):
|
||
raise ValueError("配置类型非法: class_whitelist 须为整数数组或 null")
|
||
elif name == "device":
|
||
if value is not None and not isinstance(value, str):
|
||
raise ValueError("配置类型非法: device 须为字符串或 null")
|
||
elif name in _STR_FIELDS:
|
||
if not isinstance(value, str):
|
||
raise ValueError(f"配置类型非法: {name} 须为字符串")
|
||
elif name in _INT_FIELDS:
|
||
if not isinstance(value, int) or isinstance(value, bool):
|
||
raise ValueError(f"配置类型非法: {name} 须为整数")
|
||
elif isinstance(value, bool) or not isinstance(value, (int, float)):
|
||
raise ValueError(f"配置类型非法: {name} 须为数值")
|
||
return StereoYoloConfig(**data)
|
||
|
||
|
||
def _require(condition: bool, field_name: str, message: str) -> None:
|
||
if not condition:
|
||
raise ValueError(f"配置非法: {field_name} {message}")
|