"""Object detection via YOLOv8, with a graceful no-ML fallback. When the optional ML stack (torch + ultralytics) is available and enabled, this detects furniture-like objects so their floor footprints can be treated as blocked space. Otherwise it returns an empty list and the floor is treated as unobstructed — the rest of the pipeline still runs. Grounding DINO is intentionally OUT for the MVP (YOLO covers detection). """ from __future__ import annotations import functools from dataclasses import dataclass, field from pathlib import Path from ..config import settings from .ml_runtime import ml_available # COCO labels that occupy floor space and matter for placement. FURNITURE_LABELS = { "bed", "couch", "chair", "dining table", "tv", "potted plant", "refrigerator", "toilet", "sink", "bench", "vase", "book", "clock", } @dataclass class Detection: label: str confidence: float box: list[float] = field(default_factory=list) # [x1, y1, x2, y2] in pixels @functools.lru_cache(maxsize=1) def _load_model(): from ultralytics import YOLO return YOLO(settings.YOLO_MODEL) def detect_objects(image_path: Path, conf_threshold: float = 0.30) -> list[Detection]: """Detect objects in an image. Returns [] when ML is unavailable/failing.""" if not ml_available(): return [] try: model = _load_model() result = model(str(image_path), verbose=False)[0] names = result.names out: list[Detection] = [] for b in result.boxes: conf = float(b.conf) if conf < conf_threshold: continue label = str(names[int(b.cls)]) xyxy = [float(v) for v in b.xyxy[0].tolist()] out.append(Detection(label=label, confidence=conf, box=xyxy)) return out except Exception: # Missing weights / runtime error -> degrade gracefully. return []