Spaces:
Sleeping
Sleeping
| """ByteTrack multi-object tracking via the supervision library. | |
| Wraps supervision.ByteTrack to consume Detection objects and produce | |
| Track objects with stable IDs across frames. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| import numpy as np | |
| import supervision as sv | |
| from core.detector import Detection, CLASS_NAMES | |
| class Track: | |
| """A detected+tracked vehicle with a stable ID across frames.""" | |
| track_id: int | |
| bbox: tuple[float, float, float, float] # x1, y1, x2, y2 | |
| confidence: float | |
| class_id: int | |
| class_name: str = field(init=False) | |
| def __post_init__(self) -> None: | |
| self.class_name = CLASS_NAMES[self.class_id] if self.class_id < len(CLASS_NAMES) else "unknown" | |
| def center(self) -> tuple[float, float]: | |
| x1, y1, x2, y2 = self.bbox | |
| return (x1 + x2) / 2, (y1 + y2) / 2 | |
| def xywh(self) -> tuple[float, float, float, float]: | |
| x1, y1, x2, y2 = self.bbox | |
| return x1, y1, x2 - x1, y2 - y1 | |
| class Tracker: | |
| """ByteTrack wrapper. | |
| Usage: | |
| tracker = Tracker() | |
| for frame, meta in source.stream(): | |
| detections = detector.detect(frame) | |
| tracks = tracker.update(detections) | |
| """ | |
| def __init__( | |
| self, | |
| *, | |
| track_activation_threshold: float = 0.25, | |
| lost_track_buffer: int = 30, | |
| minimum_matching_threshold: float = 0.8, | |
| frame_rate: int = 25, | |
| ) -> None: | |
| self._byte_tracker = sv.ByteTrack( | |
| track_activation_threshold=track_activation_threshold, | |
| lost_track_buffer=lost_track_buffer, | |
| minimum_matching_threshold=minimum_matching_threshold, | |
| frame_rate=frame_rate, | |
| ) | |
| def update(self, detections: list[Detection]) -> list[Track]: | |
| """Feed new detections, get back tracks with stable IDs.""" | |
| if not detections: | |
| sv_dets = sv.Detections.empty() | |
| else: | |
| sv_dets = self._to_sv_detections(detections) | |
| tracked = self._byte_tracker.update_with_detections(sv_dets) | |
| return self._from_sv_detections(tracked) | |
| def reset(self) -> None: | |
| """Reset tracker state (call when switching video source).""" | |
| self._byte_tracker.reset() | |
| # ββ Conversion helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _to_sv_detections(detections: list[Detection]) -> sv.Detections: | |
| xyxy = np.array([d.bbox for d in detections], dtype=np.float32) | |
| confidence = np.array([d.confidence for d in detections], dtype=np.float32) | |
| class_id = np.array([d.class_id for d in detections], dtype=int) | |
| return sv.Detections(xyxy=xyxy, confidence=confidence, class_id=class_id) | |
| def _from_sv_detections(sv_dets: sv.Detections) -> list[Track]: | |
| if sv_dets.tracker_id is None or len(sv_dets) == 0: | |
| return [] | |
| tracks = [] | |
| for i in range(len(sv_dets)): | |
| x1, y1, x2, y2 = sv_dets.xyxy[i].tolist() | |
| tracks.append( | |
| Track( | |
| track_id=int(sv_dets.tracker_id[i]), | |
| bbox=(x1, y1, x2, y2), | |
| confidence=float(sv_dets.confidence[i]) if sv_dets.confidence is not None else 1.0, | |
| class_id=int(sv_dets.class_id[i]) if sv_dets.class_id is not None else 0, | |
| ) | |
| ) | |
| return tracks | |