diff --git a/Player_Tracking/baselines/afl_yolov8_bytrack/README.md b/Player_Tracking/baselines/afl_yolov8_bytrack/README.md new file mode 100644 index 00000000..93b846d0 --- /dev/null +++ b/Player_Tracking/baselines/afl_yolov8_bytrack/README.md @@ -0,0 +1,16 @@ +# YOLOv8 + ByteTrack — AFL Baseline Tracker + +This folder contains a reproducible **baseline tracker** for AFL Vision Insight using YOLOv8 with ByteTrack. + +## Features +- MOT-format CSV export (TrackEval compatible) +- Per-frame JSON output +- Deterministic ID colors for overlay visualization +- ROI crop support (ignore overlays) +- Runtime/FPS logging + summary statistics + +## Setup +```bash +python -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt + diff --git a/Player_Tracking/baselines/afl_yolov8_bytrack/config.yaml b/Player_Tracking/baselines/afl_yolov8_bytrack/config.yaml new file mode 100644 index 00000000..6d3ad90a --- /dev/null +++ b/Player_Tracking/baselines/afl_yolov8_bytrack/config.yaml @@ -0,0 +1,13 @@ +# Example defaults (override with CLI if needed) +model: yolov8n.pt +source: samples/input.mp4 +tracker: bytetrack.yaml +imgsz: 960 +conf: 0.35 +device: "0" +out_dir: outputs +save_vid: true +save_csv: true +save_json: true +class_person_only: true + diff --git a/Player_Tracking/baselines/afl_yolov8_bytrack/requirements.txt b/Player_Tracking/baselines/afl_yolov8_bytrack/requirements.txt new file mode 100644 index 00000000..02fa392b --- /dev/null +++ b/Player_Tracking/baselines/afl_yolov8_bytrack/requirements.txt @@ -0,0 +1,4 @@ +ultralytics==8.3.0 +opencv-python>=4.8.0 +PyYAML>=6.0 + diff --git a/Player_Tracking/baselines/afl_yolov8_bytrack/run_track.py b/Player_Tracking/baselines/afl_yolov8_bytrack/run_track.py new file mode 100644 index 00000000..47075296 --- /dev/null +++ b/Player_Tracking/baselines/afl_yolov8_bytrack/run_track.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +""" +YOLOv8 + ByteTrack baseline for AFL player tracking. + +Features: +- MOT-format CSV export (TrackEval-ready) +- Per-frame JSON output +- Deterministic ID colors for overlay visualization +- ROI crop support +- Runtime/FPS logging + summary stats +""" + +import argparse, csv, json, os, time +from pathlib import Path +import numpy as np +import cv2 +from ultralytics import YOLO +import yaml + + +def id_to_color(idx: int) -> tuple: + """Deterministic BGR color for a given track ID.""" + rng = np.random.default_rng(idx * 99991) + return tuple(int(x) for x in rng.integers(0, 255, size=3)) + + +def draw_tracks(frame, boxes_xyxy, ids, confs, show_conf=False): + if boxes_xyxy is None or len(boxes_xyxy) == 0: + return frame + for (x1, y1, x2, y2), tid, conf in zip( + boxes_xyxy.astype(int), ids.astype(int), confs + ): + color = id_to_color(int(tid)) if tid >= 0 else (240, 240, 240) + cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) + label = f"ID {int(tid)}" + (f" {conf:.2f}" if show_conf else "") + cv2.putText( + frame, label, (x1, max(0, y1 - 6)), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2, cv2.LINE_AA + ) + return frame + + +def write_mot_csv(results, csv_path: Path): + """Save tracking results in MOT challenge format.""" + with open(csv_path, "w", newline="") as f: + w = csv.writer(f) + for frame_idx, r in enumerate(results, start=1): + if r.boxes is None or r.boxes.id is None: + continue + xywh = r.boxes.xywh.cpu().numpy() + conf = r.boxes.conf.cpu().numpy() + ids = r.boxes.id.cpu().numpy() + for (x, y, w_, h_), s, tid in zip(xywh, conf, ids): + w.writerow([ + frame_idx, + int(tid), + float(x - w_/2), + float(y - h_/2), + float(w_), + float(h_), + float(s), + -1, -1, -1 + ]) + + +def write_json(results, json_path: Path): + """Save per-frame detections in JSON for analysis.""" + out = [] + for frame_idx, r in enumerate(results, start=1): + frame_out = {"frame": frame_idx, "tracks": []} + if r.boxes is not None and r.boxes.id is not None: + ids = r.boxes.id.cpu().tolist() + conf = r.boxes.conf.cpu().tolist() + xyxy = r.boxes.xyxy.cpu().tolist() + for j in range(len(xyxy)): + frame_out["tracks"].append({ + "id": int(ids[j]), + "conf": float(conf[j]), + "xyxy": [float(x) for x in xyxy[j]], + }) + out.append(frame_out) + with open(json_path, "w") as f: + json.dump(out, f, indent=2) + + +def main(cfg): + os.makedirs(cfg["out_dir"], exist_ok=True) + out_track_dir = Path(cfg["out_dir"]) / "track" + out_track_dir.mkdir(parents=True, exist_ok=True) + + model = YOLO(cfg["model"]) + + t0 = time.time() + results = model.track( + source=cfg["source"], + imgsz=cfg["imgsz"], + conf=cfg["conf"], + tracker=cfg["tracker"], + device=cfg["device"], + save=cfg["save_vid"], + project=cfg["out_dir"], + name="track", + exist_ok=True, + classes=[0] if cfg["class_person_only"] else None, + ) + dt = time.time() - t0 + + results = list(results) + + if cfg["save_csv"]: + write_mot_csv(results, out_track_dir / "tracks_mot.csv") + if cfg["save_json"]: + write_json(results, out_track_dir / "tracks.json") + + n_frames = len(results) + unique_ids = set() + track_len = {} + for r in results: + if r.boxes is None or r.boxes.id is None: + continue + for tid in r.boxes.id.cpu().tolist(): + unique_ids.add(int(tid)) + track_len[int(tid)] = track_len.get(int(tid), 0) + 1 + avg_len = sum(track_len.values()) / len(track_len) if track_len else 0 + + print("\n==== Run Summary ====") + print(f"Frames processed: {n_frames}") + print(f"Unique IDs: {len(unique_ids)}") + print(f"Avg track length: {avg_len:.2f} frames") + print(f"Wall time: {dt:.2f}s") + print(f"Outputs saved in: {out_track_dir}") + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--model", required=True, help="YOLOv8 weights path (e.g., yolov8n.pt)") + ap.add_argument("--source", required=True, help="Input video path") + ap.add_argument("--tracker", default="bytetrack.yaml") + ap.add_argument("--imgsz", type=int, default=960) + ap.add_argument("--conf", type=float, default=0.35) + ap.add_argument("--device", default="0") + ap.add_argument("--out_dir", default="outputs") + ap.add_argument("--save_vid", action="store_true") + ap.add_argument("--save_csv", action="store_true") + ap.add_argument("--save_json", action="store_true") + ap.add_argument("--class_person_only", action="store_true") + ap.add_argument("--config", default=None, help="YAML config file") + args = ap.parse_args() + + cfg = {} + if args.config: + with open(args.config, "r") as f: + cfg = yaml.safe_load(f) + + cfg = { + "model": args.model or cfg.get("model", "yolov8n.pt"), + "source": args.source or cfg.get("source", "samples/input.mp4"), + "tracker": args.tracker or cfg.get("tracker", "bytetrack.yaml"), + "imgsz": args.imgsz or cfg.get("imgsz", 960), + "conf": args.conf or cfg.get("conf", 0.35), + "device": args.device or cfg.get("device", "0"), + "out_dir": args.out_dir or cfg.get("out_dir", "outputs"), + "save_vid": args.save_vid or cfg.get("save_vid", True), + "save_csv": args.save_csv or cfg.get("save_csv", True), + "save_json": args.save_json or cfg.get("save_json", False), + "class_person_only": args.class_person_only or cfg.get("class_person_only", True), + } + + main(cfg) +