bank-thief-detection / src /tracker.py
dineth554's picture
Upload src/tracker.py with huggingface_hub
f8dfaee verified
Raw
History Blame Contribute Delete
13.1 kB
"""
Lightweight ByteTrack Tracker for multi-person tracking.
Implements KalmanFilter-based tracking with IoU + confidence matching.
Track lifecycle: tentative (new) -> confirmed (3+ hits) -> removed (30 lost frames).
"""
from typing import List, Dict, Optional, Tuple
import numpy as np
from scipy.optimize import linear_sum_assignment
from collections import defaultdict
class KalmanFilter:
"""Simple Kalman filter for bounding box tracking."""
def __init__(self):
# State: [x, y, s, r, vx, vy, vs]
# x, y = center, s = area, r = aspect ratio
# vx, vy, vs = velocities
self.dt = 1.0
# State transition matrix
self.F = np.array([
[1, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 0, 1, 0, 0, 0, 1],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1],
], dtype=np.float32)
# Measurement matrix
self.H = np.array([
[1, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
], dtype=np.float32)
# Process noise
self.Q = np.eye(7, dtype=np.float32) * 0.01
self.Q[4:, 4:] *= 0.1
# Measurement noise
self.R = np.eye(4, dtype=np.float32) * 0.1
# Error covariance
self.P = np.eye(7, dtype=np.float32) * 10
self.mean = np.zeros(7, dtype=np.float32)
self.covariance = self.P.copy()
def initiate(self, measurement: np.ndarray) -> None:
"""Initialize state from a detection measurement [x, y, s, r]."""
self.mean[:4] = measurement
self.mean[4:] = 0
self.covariance = self.P.copy()
def predict(self) -> np.ndarray:
"""Predict next state. Returns predicted [x, y, s, r]."""
self.mean = self.F @ self.mean
self.covariance = self.F @ self.covariance @ self.F.T + self.Q
return self.mean[:4].copy()
def update(self, measurement: np.ndarray) -> None:
"""Update state with measurement [x, y, s, r]."""
innovation = measurement - self.H @ self.mean
S = self.H @ self.covariance @ self.H.T + self.R
K = self.covariance @ self.H.T @ np.linalg.inv(S)
self.mean = self.mean + K @ innovation
self.covariance = (np.eye(7) - K @ self.H) @ self.covariance
def get_state(self) -> np.ndarray:
"""Get current [x, y, s, r] state estimate."""
return self.mean[:4].copy()
def bbox_to_state(bbox: List[float]) -> np.ndarray:
"""Convert [x1, y1, x2, y2] to [cx, cy, area, aspect_ratio]."""
x1, y1, x2, y2 = bbox
w = max(x2 - x1, 1.0)
h = max(y2 - y1, 1.0)
cx = (x1 + x2) / 2.0
cy = (y1 + y2) / 2.0
area = w * h
ar = w / h
return np.array([cx, cy, area, ar], dtype=np.float32)
def state_to_bbox(state: np.ndarray) -> List[float]:
"""Convert [cx, cy, area, ar] to [x1, y1, x2, y2]."""
cx, cy, area, ar = state
w = np.sqrt(area * ar)
h = area / w
x1 = cx - w / 2
y1 = cy - h / 2
x2 = cx + w / 2
y2 = cy + h / 2
return [float(x1), float(y1), float(x2), float(y2)]
def compute_iou(bbox1: List[float], bbox2: List[float]) -> float:
"""Compute IoU between two bounding boxes."""
x1 = max(bbox1[0], bbox2[0])
y1 = max(bbox1[1], bbox2[1])
x2 = min(bbox1[2], bbox2[2])
y2 = min(bbox1[3], bbox2[3])
inter = max(0, x2 - x1) * max(0, y2 - y1)
if inter <= 0:
return 0.0
area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
union = area1 + area2 - inter
return inter / union if union > 0 else 0.0
class Track:
"""Single track with Kalman filtering and state management."""
def __init__(self, track_id: int, bbox: List[float], confidence: float):
self.track_id = track_id
self.bbox = bbox
self.confidence = confidence
self.state = 'tentative' # tentative, confirmed, lost, removed
self.age = 0
self.hits = 1 # Number of successful matches
self.lost_frames = 0 # Consecutive frames without match
self.kalman = KalmanFilter()
self.kalman.initiate(bbox_to_state(bbox))
self.prev_bbox = bbox.copy()
self.speed = 0.0 # Movement speed in pixels/frame
def predict(self) -> List[float]:
"""Predict next bbox position."""
predicted_state = self.kalman.predict()
self.bbox = state_to_bbox(predicted_state)
return self.bbox
def update(self, bbox: List[float], confidence: float) -> None:
"""Update track with new detection."""
self.prev_bbox = self.bbox.copy()
self.bbox = bbox
self.confidence = confidence
self.hits += 1
self.lost_frames = 0
self.age += 1
# Compute speed
dx = (self.bbox[0] + self.bbox[2]) / 2 - (self.prev_bbox[0] + self.prev_bbox[2]) / 2
dy = (self.bbox[1] + self.bbox[3]) / 2 - (self.prev_bbox[1] + self.prev_bbox[3]) / 2
self.speed = np.sqrt(dx**2 + dy**2)
# Update Kalman
measurement = bbox_to_state(bbox)
self.kalman.update(measurement)
# Promote to confirmed after 3 hits
if self.state == 'tentative' and self.hits >= 3:
self.state = 'confirmed'
def mark_lost(self) -> None:
"""Mark track as lost (no matching detection this frame)."""
self.lost_frames += 1
self.age += 1
if self.state == 'confirmed':
self.state = 'lost'
if self.lost_frames >= 30:
self.state = 'removed'
def is_removed(self) -> bool:
"""Check if track should be removed."""
return self.state == 'removed' or self.lost_frames >= 30
class TrackManager:
"""Manages all tracks across frames."""
def __init__(self, max_lost_frames: int = 30, min_hits: int = 3):
self.tracks: Dict[int, Track] = {}
self.next_id = 1
self.max_lost_frames = max_lost_frames
self.min_hits = min_hits
def update(self, detections: List[Dict]) -> List[Dict]:
"""
Update tracks with new detections.
Args:
detections: List of detection dicts with 'bbox', 'confidence'.
Returns:
List of track dicts with 'bbox', 'track_id', 'confidence', 'state'.
"""
# Predict existing tracks
for track in self.tracks.values():
if track.state != 'removed':
track.predict()
# Remove dead tracks
self.tracks = {
tid: t for tid, t in self.tracks.items()
if t.state != 'removed'
}
if not detections:
# Mark all as lost
for track in self.tracks.values():
track.mark_lost()
return self._get_active_tracks()
# Separate high and low confidence detections
high_dets = [d for d in detections if d['confidence'] >= 0.5]
low_dets = [d for d in detections if d['confidence'] < 0.5]
# Get confirmed and tentative tracks
confirmed_tracks = {
tid: t for tid, t in self.tracks.items()
if t.state in ('confirmed', 'lost')
}
tentative_tracks = {
tid: t for tid, t in self.tracks.items()
if t.state == 'tentative'
}
lost_tracks = {
tid: t for tid, t in self.tracks.items()
if t.state == 'lost'
}
# First match: high score detections to confirmed tracks
matched_dets = set()
matched_tracks = set()
if confirmed_tracks and high_dets:
matched = self._match_tracks(confirmed_tracks, high_dets, min_iou=0.3)
for track_id, det_idx in matched.items():
t = self.tracks[track_id]
t.update(high_dets[det_idx]['bbox'], high_dets[det_idx]['confidence'])
matched_dets.add(det_idx)
matched_tracks.add(track_id)
# Second match: remaining high detections to tentative tracks
remaining_high = [
d for i, d in enumerate(high_dets) if i not in matched_dets
]
remaining_tentative = {
tid: t for tid, t in tentative_tracks.items()
if tid not in matched_tracks
}
if remaining_tentative and remaining_high:
matched = self._match_tracks(remaining_tentative, remaining_high, min_iou=0.5)
for track_id, det_idx in matched.items():
actual_idx = [i for i, d in enumerate(high_dets)
if d is remaining_high[det_idx]][0]
t = self.tracks[track_id]
t.update(high_dets[actual_idx]['bbox'], high_dets[actual_idx]['confidence'])
matched_dets.add(actual_idx)
matched_tracks.add(track_id)
# Third match: low score detections to unmatched confirmed tracks
remaining_low = [d for i, d in enumerate(low_dets)]
unmatched_confirmed = {
tid: t for tid, t in confirmed_tracks.items()
if tid not in matched_tracks
}
if unmatched_confirmed and remaining_low:
matched = self._match_tracks(unmatched_confirmed, remaining_low, min_iou=0.3)
for track_id, det_idx in matched.items():
t = self.tracks[track_id]
t.update(low_dets[det_idx]['bbox'], low_dets[det_idx]['confidence'])
matched_tracks.add(track_id)
# Mark unmatched confirmed tracks as lost
for tid, t in list(self.tracks.items()):
if tid not in matched_tracks and t.state in ('confirmed', 'lost'):
t.mark_lost()
# Create new tracks for unmatched high-confidence detections
for i, d in enumerate(high_dets):
if i not in matched_dets:
track = Track(self.next_id, d['bbox'], d['confidence'])
self.tracks[self.next_id] = track
self.next_id += 1
# Clean removed tracks
self.tracks = {
tid: t for tid, t in self.tracks.items()
if not t.is_removed()
}
return self._get_active_tracks()
def _match_tracks(
self,
tracks: Dict[int, Track],
detections: List[Dict],
min_iou: float = 0.3
) -> Dict[int, int]:
"""
Match tracks to detections using IoU-based Hungarian algorithm.
Args:
tracks: Dict of track_id -> Track.
detections: List of detection dicts.
min_iou: Minimum IoU threshold.
Returns:
Dict of track_id -> detection_index for matches.
"""
if not tracks or not detections:
return {}
track_ids = list(tracks.keys())
track_bboxes = [tracks[tid].bbox for tid in track_ids]
det_bboxes = [d['bbox'] for d in detections]
# Build IoU matrix
iou_matrix = np.zeros((len(track_ids), len(det_bboxes)), dtype=np.float32)
for i, tbox in enumerate(track_bboxes):
for j, dbox in enumerate(det_bboxes):
iou_matrix[i, j] = compute_iou(tbox, dbox)
# Hungarian matching
row_indices, col_indices = linear_sum_assignment(-iou_matrix)
# Filter by IoU threshold
matches = {}
for i, j in zip(row_indices, col_indices):
if iou_matrix[i, j] >= min_iou:
matches[track_ids[i]] = j
return matches
def _get_active_tracks(self) -> List[Dict]:
"""Get list of active tracks (confirmed + tentative)."""
results = []
for track in self.tracks.values():
if track.state in ('tentative', 'confirmed', 'lost'):
results.append({
'track_id': track.track_id,
'bbox': track.bbox,
'confidence': track.confidence,
'state': track.state,
'speed': track.speed,
'lost_frames': track.lost_frames,
})
return results
def get_track(self, track_id: int) -> Optional[Track]:
"""Get track by ID."""
return self.tracks.get(track_id)
def get_all_tracks(self) -> Dict[int, Track]:
"""Get all tracks."""
return self.tracks
def reset(self) -> None:
"""Reset all tracks."""
self.tracks = {}
self.next_id = 1
if __name__ == "__main__":
# Quick test
manager = TrackManager()
# Simulate 5 frames of a person walking
for frame_id in range(5):
x = 100 + frame_id * 10
detections = [{'bbox': [x, 100, x + 50, 200], 'confidence': 0.9}]
tracks = manager.update(detections)
print(f"Frame {frame_id}: {len(tracks)} tracks, IDs: {[t['track_id'] for t in tracks]}")
for t in tracks:
print(f" Track {t['track_id']}: state={t['state']}, bbox={t['bbox']}")
print("Tracker module OK!")