Skip to content

Commit b39d4bf

Browse files
Add SPEED_AND_DISTANCE spotlight mode and ALL orchestrator (PR5).
Extract shared trace renderer from distance.py, add dim/spotlight helpers, run_all batching over a single VideoTrackingSession, and wire new modes in main. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent dac284d commit b39d4bf

4 files changed

Lines changed: 158 additions & 1 deletion

File tree

examples/soccer/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,36 @@ on the field.
148148

149149
Requires pitch keypoints (same flags as SPEED).
150150

151+
- `SPEED_AND_DISTANCE` — Speed + distance chips and per-player trace lines on the
152+
radar minimap (no end-card). Default shows all players; pass `--track-id N` to
153+
spotlight one player (dimmed background, single trace on minimap).
154+
155+
```bash
156+
python main.py --source_video_path data/2e57b9_0.mp4 \
157+
--target_video_path data/renders/2e57b9_0-speed-distance.mp4 \
158+
--device mps --mode SPEED_AND_DISTANCE --max-frames 90
159+
160+
# spotlight one player
161+
python main.py --source_video_path data/2e57b9_0.mp4 \
162+
--target_video_path data/renders/2e57b9_0-speed-distance-spotlight.mp4 \
163+
--device mps --mode SPEED_AND_DISTANCE --track-id 42 --max-frames 90
164+
```
165+
166+
- `ALL` — Runs DIRECTION, SPEED, DISTANCE, SPEED_AND_DISTANCE (all players), and
167+
SPEED_AND_DISTANCE (spotlight) in one pass. Builds a shared
168+
`VideoTrackingSession` once; each mode writes a separate `-{suffix}.mp4` next to
169+
the base `--target_video_path`. Spotlight track is the player with max cumulative
170+
distance unless `--track-id` is set.
171+
172+
```bash
173+
python main.py --source_video_path data/2e57b9_0.mp4 \
174+
--target_video_path data/renders/2e57b9_0.mp4 \
175+
--device mps --mode ALL --max-frames 90
176+
```
177+
178+
Analytics flags: `--max-frames`, `--tracker`, `--track-id`, `--cache`, detector/model
179+
paths (same as SPEED/DISTANCE).
180+
151181
## 🗺️ roadmap
152182

153183
- [ ] Add smoothing to eliminate flickering in RADAR mode.

examples/soccer/main.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626

2727
from direction import run_direction
2828
from distance import run_distance
29+
from run_all import run_all
2930
from speed import run_speed
31+
from speed_and_distance import run_speed_and_distance
3032

3133
PARENT_DIR = os.path.dirname(os.path.abspath(__file__))
3234
PLAYER_DETECTION_MODEL_PATH = os.path.join(PARENT_DIR, 'data/football-player-detection.pt')
@@ -88,9 +90,14 @@ class Mode(Enum):
8890
DIRECTION = 'DIRECTION'
8991
SPEED = 'SPEED'
9092
DISTANCE = 'DISTANCE'
93+
SPEED_AND_DISTANCE = 'SPEED_AND_DISTANCE'
94+
ALL = 'ALL'
9195

9296

93-
ANALYTICS_MODES = (Mode.DIRECTION, Mode.SPEED, Mode.DISTANCE)
97+
ANALYTICS_MODES = (
98+
Mode.DIRECTION, Mode.SPEED, Mode.DISTANCE,
99+
Mode.SPEED_AND_DISTANCE, Mode.ALL,
100+
)
94101

95102

96103
def run_analytics_mode(mode: Mode, args: argparse.Namespace) -> None:
@@ -100,6 +107,10 @@ def run_analytics_mode(mode: Mode, args: argparse.Namespace) -> None:
100107
run_speed(args)
101108
elif mode == Mode.DISTANCE:
102109
run_distance(args)
110+
elif mode == Mode.SPEED_AND_DISTANCE:
111+
run_speed_and_distance(args)
112+
elif mode == Mode.ALL:
113+
run_all(args)
103114
else:
104115
raise NotImplementedError(f"Mode {mode} is not an analytics mode.")
105116

@@ -429,6 +440,8 @@ def main(source_video_path: str, target_video_path: str, device: str, mode: Mode
429440
help='(analytics) Cache per-frame detections on disk')
430441
parser.add_argument('--cache-dir', dest='cache_dir', default=None,
431442
help='(analytics) Directory for the on-disk cache')
443+
parser.add_argument('--track-id', dest='track_id', type=int, default=None,
444+
help='(analytics) Spotlight a specific tracker id (SPEED_AND_DISTANCE)')
432445

433446
args = parser.parse_args()
434447

examples/soccer/run_all.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""In-process orchestrator for all five analytics renders.
2+
3+
Builds a shared :class:`~sports.common.video_tracking.VideoTrackingSession` once
4+
(one BoTSORT pass, one team-classifier fit, homography maps, kinematics) and drives
5+
each mode renderer in-process. Each mode writes its own output video.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import copy
11+
import time
12+
from pathlib import Path
13+
14+
from sports.common.video_tracking import build_video_tracking_session
15+
from direction import run_direction
16+
from distance import run_distance
17+
from speed import run_speed
18+
from speed_and_distance import run_speed_and_distance
19+
20+
_RENDER_PLAN = (
21+
("direction", "DIRECTION"),
22+
("speed", "SPEED"),
23+
("distance", "DISTANCE"),
24+
("speed-distance-all", "SPEED_AND_DISTANCE (all players)"),
25+
("speed-distance-single", "SPEED_AND_DISTANCE (spotlight)"),
26+
)
27+
28+
29+
def _derive_target(base_path: str, suffix: str) -> str:
30+
"""``data/renders/08fd33_0.mp4`` + ``direction`` → ``…/08fd33_0-direction.mp4``."""
31+
p = Path(base_path)
32+
return str(p.with_name(f"{p.stem}-{suffix}{p.suffix or '.mp4'}"))
33+
34+
35+
def _mode_args(args, *, target: str, track_id: int | None = None):
36+
"""Shallow copy of ``args`` with a per-mode target path and spotlight track id."""
37+
new = copy.copy(args)
38+
new.target_video_path = target
39+
new.track_id = track_id
40+
return new
41+
42+
43+
def _pick_spotlight_track_id(tracks) -> int | None:
44+
"""Pick the most-active tracked player (max cumulative distance) for the spotlight."""
45+
best_tid: int | None = None
46+
best_distance = -1.0
47+
for tid, track in tracks.items():
48+
distance = float(getattr(track, "distance_m", 0.0) or 0.0)
49+
if distance > best_distance:
50+
best_distance = distance
51+
best_tid = int(tid)
52+
return best_tid
53+
54+
55+
def run_all(args) -> list[str]:
56+
"""Build shared session once and render all five analytics videos in-process."""
57+
t_start = time.time()
58+
59+
print("── run-all: building shared VideoTrackingSession ─────────")
60+
session = build_video_tracking_session(args, need_homography=True)
61+
print(f"Shared session ready in {time.time() - t_start:.1f}s.")
62+
63+
base = args.target_video_path
64+
explicit_spotlight = getattr(args, "track_id", None)
65+
spotlight_id = (
66+
explicit_spotlight
67+
if explicit_spotlight is not None
68+
else _pick_spotlight_track_id(session.tracks)
69+
)
70+
print(f"Spotlight (SPEED_AND_DISTANCE) track id: {spotlight_id}")
71+
72+
outputs: list[str] = []
73+
timings: list[tuple[str, float]] = []
74+
for suffix, label in _RENDER_PLAN:
75+
target = _derive_target(base, suffix)
76+
track_id = spotlight_id if suffix == "speed-distance-single" else None
77+
t_mode = time.time()
78+
print(f"── run-all: rendering {label}{target} ─────────")
79+
if suffix == "direction":
80+
run_direction(_mode_args(args, target=target), session)
81+
elif suffix == "speed":
82+
run_speed(_mode_args(args, target=target), session)
83+
elif suffix == "distance":
84+
run_distance(_mode_args(args, target=target), session)
85+
else:
86+
run_speed_and_distance(
87+
_mode_args(args, target=target, track_id=track_id), session,
88+
)
89+
outputs.append(target)
90+
timings.append((label, time.time() - t_mode))
91+
92+
total_time = time.time() - t_start
93+
print("── run-all: done ─────────────────────────────────────────────────────")
94+
for label, dt in timings:
95+
print(f" {label:<28} {dt:6.1f}s")
96+
print(f" {'TOTAL':<28} {total_time:6.1f}s")
97+
print(f" All {len(outputs)} renders used a single shared tracking pass.")
98+
print("Outputs:")
99+
for path in outputs:
100+
print(f" {path}")
101+
return outputs
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from sports.common.video_tracking import build_video_tracking_session
2+
from distance import _render_speed_distance_traces
3+
4+
5+
def run_speed_and_distance(args, session=None):
6+
if session is None:
7+
session = build_video_tracking_session(args, need_homography=True)
8+
focus = getattr(args, "track_id", None)
9+
_render_speed_distance_traces(
10+
args, session,
11+
focus_tid=int(focus) if focus is not None else None,
12+
append_end_card=False,
13+
)

0 commit comments

Comments
 (0)