Files
keita df91b1110e 精简仓库结构并增强任务链路:归档双目可选后端、新增LOS判据与自定义返航
归档 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。
2026-10-21 14:48:41 +08:00

79 lines
2.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""UltralyticsDetector —— ultralytics YOLO .pt 直载的检测后端。
⚠ 移植注记(C++ 侧需自行复现,详见 README):letterbox(缩放 + 填充到
imgsz)、输出张量解码、NMS、框映射回输入图原生坐标系,本例由 ultralytics
库内完成;移植时必须逐项复现同一几何(步长对齐、填充方式、IoU 门限、
原生坐标回映),否则视差取点会引入系统性偏差。
"""
import queue
from pathlib import Path
from typing import List
from stereo_ranger.types import Detection
from .config import StereoYoloConfig
class UltralyticsDetector:
"""双实例池:ultralytics 单实例非线程安全,左右并发各持一份(§3/§4)。
接口约定(可注入替换):``detect(image) -> list[Detection]``,
image 为 BGR ndarray(cv2 约定),返回框位于该图原生坐标系。
"""
def __init__(self, config: StereoYoloConfig) -> None:
self._cfg = config
self._model_path = _resolve_model_path(config.model_path)
self._pool: "queue.SimpleQueue" = queue.SimpleQueue()
from ultralytics import YOLO # 延迟导入:仅真实推理付加载成本,单测注入假检测器(§6)
for _ in range(2):
self._pool.put(YOLO(str(self._model_path)))
def detect(self, image) -> List[Detection]:
model = self._pool.get()
try:
results = model.predict(
source=image,
imgsz=self._cfg.imgsz,
conf=self._cfg.conf_threshold,
iou=self._cfg.iou_threshold,
device=self._cfg.device,
verbose=False,
)
finally:
self._pool.put(model)
if not results:
return []
return _result_to_detections(results[0])
def _result_to_detections(result) -> List[Detection]:
boxes = getattr(result, "boxes", None)
if boxes is None or len(boxes) == 0:
return []
xyxy = boxes.xyxy.cpu().numpy()
conf = boxes.conf.cpu().numpy()
cls = boxes.cls.cpu().numpy()
return [
Detection(
float(box[0]),
float(box[1]),
float(box[2]),
float(box[3]),
confidence=float(score),
class_id=int(class_id),
)
for box, score, class_id in zip(xyxy, conf, cls)
]
def _resolve_model_path(raw: str) -> Path:
path = Path(raw).expanduser()
if not path.is_absolute():
path = Path.cwd() / path
if not path.is_file():
raise FileNotFoundError(f"模型权重不存在: {path}(model_path 相对路径按启动工作目录解析)")
return path.resolve()