Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions examples/soccer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,36 @@ on the field.

Requires pitch keypoints (same flags as SPEED).

- `SPEED_AND_DISTANCE` — Speed + distance chips and per-player trace lines on the
radar minimap (no end-card). Default shows all players; pass `--track-id N` to
spotlight one player (dimmed background, single trace on minimap).

```bash
python main.py --source_video_path data/2e57b9_0.mp4 \
--target_video_path data/renders/2e57b9_0-speed-distance.mp4 \
--device mps --mode SPEED_AND_DISTANCE --max-frames 90

# spotlight one player
python main.py --source_video_path data/2e57b9_0.mp4 \
--target_video_path data/renders/2e57b9_0-speed-distance-spotlight.mp4 \
--device mps --mode SPEED_AND_DISTANCE --track-id 42 --max-frames 90
```

- `ALL` — Runs DIRECTION, SPEED, DISTANCE, SPEED_AND_DISTANCE (all players), and
SPEED_AND_DISTANCE (spotlight) in one pass. Builds a shared
`VideoTrackingSession` once; each mode writes a separate `-{suffix}.mp4` next to
the base `--target_video_path`. Spotlight track is the player with max cumulative
distance unless `--track-id` is set.

```bash
python main.py --source_video_path data/2e57b9_0.mp4 \
--target_video_path data/renders/2e57b9_0.mp4 \
--device mps --mode ALL --max-frames 90
```

Analytics flags: `--max-frames`, `--tracker`, `--track-id`, `--cache`, detector/model
paths (same as SPEED/DISTANCE).

## 🗺️ roadmap

- [ ] Add smoothing to eliminate flickering in RADAR mode.
Expand Down
15 changes: 14 additions & 1 deletion examples/soccer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@

from direction import run_direction
from distance import run_distance
from run_all import run_all
from speed import run_speed
from speed_and_distance import run_speed_and_distance

PARENT_DIR = os.path.dirname(os.path.abspath(__file__))
PLAYER_DETECTION_MODEL_PATH = os.path.join(PARENT_DIR, 'data/football-player-detection.pt')
Expand Down Expand Up @@ -88,9 +90,14 @@ class Mode(Enum):
DIRECTION = 'DIRECTION'
SPEED = 'SPEED'
DISTANCE = 'DISTANCE'
SPEED_AND_DISTANCE = 'SPEED_AND_DISTANCE'
ALL = 'ALL'


ANALYTICS_MODES = (Mode.DIRECTION, Mode.SPEED, Mode.DISTANCE)
ANALYTICS_MODES = (
Mode.DIRECTION, Mode.SPEED, Mode.DISTANCE,
Mode.SPEED_AND_DISTANCE, Mode.ALL,
)


def run_analytics_mode(mode: Mode, args: argparse.Namespace) -> None:
Expand All @@ -100,6 +107,10 @@ def run_analytics_mode(mode: Mode, args: argparse.Namespace) -> None:
run_speed(args)
elif mode == Mode.DISTANCE:
run_distance(args)
elif mode == Mode.SPEED_AND_DISTANCE:
run_speed_and_distance(args)
elif mode == Mode.ALL:
run_all(args)
else:
raise NotImplementedError(f"Mode {mode} is not an analytics mode.")

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

args = parser.parse_args()

Expand Down
101 changes: 101 additions & 0 deletions examples/soccer/run_all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""In-process orchestrator for all five analytics renders.

Builds a shared :class:`~sports.common.video_tracking.VideoTrackingSession` once
(one BoTSORT pass, one team-classifier fit, homography maps, kinematics) and drives
each mode renderer in-process. Each mode writes its own output video.
"""

from __future__ import annotations

import copy
import time
from pathlib import Path

from sports.common.video_tracking import build_video_tracking_session
from direction import run_direction
from distance import run_distance
from speed import run_speed
from speed_and_distance import run_speed_and_distance

_RENDER_PLAN = (
("direction", "DIRECTION"),
("speed", "SPEED"),
("distance", "DISTANCE"),
("speed-distance-all", "SPEED_AND_DISTANCE (all players)"),
("speed-distance-single", "SPEED_AND_DISTANCE (spotlight)"),
)


def _derive_target(base_path: str, suffix: str) -> str:
"""``data/renders/08fd33_0.mp4`` + ``direction`` → ``…/08fd33_0-direction.mp4``."""
p = Path(base_path)
return str(p.with_name(f"{p.stem}-{suffix}{p.suffix or '.mp4'}"))


def _mode_args(args, *, target: str, track_id: int | None = None):
"""Shallow copy of ``args`` with a per-mode target path and spotlight track id."""
new = copy.copy(args)
new.target_video_path = target
new.track_id = track_id
return new


def _pick_spotlight_track_id(tracks) -> int | None:
"""Pick the most-active tracked player (max cumulative distance) for the spotlight."""
best_tid: int | None = None
best_distance = -1.0
for tid, track in tracks.items():
distance = float(getattr(track, "distance_m", 0.0) or 0.0)
if distance > best_distance:
best_distance = distance
best_tid = int(tid)
return best_tid


def run_all(args) -> list[str]:
"""Build shared session once and render all five analytics videos in-process."""
t_start = time.time()

print("── run-all: building shared VideoTrackingSession ─────────")
session = build_video_tracking_session(args, need_homography=True)
print(f"Shared session ready in {time.time() - t_start:.1f}s.")

base = args.target_video_path
explicit_spotlight = getattr(args, "track_id", None)
spotlight_id = (
explicit_spotlight
if explicit_spotlight is not None
else _pick_spotlight_track_id(session.tracks)
)
print(f"Spotlight (SPEED_AND_DISTANCE) track id: {spotlight_id}")

outputs: list[str] = []
timings: list[tuple[str, float]] = []
for suffix, label in _RENDER_PLAN:
target = _derive_target(base, suffix)
track_id = spotlight_id if suffix == "speed-distance-single" else None
t_mode = time.time()
print(f"── run-all: rendering {label} → {target} ─────────")
if suffix == "direction":
run_direction(_mode_args(args, target=target), session)
elif suffix == "speed":
run_speed(_mode_args(args, target=target), session)
elif suffix == "distance":
run_distance(_mode_args(args, target=target), session)
else:
run_speed_and_distance(
_mode_args(args, target=target, track_id=track_id), session,
)
outputs.append(target)
timings.append((label, time.time() - t_mode))

total_time = time.time() - t_start
print("── run-all: done ─────────────────────────────────────────────────────")
for label, dt in timings:
print(f" {label:<28} {dt:6.1f}s")
print(f" {'TOTAL':<28} {total_time:6.1f}s")
print(f" All {len(outputs)} renders used a single shared tracking pass.")
print("Outputs:")
for path in outputs:
print(f" {path}")
return outputs
14 changes: 14 additions & 0 deletions examples/soccer/speed_and_distance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from sports.common.video_tracking import build_video_tracking_session
from distance import _render_speed_distance_traces


def run_speed_and_distance(args, session=None):
if session is None:
session = build_video_tracking_session(args, need_homography=True)
focus = getattr(args, "track_id", None)
_render_speed_distance_traces(
args, session,
show_speed=True,
focus_tid=int(focus) if focus is not None else None,
append_end_card=False,
)