Skip to content

Commit d851363

Browse files
Bordaclaude
andcommitted
feat: add generate_codabench.py for MOT17 submission
- Runs tracker over all 21 MOT17 test sequences (7 IDs × 3 public detectors) - Produces a flat ZIP ready for CodaBench upload (21 files, MOT 10-col format) - Default: --det-source all (DPM/FRCNN/SDP run separately); single source replicates to all 3 DET slots - Uses tracker constructor defaults; dataset path resolved relative to script - Gitignore: track autotrack/*.zip to keep submission ZIPs out of repo --- Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent cf03dd4 commit d851363

2 files changed

Lines changed: 378 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,5 @@ wandb/
178178

179179
autotrack/mot17/
180180
autotrack/pretrained/
181-
_optimizations/
182-
_outputs/
181+
autotrack/*.zip
183182
*.local.*

autotrack/generate_codabench.py

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
#!/usr/bin/env python3
2+
# ------------------------------------------------------------------------
3+
# Trackers
4+
# Copyright (c) 2026 Roboflow. All Rights Reserved.
5+
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
6+
# ------------------------------------------------------------------------
7+
8+
"""generate_codabench.py — Generate a MOT17 CodaBench submission ZIP.
9+
10+
Runs a tracker over the MOT17 test sequences and packages the results into
11+
a flat ZIP ready for upload to https://www.codabench.org/competitions/10049.
12+
13+
Submission format (21 files, flat ZIP):
14+
MOT17-{01,03,06,07,08,12,14}-{DPM,FRCNN,SDP}.txt
15+
16+
Each line (10 comma-separated values, 1-based frame and id):
17+
frame, id, bb_left, bb_top, bb_width, bb_height, conf, x, y, z
18+
where x/y/z are always -1 (2-D challenge).
19+
20+
Detector variants
21+
-----------------
22+
``--det-source`` selects which detection file(s) to use:
23+
24+
* ``frcnn`` / ``dpm`` / ``sdp`` — one of the three public MOT17 bundled
25+
detectors. The tracker runs on that detector's detections; the same
26+
result is written to all three detector-name slots in the ZIP (MOT17
27+
requires 21 files regardless of which detector was used).
28+
29+
* ``all`` — runs the tracker separately on each of DPM, FRCNN, and SDP;
30+
produces 21 distinct result files.
31+
32+
* Any other string (e.g. ``rfdetr``) — treats it as a custom detector tag.
33+
Expects ``{dataset_dir}/MOT17-{seq}-{TAG}/det/det.txt`` (TAG = uppercased
34+
det-source string). Result is copied to all three DET slots in the ZIP.
35+
36+
Config loading
37+
--------------
38+
Tracker params are resolved in this order (highest priority first):
39+
40+
1. CLI keyword arguments (``--lost-track-buffer 30``, etc.)
41+
2. ``best_config.json`` entry for ``tracker / det_source`` (if the file exists)
42+
3. Tracker constructor defaults
43+
44+
Usage:
45+
# Best config for bytetrack+frcnn, result replicated to all 3 det slots:
46+
uv run python generate_codabench.py bytetrack --det-source frcnn
47+
48+
# Run all 3 public detectors separately (21 unique result files):
49+
uv run python generate_codabench.py bytetrack --det-source all
50+
51+
# Custom RF-DETR detections, bytetrack, explicit params:
52+
uv run python generate_codabench.py bytetrack --det-source rfdetr \\
53+
--lost-track-buffer 50 --track-activation-threshold 0.4
54+
55+
# OC-SORT with SDP detections:
56+
uv run python generate_codabench.py ocsort --det-source sdp
57+
58+
Prerequisites:
59+
trackers download mot17 --split test --asset detections
60+
(frames are NOT needed for detection-based tracking)
61+
"""
62+
63+
from __future__ import annotations
64+
65+
import sys
66+
import zipfile
67+
from pathlib import Path
68+
from typing import Any
69+
70+
import numpy as np
71+
import supervision as sv
72+
from rich.console import Console
73+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn
74+
75+
console = Console()
76+
_err = Console(stderr=True)
77+
78+
# ---------------------------------------------------------------------------
79+
# Constants
80+
# ---------------------------------------------------------------------------
81+
82+
_TEST_SEQ_IDS = ["01", "03", "06", "07", "08", "12", "14"]
83+
_PUBLIC_DET_TAGS = ["DPM", "FRCNN", "SDP"]
84+
_DEFAULT_DATASET_DIR = Path(__file__).parent / "mot17" / "test"
85+
86+
# ---------------------------------------------------------------------------
87+
# Tracker construction
88+
# ---------------------------------------------------------------------------
89+
90+
# Global to support Kalman monkey-patch for ByteTrack
91+
_ORIG_KALMAN_INIT: Any = None
92+
93+
94+
def _apply_kalman_patch(params: dict, tracker_name: str) -> None:
95+
"""Monkey-patch ByteTrack Kalman matrices from ``params``. No-op for others."""
96+
if tracker_name != "bytetrack":
97+
return
98+
99+
global _ORIG_KALMAN_INIT
100+
from trackers.core.bytetrack.kalman import ByteTrackKalmanBoxTracker
101+
102+
if _ORIG_KALMAN_INIT is None:
103+
_ORIG_KALMAN_INIT = ByteTrackKalmanBoxTracker._initialize_kalman_filter
104+
105+
q = params.get("q_scale", 0.01)
106+
r = params.get("r_scale", 0.1)
107+
p = params.get("p_scale", 1.0)
108+
vel_decay = params.get("velocity_decay", 0.95)
109+
q_miss = params.get("q_miss_alpha", 0.1)
110+
orig = _ORIG_KALMAN_INIT
111+
112+
def _patched(self: ByteTrackKalmanBoxTracker) -> None:
113+
orig(self)
114+
self.Q = np.eye(self.Q.shape[0], dtype=np.float32) * q
115+
self.R = np.eye(self.R.shape[0], dtype=np.float32) * r
116+
self.P = np.eye(self.P.shape[0], dtype=np.float32) * p
117+
118+
setattr(ByteTrackKalmanBoxTracker, "_initialize_kalman_filter", _patched)
119+
setattr(ByteTrackKalmanBoxTracker, "velocity_decay", vel_decay)
120+
setattr(ByteTrackKalmanBoxTracker, "q_miss_alpha", q_miss)
121+
setattr(
122+
ByteTrackKalmanBoxTracker,
123+
"p_reset_threshold",
124+
params.get("p_reset_threshold", 5),
125+
)
126+
setattr(ByteTrackKalmanBoxTracker, "oru_threshold", params.get("oru_threshold", 2))
127+
128+
129+
def _build_tracker(params: dict, tracker_name: str):
130+
"""Construct a tracker from ``params``."""
131+
_apply_kalman_patch(params, tracker_name)
132+
133+
if tracker_name == "bytetrack":
134+
from trackers import ByteTrackTracker
135+
136+
return ByteTrackTracker(
137+
lost_track_buffer=params.get("lost_track_buffer", 30),
138+
minimum_consecutive_frames=params.get("minimum_consecutive_frames", 1),
139+
minimum_iou_threshold=params.get("minimum_iou_threshold", 0.2),
140+
track_activation_threshold=params.get("track_activation_threshold", 0.25),
141+
high_conf_det_threshold=params.get("high_conf_det_threshold", 0.6),
142+
)
143+
if tracker_name == "sort":
144+
from trackers import SORTTracker
145+
146+
return SORTTracker(
147+
lost_track_buffer=params.get("lost_track_buffer", 1),
148+
minimum_consecutive_frames=params.get("minimum_consecutive_frames", 3),
149+
minimum_iou_threshold=params.get("minimum_iou_threshold", 0.3),
150+
)
151+
if tracker_name == "ocsort":
152+
from trackers import OCSORTTracker
153+
154+
return OCSORTTracker(
155+
lost_track_buffer=params.get("lost_track_buffer", 30),
156+
minimum_consecutive_frames=params.get("minimum_consecutive_frames", 3),
157+
minimum_iou_threshold=params.get("minimum_iou_threshold", 0.3),
158+
direction_consistency_weight=params.get(
159+
"direction_consistency_weight", 0.5
160+
),
161+
high_conf_det_threshold=params.get("high_conf_det_threshold", 0.6),
162+
delta_t=params.get("delta_t", 3),
163+
)
164+
raise ValueError(
165+
f"Unknown tracker: {tracker_name!r}. Choose: sort | bytetrack | ocsort"
166+
)
167+
168+
169+
# ---------------------------------------------------------------------------
170+
# Sequence tracking
171+
# ---------------------------------------------------------------------------
172+
173+
174+
def _run_sequence(tracker, det_file: Path, max_interpolation_gap: int = 0) -> list[str]:
175+
"""Run tracker on one sequence's detections; return MOT-format lines.
176+
177+
Args:
178+
tracker: Initialised tracker instance (will be reset before use).
179+
det_file: Path to the MOT-format detection file.
180+
max_interpolation_gap: Fill gaps up to this many frames (0 = off).
181+
182+
Returns:
183+
List of formatted MOT result lines (no trailing newline per line).
184+
"""
185+
from trackers.io.mot import _load_mot_file, _mot_frame_to_detections
186+
187+
tracker.reset()
188+
detections_data = _load_mot_file(det_file)
189+
if not detections_data:
190+
return []
191+
192+
max_frame = max(detections_data.keys())
193+
lines: list[str] = []
194+
195+
for frame_idx in range(1, max_frame + 1):
196+
dets = (
197+
_mot_frame_to_detections(detections_data[frame_idx])
198+
if frame_idx in detections_data
199+
else sv.Detections.empty()
200+
)
201+
tracked = tracker.update(dets)
202+
203+
if tracked.tracker_id is None:
204+
continue
205+
206+
for i, tid in enumerate(tracked.tracker_id):
207+
if tid < 0:
208+
continue
209+
x1, y1, x2, y2 = tracked.xyxy[i]
210+
w, h = x2 - x1, y2 - y1
211+
conf = (
212+
float(tracked.confidence[i]) if tracked.confidence is not None else 1.0
213+
)
214+
# MOT format uses 1-based track IDs; tracker returns 0-based
215+
lines.append(
216+
f"{frame_idx},{int(tid) + 1},{x1:.2f},{y1:.2f},"
217+
f"{w:.2f},{h:.2f},{conf:.4f},-1,-1,-1"
218+
)
219+
220+
if max_interpolation_gap > 0:
221+
from trackers.core.sort.utils import interpolate_mot_gaps
222+
223+
lines = interpolate_mot_gaps(lines, max_gap=max_interpolation_gap)
224+
225+
return lines
226+
227+
228+
# ---------------------------------------------------------------------------
229+
# Main entry point
230+
# ---------------------------------------------------------------------------
231+
232+
233+
def generate_submission(
234+
tracker: str = "bytetrack",
235+
det_source: str = "all",
236+
dataset_dir: str | None = None,
237+
output: str | None = None,
238+
**kwargs: Any,
239+
) -> None:
240+
"""Generate a MOT17 CodaBench submission ZIP.
241+
242+
Args:
243+
tracker: Tracker algorithm — ``sort``, ``bytetrack``, or ``ocsort``.
244+
det_source: Detection source tag. One of ``dpm``, ``frcnn``, ``sdp``,
245+
``all`` (run all three public detectors separately), or a custom tag
246+
(e.g. ``rfdetr``) pointing to
247+
``{dataset_dir}/MOT17-{seq}-{TAG}/det/det.txt``.
248+
dataset_dir: Path to the MOT17 test directory.
249+
Defaults to the ``mot17/test`` sibling of this script.
250+
output: Path for the output ZIP file.
251+
Defaults to ``submission-{tracker}-{det_source}.zip``.
252+
**kwargs: Tracker parameter overrides passed directly to the tracker
253+
constructor (e.g. ``lost_track_buffer=50``).
254+
255+
Examples:
256+
Run from the autotrack directory:
257+
258+
>>> # uv run python generate_codabench.py bytetrack
259+
>>> # uv run python generate_codabench.py bytetrack --det-source all
260+
"""
261+
data_dir = Path(dataset_dir) if dataset_dir else _DEFAULT_DATASET_DIR
262+
det_source_norm = det_source.lower()
263+
264+
if output is None:
265+
output = f"submission-{tracker}-{det_source_norm}.zip"
266+
output_path = Path(output)
267+
268+
if det_source_norm == "all":
269+
run_pairs: list[tuple[str, str]] = [(t, t) for t in _PUBLIC_DET_TAGS]
270+
else:
271+
file_tag = det_source_norm.upper()
272+
run_pairs = [(file_tag, file_tag)]
273+
274+
params = dict(kwargs)
275+
max_interp = int(params.pop("max_interpolation_gap", 0))
276+
277+
# Report configuration
278+
console.print("\n[bold]MOT17 submission generator[/bold]")
279+
console.print(f" tracker : [cyan]{tracker}[/cyan]")
280+
console.print(f" det_source: [cyan]{det_source}[/cyan]")
281+
console.print(f" dataset : [cyan]{data_dir}[/cyan]")
282+
console.print(f" output : [cyan]{output_path}[/cyan]")
283+
if params:
284+
console.print(f" params : {params}")
285+
console.print()
286+
287+
# Validate dataset dir
288+
if not data_dir.exists():
289+
_err.print(f"[red]Error: dataset_dir does not exist: {data_dir}[/red]")
290+
_err.print(" Download test data first:")
291+
_err.print(" trackers download mot17 --split test --asset detections")
292+
sys.exit(1)
293+
294+
# Collect (output_filename → lines) mapping
295+
# Key = "MOT17-{seq_id}-{DET_TAG}.txt"
296+
results: dict[str, list[str]] = {}
297+
298+
total_jobs = len(_TEST_SEQ_IDS) * len(run_pairs)
299+
300+
with Progress(
301+
TextColumn(" {task.description}"),
302+
BarColumn(),
303+
MofNCompleteColumn(),
304+
console=console,
305+
) as progress:
306+
task = progress.add_task("Tracking", total=total_jobs)
307+
308+
for seq_id in _TEST_SEQ_IDS:
309+
for out_tag, file_tag in run_pairs:
310+
seq_dir_name = f"MOT17-{seq_id}-{file_tag}"
311+
det_file = data_dir / seq_dir_name / "det" / "det.txt"
312+
313+
progress.update(task, description=f"[cyan]{seq_dir_name}[/cyan]")
314+
315+
if not det_file.exists():
316+
_err.print(
317+
f"\n[yellow]Warning: detection file not found, "
318+
f"skipping: {det_file}[/yellow]"
319+
)
320+
progress.advance(task)
321+
continue
322+
323+
tracker_inst = _build_tracker(params, tracker)
324+
lines = _run_sequence(tracker_inst, det_file, max_interp)
325+
out_name = f"MOT17-{seq_id}-{out_tag}.txt"
326+
results[out_name] = lines
327+
progress.advance(task)
328+
329+
if not results:
330+
_err.print("[red]Error: no sequences were processed.[/red]")
331+
sys.exit(1)
332+
333+
# When using a single (non-all) source, replicate to all 3 DET slots
334+
if det_source_norm != "all":
335+
expanded: dict[str, list[str]] = {}
336+
for seq_id in _TEST_SEQ_IDS:
337+
for out_tag in _PUBLIC_DET_TAGS:
338+
out_name = f"MOT17-{seq_id}-{out_tag}.txt"
339+
# Find the result we produced for this sequence
340+
src_name = next(
341+
(k for k in results if k.startswith(f"MOT17-{seq_id}-")),
342+
None,
343+
)
344+
if src_name is not None:
345+
expanded[out_name] = results[src_name]
346+
results = expanded
347+
348+
# Package into ZIP
349+
output_path.parent.mkdir(parents=True, exist_ok=True)
350+
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
351+
for filename, lines in sorted(results.items()):
352+
content = "\n".join(lines) + ("\n" if lines else "")
353+
zf.writestr(filename, content)
354+
355+
n_files = len(results)
356+
n_missing = 21 - n_files
357+
console.print(f"\n[green]✓ Wrote {n_files}/21 files → {output_path}[/green]")
358+
if n_missing > 0:
359+
console.print(
360+
f"[yellow] ⚠ {n_missing} files missing — upload may be rejected.[/yellow]"
361+
)
362+
console.print(
363+
f" Check that all 7 sequence directories are present in {data_dir}"
364+
)
365+
else:
366+
console.print(" Upload at: https://www.codabench.org/competitions/10049")
367+
console.print()
368+
369+
370+
# ---------------------------------------------------------------------------
371+
# CLI entry point
372+
# ---------------------------------------------------------------------------
373+
374+
if __name__ == "__main__":
375+
import fire
376+
377+
fire.Fire(generate_submission)

0 commit comments

Comments
 (0)