Source code for pose.human_pose

"""
YOLOv8-pose backend for real-time human pose estimation.

Recommended model weights by speed/accuracy trade-off:
  yolov8n-pose.pt  — nano,   ~4 MB,  fastest,  good for CPU preview (≥15 fps)
  yolov8s-pose.pt  — small,  ~24 MB, balanced  (CPU ≥8 fps, GPU ≥60 fps)
  yolov8m-pose.pt  — medium, ~52 MB, accurate  (GPU recommended)
  yolov8l-pose.pt  — large,  ~87 MB, best      (GPU required)

Install: pip install ultralytics
"""

from __future__ import annotations

import time

import numpy as np

from .keypoints import PoseResult, SubjectPose

# ── lazy import: ultralytics is optional ─────────────────────────────────────
try:
    from ultralytics import YOLO as _YOLO

    _ULTRALYTICS_OK = True
except ImportError:
    _ULTRALYTICS_OK = False
    _YOLO = None  # type: ignore[assignment]


[docs] class HumanPoseEstimator: """Wraps YOLOv8-pose for single-frame inference. Parameters ---------- model_name : str, default "yolov8n-pose.pt" YOLOv8 model variant. Downloaded automatically from the Ultralytics CDN on first use (~4-87 MB). device : str or None, default None Inference device — ``"cpu"``, ``"cuda:0"``, ``"mps"`` (Apple Silicon). ``None`` auto-detects (prefers CUDA → MPS → CPU). conf_threshold : float, default 0.40 Minimum detection confidence to include a subject. iou_threshold : float, default 0.70 NMS IoU threshold. Raises ------ ImportError If ``ultralytics`` is not installed. """ def __init__( self, model_name: str = "yolov8n-pose.pt", device: str | None = None, conf_threshold: float = 0.40, iou_threshold: float = 0.70, ) -> None: if not _ULTRALYTICS_OK: raise ImportError("ultralytics is not installed. Run: pip install ultralytics") self._conf = conf_threshold self._iou = iou_threshold self._device = device or self._auto_device() print(f"[HumanPoseEstimator] Loading {model_name} on {self._device} …", flush=True) self._model = _YOLO(model_name) # Warm-up pass (avoids slow first real inference) dummy = np.zeros((320, 320, 3), dtype=np.uint8) self._model(dummy, verbose=False, device=self._device) print("[HumanPoseEstimator] Ready.", flush=True) # ── Public API ────────────────────────────────────────────────────────────
[docs] def infer( self, frame: np.ndarray, frame_index: int = 0, timestamp_ns: int = 0, camera_index: int = 0, ) -> PoseResult: """Run pose estimation on one BGR frame. Parameters ---------- frame : numpy.ndarray BGR frame, as returned by ``cv2.imread``/``cv2.VideoCapture``. frame_index : int, default 0 Caller-supplied frame index, echoed into the result. timestamp_ns : int, default 0 Caller-supplied timestamp (ns), echoed into the result. camera_index : int, default 0 Caller-supplied camera index, echoed into the result. Returns ------- PoseResult Structured result containing per-subject keypoints, echoing ``frame_index``/``timestamp_ns``/``camera_index`` back unchanged and reporting this call's own ``inference_ms``. """ t0 = time.perf_counter() results = self._model( frame, conf=self._conf, iou=self._iou, device=self._device, verbose=False, ) inference_ms = (time.perf_counter() - t0) * 1000.0 subjects: list[SubjectPose] = [] for res in results: if res.keypoints is None: continue kpts_xy = res.keypoints.xy.cpu().numpy() # (N, 17, 2) kpts_conf = res.keypoints.conf # may be None boxes = res.boxes for i, kxy in enumerate(kpts_xy): vis: list[float] if kpts_conf is not None: vis = kpts_conf[i].cpu().numpy().tolist() else: vis = [1.0] * kxy.shape[0] det_conf = float(boxes.conf[i].cpu()) if boxes is not None else 1.0 bbox = (0.0, 0.0, 0.0, 0.0) if boxes is not None: b = boxes.xyxy[i].cpu().numpy() bbox = (float(b[0]), float(b[1]), float(b[2]), float(b[3])) subjects.append( SubjectPose( subject_id=i, confidence=det_conf, keypoints=[(float(pt[0]), float(pt[1])) for pt in kxy], visibilities=vis, bbox_xyxy=bbox, ) ) return PoseResult( frame_index=frame_index, timestamp_ns=timestamp_ns, camera_index=camera_index, subjects=subjects, backend=f"yolov8-pose/{self._model.ckpt_path}", inference_ms=inference_ms, )
# ── Helpers ─────────────────────────────────────────────────────────────── @staticmethod def _auto_device() -> str: try: import torch if torch.cuda.is_available(): return "cuda:0" if torch.backends.mps.is_available(): return "mps" except ImportError: pass return "cpu"