tiny-trigger / tiny_trigger /detector.py
Javier
UI Functional, next fixing automations
d725335
Raw History Blame
4.49 kB
from __future__ import annotations
import re
from typing import Any, Protocol
from .models import Detection
def parse_class_prompt(class_prompt: str | list[str] | tuple[str, ...]) -> list[str]:
if isinstance(class_prompt, (list, tuple)):
parts = [str(item) for item in class_prompt]
else:
parts = re.split(r"[,;\n]+", class_prompt)
seen: set[str] = set()
classes: list[str] = []
for part in parts:
label = " ".join(part.strip().split())
key = label.lower()
if label and key not in seen:
seen.add(key)
classes.append(label)
return classes
class Detector(Protocol):
class_names: list[str]
def detect(
self,
frame: Any,
*,
frame_index: int,
timestamp_sec: float,
confidence: float,
image_size: int | None = None,
max_detections: int | None = None,
) -> list[Detection]:
...
class UltralyticsYOLOEDetector:
def __init__(
self,
*,
class_names: list[str],
model_name: str = "yoloe-26s-seg.pt",
device: str | None = None,
) -> None:
if not class_names:
raise ValueError("YOLOE needs at least one open-vocabulary class.")
try:
from ultralytics import YOLOE
except ImportError as exc: # pragma: no cover - optional heavy dependency
raise RuntimeError("Install ultralytics to use the YOLOE detector.") from exc
self.class_names = class_names
self.model_name = model_name
self.device = device
self.model = YOLOE(model_name)
self.model.set_classes(class_names)
def detect(
self,
frame: Any,
*,
frame_index: int,
timestamp_sec: float,
confidence: float,
image_size: int | None = None,
max_detections: int | None = None,
) -> list[Detection]:
kwargs: dict[str, Any] = {"conf": confidence, "verbose": False}
if self.device:
kwargs["device"] = self.device
if image_size:
kwargs["imgsz"] = image_size
if max_detections:
kwargs["max_det"] = max_detections
results = self.model.predict(frame, **kwargs)
if not results:
return []
return detections_from_ultralytics_result(
results[0],
frame_shape=frame.shape,
frame_index=frame_index,
timestamp_sec=timestamp_sec,
fallback_names=self.class_names,
)
def detections_from_ultralytics_result(
result: Any,
*,
frame_shape: tuple[int, int, int],
frame_index: int,
timestamp_sec: float,
fallback_names: list[str],
) -> list[Detection]:
boxes = getattr(result, "boxes", None)
if boxes is None or boxes.xyxy is None:
return []
height, width = frame_shape[:2]
xyxy_values = boxes.xyxy.cpu().tolist()
confidences = boxes.conf.cpu().tolist()
class_ids = boxes.cls.cpu().tolist()
names = getattr(result, "names", {}) or {}
detections: list[Detection] = []
for bbox, score, class_id in zip(xyxy_values, confidences, class_ids):
label = _label_from_names(names, int(class_id), fallback_names)
detections.append(
Detection(
frame_index=frame_index,
timestamp_sec=timestamp_sec,
label=label,
confidence=float(score),
bbox_xyxy=tuple(float(value) for value in bbox),
bbox_xyxy_norm=_normalize_box(bbox, width, height),
)
)
return detections
def _label_from_names(names: Any, class_id: int, fallback_names: list[str]) -> str:
if isinstance(names, dict) and class_id in names:
return str(names[class_id])
if isinstance(names, list) and 0 <= class_id < len(names):
return str(names[class_id])
if 0 <= class_id < len(fallback_names):
return fallback_names[class_id]
return f"class_{class_id}"
def _normalize_box(bbox: list[float], width: int, height: int) -> tuple[float, float, float, float]:
x1, y1, x2, y2 = bbox
if width <= 0 or height <= 0:
return (0.0, 0.0, 0.0, 0.0)
return (
_clamp01(float(x1) / width),
_clamp01(float(y1) / height),
_clamp01(float(x2) / width),
_clamp01(float(y2) / height),
)
def _clamp01(value: float) -> float:
return max(0.0, min(1.0, value))