CoolFace
Apppublic

AI-Talent-Force/dev_caio

sourceHugging Facemitupdated 9mo agoView on Hugging Face
0likes
tracker.py405 linesDownload Raw Back to models
1"""2ShortSmith v2 - Object Tracker Module3 4Multi-object tracking using ByteTrack for:5- Maintaining person identity across frames6- Handling occlusions and reappearances7- Tracking specific individuals through video8 9ByteTrack uses two-stage association for robust tracking.10"""11 12from pathlib import Path13from typing import List, Optional, Dict, Tuple, Union14from dataclasses import dataclass, field15import numpy as np16 17from utils.logger import get_logger, LogTimer18from utils.helpers import InferenceError19from config import get_config20 21logger = get_logger("models.tracker")22 23 24@dataclass25class TrackedObject:26    """Represents a tracked object across frames."""27    track_id: int                         # Unique track identifier28    bbox: Tuple[int, int, int, int]       # Current bounding box (x1, y1, x2, y2)29    confidence: float                      # Detection confidence30    class_id: int = 0                      # Object class (0 = person)31    frame_id: int = 0                      # Current frame number32 33    # Track history34    history: List[Tuple[int, int, int, int]] = field(default_factory=list)35    age: int = 0                          # Frames since first detection36    hits: int = 0                         # Number of detections37    time_since_update: int = 0            # Frames since last detection38 39    @property40    def center(self) -> Tuple[int, int]:41        x1, y1, x2, y2 = self.bbox42        return ((x1 + x2) // 2, (y1 + y2) // 2)43 44    @property45    def area(self) -> int:46        x1, y1, x2, y2 = self.bbox47        return (x2 - x1) * (y2 - y1)48 49    @property50    def is_confirmed(self) -> bool:51        """Track is confirmed after multiple detections."""52        return self.hits >= 353 54 55@dataclass56class TrackingResult:57    """Result of tracking for a single frame."""58    frame_id: int59    tracks: List[TrackedObject]60    lost_tracks: List[int]  # Track IDs lost this frame61    new_tracks: List[int]   # New track IDs this frame62 63 64class ObjectTracker:65    """66    Multi-object tracker using ByteTrack algorithm.67 68    ByteTrack features:69    - Two-stage association (high-confidence first, then low-confidence)70    - Handles occlusions by keeping lost tracks71    - Re-identifies objects after temporary disappearance72    """73 74    def __init__(75        self,76        track_thresh: float = 0.5,77        track_buffer: int = 30,78        match_thresh: float = 0.8,79    ):80        """81        Initialize tracker.82 83        Args:84            track_thresh: Detection confidence threshold for new tracks85            track_buffer: Frames to keep lost tracks86            match_thresh: IoU threshold for matching87        """88        self.track_thresh = track_thresh89        self.track_buffer = track_buffer90        self.match_thresh = match_thresh91 92        self._tracks: Dict[int, TrackedObject] = {}93        self._lost_tracks: Dict[int, TrackedObject] = {}94        self._next_id = 195        self._frame_id = 096 97        logger.info(98            f"ObjectTracker initialized (thresh={track_thresh}, "99            f"buffer={track_buffer}, match={match_thresh})"100        )101 102    def update(103        self,104        detections: List[Tuple[Tuple[int, int, int, int], float]],105    ) -> TrackingResult:106        """107        Update tracker with new detections.108 109        Args:110            detections: List of (bbox, confidence) tuples111 112        Returns:113            TrackingResult with current tracks114        """115        self._frame_id += 1116 117        if not detections:118            # No detections - age all tracks119            return self._handle_no_detections()120 121        # Separate high and low confidence detections122        high_conf = [(bbox, conf) for bbox, conf in detections if conf >= self.track_thresh]123        low_conf = [(bbox, conf) for bbox, conf in detections if conf < self.track_thresh]124 125        # First association: match high-confidence detections to active tracks126        matched, unmatched_tracks, unmatched_dets = self._associate(127            list(self._tracks.values()),128            high_conf,129            self.match_thresh,130        )131 132        # Update matched tracks133        for track_id, det_idx in matched:134            bbox, conf = high_conf[det_idx]135            self._update_track(track_id, bbox, conf)136 137        # Second association: match low-confidence to remaining tracks138        if low_conf and unmatched_tracks:139            remaining_tracks = [self._tracks[tid] for tid in unmatched_tracks]140            matched2, unmatched_tracks, _ = self._associate(141                remaining_tracks,142                low_conf,143                self.match_thresh * 0.9,  # Lower threshold144            )145 146            for track_id, det_idx in matched2:147                bbox, conf = low_conf[det_idx]148                self._update_track(track_id, bbox, conf)149 150        # Handle unmatched tracks151        lost_this_frame = []152        for track_id in unmatched_tracks:153            track = self._tracks[track_id]154            track.time_since_update += 1155 156            if track.time_since_update > self.track_buffer:157                # Remove track158                del self._tracks[track_id]159                lost_this_frame.append(track_id)160            else:161                # Move to lost tracks162                self._lost_tracks[track_id] = self._tracks.pop(track_id)163 164        # Try to recover lost tracks with unmatched detections165        recovered = self._recover_lost_tracks(166            [(high_conf[i] if i < len(high_conf) else low_conf[i - len(high_conf)])167             for i in unmatched_dets]168        )169 170        # Create new tracks for remaining detections171        new_tracks = []172        for i in unmatched_dets:173            if i not in recovered:174                det = high_conf[i] if i < len(high_conf) else low_conf[i - len(high_conf)]175                bbox, conf = det176                track_id = self._create_track(bbox, conf)177                new_tracks.append(track_id)178 179        return TrackingResult(180            frame_id=self._frame_id,181            tracks=list(self._tracks.values()),182            lost_tracks=lost_this_frame,183            new_tracks=new_tracks,184        )185 186    def _associate(187        self,188        tracks: List[TrackedObject],189        detections: List[Tuple[Tuple[int, int, int, int], float]],190        thresh: float,191    ) -> Tuple[List[Tuple[int, int]], List[int], List[int]]:192        """193        Associate detections to tracks using IoU.194 195        Returns:196            (matched_pairs, unmatched_track_ids, unmatched_detection_indices)197        """198        if not tracks or not detections:199            return [], [t.track_id for t in tracks], list(range(len(detections)))200 201        # Compute IoU matrix202        iou_matrix = np.zeros((len(tracks), len(detections)))203 204        for i, track in enumerate(tracks):205            for j, (det_bbox, _) in enumerate(detections):206                iou_matrix[i, j] = self._compute_iou(track.bbox, det_bbox)207 208        # Greedy matching209        matched = []210        unmatched_tracks = set(t.track_id for t in tracks)211        unmatched_dets = set(range(len(detections)))212 213        while True:214            # Find best match215            if iou_matrix.size == 0:216                break217 218            max_iou = np.max(iou_matrix)219            if max_iou < thresh:220                break221 222            max_idx = np.unravel_index(np.argmax(iou_matrix), iou_matrix.shape)223            track_idx, det_idx = max_idx224 225            track_id = tracks[track_idx].track_id226            matched.append((track_id, det_idx))227            unmatched_tracks.discard(track_id)228            unmatched_dets.discard(det_idx)229 230            # Remove matched row and column231            iou_matrix[track_idx, :] = -1232            iou_matrix[:, det_idx] = -1233 234        return matched, list(unmatched_tracks), list(unmatched_dets)235 236    def _compute_iou(237        self,238        bbox1: Tuple[int, int, int, int],239        bbox2: Tuple[int, int, int, int],240    ) -> float:241        """Compute IoU between two bounding boxes."""242        x1_1, y1_1, x2_1, y2_1 = bbox1243        x1_2, y1_2, x2_2, y2_2 = bbox2244 245        # Intersection246        xi1 = max(x1_1, x1_2)247        yi1 = max(y1_1, y1_2)248        xi2 = min(x2_1, x2_2)249        yi2 = min(y2_1, y2_2)250 251        if xi2 <= xi1 or yi2 <= yi1:252            return 0.0253 254        intersection = (xi2 - xi1) * (yi2 - yi1)255 256        # Union257        area1 = (x2_1 - x1_1) * (y2_1 - y1_1)258        area2 = (x2_2 - x1_2) * (y2_2 - y1_2)259        union = area1 + area2 - intersection260 261        return intersection / union if union > 0 else 0.0262 263    def _update_track(264        self,265        track_id: int,266        bbox: Tuple[int, int, int, int],267        confidence: float,268    ) -> None:269        """Update an existing track."""270        track = self._tracks.get(track_id) or self._lost_tracks.get(track_id)271 272        if track is None:273            return274 275        # Move from lost to active if needed276        if track_id in self._lost_tracks:277            self._tracks[track_id] = self._lost_tracks.pop(track_id)278 279        track = self._tracks[track_id]280        track.history.append(track.bbox)281        track.bbox = bbox282        track.confidence = confidence283        track.frame_id = self._frame_id284        track.hits += 1285        track.time_since_update = 0286 287    def _create_track(288        self,289        bbox: Tuple[int, int, int, int],290        confidence: float,291    ) -> int:292        """Create a new track."""293        track_id = self._next_id294        self._next_id += 1295 296        track = TrackedObject(297            track_id=track_id,298            bbox=bbox,299            confidence=confidence,300            frame_id=self._frame_id,301            age=1,302            hits=1,303        )304 305        self._tracks[track_id] = track306        logger.debug(f"Created new track {track_id}")307        return track_id308 309    def _recover_lost_tracks(310        self,311        detections: List[Tuple[Tuple[int, int, int, int], float]],312    ) -> set:313        """Try to recover lost tracks with unmatched detections."""314        recovered = set()315 316        if not self._lost_tracks or not detections:317            return recovered318 319        for det_idx, (bbox, conf) in enumerate(detections):320            best_iou = 0321            best_track_id = None322 323            for track_id, track in self._lost_tracks.items():324                iou = self._compute_iou(track.bbox, bbox)325                if iou > best_iou and iou > self.match_thresh * 0.7:326                    best_iou = iou327                    best_track_id = track_id328 329            if best_track_id is not None:330                self._update_track(best_track_id, bbox, conf)331                recovered.add(det_idx)332                logger.debug(f"Recovered track {best_track_id}")333 334        return recovered335 336    def _handle_no_detections(self) -> TrackingResult:337        """Handle frame with no detections."""338        lost_this_frame = []339 340        for track_id in list(self._tracks.keys()):341            track = self._tracks[track_id]342            track.time_since_update += 1343 344            if track.time_since_update > self.track_buffer:345                del self._tracks[track_id]346                lost_this_frame.append(track_id)347            else:348                self._lost_tracks[track_id] = self._tracks.pop(track_id)349 350        return TrackingResult(351            frame_id=self._frame_id,352            tracks=list(self._tracks.values()),353            lost_tracks=lost_this_frame,354            new_tracks=[],355        )356 357    def get_track(self, track_id: int) -> Optional[TrackedObject]:358        """Get a specific track by ID."""359        return self._tracks.get(track_id) or self._lost_tracks.get(track_id)360 361    def get_active_tracks(self) -> List[TrackedObject]:362        """Get all active tracks."""363        return list(self._tracks.values())364 365    def get_confirmed_tracks(self) -> List[TrackedObject]:366        """Get only confirmed tracks (multiple detections)."""367        return [t for t in self._tracks.values() if t.is_confirmed]368 369    def reset(self) -> None:370        """Reset tracker state."""371        self._tracks.clear()372        self._lost_tracks.clear()373        self._frame_id = 0374        logger.info("Tracker reset")375 376    def get_track_for_target(377        self,378        target_bbox: Tuple[int, int, int, int],379        threshold: float = 0.5,380    ) -> Optional[int]:381        """382        Find track that best matches a target bounding box.383 384        Args:385            target_bbox: Target bounding box to match386            threshold: Minimum IoU for match387 388        Returns:389            Track ID if found, None otherwise390        """391        best_iou = 0392        best_track = None393 394        for track in self._tracks.values():395            iou = self._compute_iou(track.bbox, target_bbox)396            if iou > best_iou and iou > threshold:397                best_iou = iou398                best_track = track.track_id399 400        return best_track401 402 403# Export public interface404__all__ = ["ObjectTracker", "TrackedObject", "TrackingResult"]405