精简仓库结构并增强任务链路:归档双目可选后端、新增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。
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user