Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |
| 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() | |