Vehicle-Counting / core /tracker.py
riezqidr's picture
Add initial project structure with Streamlit UI and utility functions
ab2f940
Raw
History Blame Contribute Delete
12.7 kB
"""
ByteTrack Object Tracker
Implementasi ByteTrack untuk multi-object tracking pada kendaraan.
Menggunakan Kalman Filter untuk prediksi state dan Hungarian Algorithm
untuk data association.
Referensi paper: ByteTrack: Multi-Object Tracking by Associating Every Detection Box
https://arxiv.org/abs/2110.06864
"""
import numpy as np
from scipy.optimize import linear_sum_assignment
from collections import deque
class KalmanFilter:
"""
Kalman Filter sederhana untuk tracking bounding box.
State vector: [cx, cy, w, h, vx, vy, vw, vh]
- cx, cy: center x, y
- w, h: width, height
- vx, vy, vw, vh: velocity components
"""
def __init__(self):
# state transition matrix (8x8)
self.F = np.eye(8)
self.F[0, 4] = 1 # cx += vx
self.F[1, 5] = 1 # cy += vy
self.F[2, 6] = 1 # w += vw
self.F[3, 7] = 1 # h += vh
# observation matrix (4x8), kita observe [cx, cy, w, h]
self.H = np.zeros((4, 8))
self.H[0, 0] = 1
self.H[1, 1] = 1
self.H[2, 2] = 1
self.H[3, 3] = 1
# process noise
self.Q = np.eye(8) * 1.0
self.Q[4:, 4:] *= 0.01 # velocity noise lebih kecil
# measurement noise
self.R = np.eye(4) * 1.0
# state dan covariance
self.x = np.zeros(8)
self.P = np.eye(8) * 10.0
def init_state(self, bbox):
"""
Inisialisasi state dari bounding box [x1, y1, x2, y2].
"""
cx = (bbox[0] + bbox[2]) / 2
cy = (bbox[1] + bbox[3]) / 2
w = bbox[2] - bbox[0]
h = bbox[3] - bbox[1]
self.x = np.array([cx, cy, w, h, 0, 0, 0, 0], dtype=np.float64)
self.P = np.eye(8) * 10.0
def predict(self):
"""Predict step."""
self.x = self.F @ self.x
self.P = self.F @ self.P @ self.F.T + self.Q
return self.x[:4] # return predicted [cx, cy, w, h]
def update(self, bbox):
"""
Update step dengan measurement baru.
Args:
bbox: [x1, y1, x2, y2]
"""
cx = (bbox[0] + bbox[2]) / 2
cy = (bbox[1] + bbox[3]) / 2
w = bbox[2] - bbox[0]
h = bbox[3] - bbox[1]
z = np.array([cx, cy, w, h])
# innovation
y = z - self.H @ self.x
S = self.H @ self.P @ self.H.T + self.R
K = self.P @ self.H.T @ np.linalg.inv(S)
self.x = self.x + K @ y
self.P = (np.eye(8) - K @ self.H) @ self.P
def get_bbox(self):
"""Return current state sebagai [x1, y1, x2, y2]."""
cx, cy, w, h = self.x[:4]
x1 = cx - w / 2
y1 = cy - h / 2
x2 = cx + w / 2
y2 = cy + h / 2
return [x1, y1, x2, y2]
def get_center(self):
"""Return center point (cx, cy)."""
return self.x[0], self.x[1]
class Track:
"""
Representasi satu tracked object.
Menyimpan state, history, dan metadata untuk setiap kendaraan yang dilacak.
"""
_next_id = 1 # class variable untuk auto-increment ID
def __init__(self, bbox, class_id, class_name, confidence):
self.track_id = Track._next_id
Track._next_id += 1
self.kf = KalmanFilter()
self.kf.init_state(bbox)
self.class_id = class_id
self.class_name = class_name
self.confidence = confidence
self.hits = 1 # berapa kali di-match
self.age = 0 # umur track (frame)
self.time_since_update = 0 # frame sejak terakhir di-update
# simpan history centroid untuk counting
self.center_history = deque(maxlen=50)
cx = (bbox[0] + bbox[2]) / 2
cy = (bbox[1] + bbox[3]) / 2
self.center_history.append((cx, cy))
self.is_confirmed = False # confirmed setelah beberapa hits
def predict(self):
"""Predict posisi berikutnya."""
self.kf.predict()
self.age += 1
self.time_since_update += 1
def update(self, bbox, class_id, class_name, confidence):
"""Update track dengan deteksi baru."""
self.kf.update(bbox)
self.class_id = class_id
self.class_name = class_name
self.confidence = confidence
self.hits += 1
self.time_since_update = 0
cx, cy = self.kf.get_center()
self.center_history.append((cx, cy))
# confirm track setelah 3 hits
if self.hits >= 3:
self.is_confirmed = True
def get_bbox(self):
"""Return bounding box saat ini."""
return self.kf.get_bbox()
def get_center(self):
"""Return center point saat ini."""
return self.kf.get_center()
@classmethod
def reset_id_counter(cls):
"""Reset ID counter, panggil di awal video baru."""
cls._next_id = 1
def compute_iou(bbox1, bbox2):
"""
Hitung Intersection over Union antara dua bounding box.
Args:
bbox1, bbox2: [x1, y1, x2, y2]
Returns:
float: IoU value
"""
x1 = max(bbox1[0], bbox2[0])
y1 = max(bbox1[1], bbox2[1])
x2 = min(bbox1[2], bbox2[2])
y2 = min(bbox1[3], bbox2[3])
intersection = max(0, x2 - x1) * max(0, y2 - y1)
area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
union = area1 + area2 - intersection
if union <= 0:
return 0.0
return intersection / union
def compute_iou_matrix(bboxes1, bboxes2):
"""
Hitung IoU matrix antara dua set bounding boxes.
Returns:
numpy array shape (len(bboxes1), len(bboxes2))
"""
n = len(bboxes1)
m = len(bboxes2)
iou_matrix = np.zeros((n, m))
for i in range(n):
for j in range(m):
iou_matrix[i, j] = compute_iou(bboxes1[i], bboxes2[j])
return iou_matrix
class ByteTracker:
"""
ByteTrack multi-object tracker.
Implementasi berdasarkan paper ByteTrack yang menggunakan
two-stage association untuk memanfaatkan deteksi low-confidence
yang biasanya dibuang oleh tracker lain.
Args:
track_thresh: threshold untuk membedakan high/low confidence detections
match_thresh: minimum IoU untuk matching
track_buffer: jumlah frame sebelum track dihapus (kalau tidak di-update)
"""
def __init__(self, track_thresh=0.5, match_thresh=0.3, track_buffer=30):
self.track_thresh = track_thresh
self.match_thresh = match_thresh
self.track_buffer = track_buffer
self.active_tracks = [] # track yang sedang aktif
self.lost_tracks = [] # track yang hilang tapi belum dihapus
def update(self, detections):
"""
Update tracker dengan deteksi baru dari frame saat ini.
Ini inti dari ByteTrack: two-stage association.
Stage 1: match high-confidence detections dengan active tracks
Stage 2: match low-confidence detections dengan unmatched tracks
Args:
detections: list of dict dari VehicleDetector.detect()
masing-masing harus punya: bbox, confidence, class_id, class_name
Returns:
list of dict, setiap item berisi:
- track_id: unique ID
- bbox: [x1, y1, x2, y2]
- class_id: int
- class_name: str
- confidence: float
- center: (cx, cy)
"""
# predict semua active tracks dulu
for track in self.active_tracks:
track.predict()
for track in self.lost_tracks:
track.predict()
# pisahkan deteksi jadi high confidence dan low confidence
high_dets = []
low_dets = []
for det in detections:
if det["confidence"] >= self.track_thresh:
high_dets.append(det)
else:
low_dets.append(det)
# ============ Stage 1: match high-confidence dets dengan active tracks ============
unmatched_tracks_idx = list(range(len(self.active_tracks)))
unmatched_dets_idx = list(range(len(high_dets)))
if len(self.active_tracks) > 0 and len(high_dets) > 0:
track_bboxes = [t.get_bbox() for t in self.active_tracks]
det_bboxes = [d["bbox"] for d in high_dets]
iou_matrix = compute_iou_matrix(track_bboxes, det_bboxes)
cost_matrix = 1.0 - iou_matrix # karena hungarian minimize cost
row_indices, col_indices = linear_sum_assignment(cost_matrix)
matched_tracks = set()
matched_dets = set()
for row, col in zip(row_indices, col_indices):
if iou_matrix[row, col] >= self.match_thresh:
# match berhasil
self.active_tracks[row].update(
high_dets[col]["bbox"],
high_dets[col]["class_id"],
high_dets[col]["class_name"],
high_dets[col]["confidence"]
)
matched_tracks.add(row)
matched_dets.add(col)
unmatched_tracks_idx = [i for i in range(len(self.active_tracks)) if i not in matched_tracks]
unmatched_dets_idx = [i for i in range(len(high_dets)) if i not in matched_dets]
# ============ Stage 2: match low-confidence dets dengan unmatched tracks ============
remaining_tracks = [self.active_tracks[i] for i in unmatched_tracks_idx]
still_unmatched_tracks = list(range(len(remaining_tracks)))
if len(remaining_tracks) > 0 and len(low_dets) > 0:
track_bboxes = [t.get_bbox() for t in remaining_tracks]
det_bboxes = [d["bbox"] for d in low_dets]
iou_matrix = compute_iou_matrix(track_bboxes, det_bboxes)
cost_matrix = 1.0 - iou_matrix
row_indices, col_indices = linear_sum_assignment(cost_matrix)
matched_in_stage2 = set()
for row, col in zip(row_indices, col_indices):
if iou_matrix[row, col] >= self.match_thresh:
remaining_tracks[row].update(
low_dets[col]["bbox"],
low_dets[col]["class_id"],
low_dets[col]["class_name"],
low_dets[col]["confidence"]
)
matched_in_stage2.add(row)
still_unmatched_tracks = [i for i in range(len(remaining_tracks)) if i not in matched_in_stage2]
# handle unmatched tracks -> pindahkan ke lost
for idx in still_unmatched_tracks:
track = remaining_tracks[idx]
if track.time_since_update > self.track_buffer:
continue # buang, sudah terlalu lama hilang
self.lost_tracks.append(track)
# handle unmatched detections -> buat track baru
for idx in unmatched_dets_idx:
det = high_dets[idx]
new_track = Track(
det["bbox"],
det["class_id"],
det["class_name"],
det["confidence"]
)
self.active_tracks.append(new_track)
# coba match lost tracks juga dengan unmatched high-confidence dets
# (ini versi simplified, di paper asli lebih kompleks)
# bersihkan tracks yang sudah expired dari lost
self.lost_tracks = [
t for t in self.lost_tracks
if t.time_since_update <= self.track_buffer
]
# update active tracks: buang yang sudah lama tidak di-update
self.active_tracks = [
t for t in self.active_tracks
if t.time_since_update <= self.track_buffer
]
# gabungkan kembali lost tracks yang di-match ke active
# (simplified: lost tracks tetap di list terpisah)
# return hasil tracking
output = []
for track in self.active_tracks:
if not track.is_confirmed:
continue # skip track yang belum confirmed
bbox = track.get_bbox()
cx, cy = track.get_center()
output.append({
"track_id": track.track_id,
"bbox": bbox,
"class_id": track.class_id,
"class_name": track.class_name,
"confidence": track.confidence,
"center": (cx, cy),
"center_history": list(track.center_history)
})
return output
def reset(self):
"""Reset tracker untuk video baru."""
self.active_tracks = []
self.lost_tracks = []
Track.reset_id_counter()