Skip to content
Open
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
24 changes: 24 additions & 0 deletions examples/soccer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,30 @@ on the field.

https://github.com/user-attachments/assets/263b4cd0-2185-4ed3-9be2-cf4d8f5bfa67

## 🎬 keyframe generation

Generate square-crop `(t, o)` keyframes (timestamp in seconds, offset in pixels from the
left edge of a 1920×1080 frame) using the provided models:

```bash
python generate_keyframes.py \
--source_video_path data/2e57b9_0.mp4 \
--output_path data/2e57b9_0-keyframes.json \
--device mps
```

The output JSON looks like:

```json
[
{"t": 0.0, "o": 256},
{"t": 4.8, "o": 312}
]
```

Tune smoothing, motion limits, and compression with the CLI flags (`--smoothing_alpha`,
`--max_speed`, `--epsilon`, etc.) to match your editing workflow.

## 🗺️ roadmap

- [ ] Add smoothing to eliminate flickering in RADAR mode.
Expand Down
76 changes: 76 additions & 0 deletions examples/soccer/generate_keyframes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import argparse
import json
import sys
from typing import List

from sports.pipelines import FOOTBALL, TENNIS, Keyframe, KeyframeGenerator, SportConfig

SPORTS: dict[str, SportConfig] = {
"football": FOOTBALL,
"soccer": FOOTBALL,
"tennis": TENNIS,
}


def write_output(path: str, keyframes: List[Keyframe]) -> None:
payload = [keyframe.as_dict() for keyframe in keyframes]
with open(path, "w", encoding="utf-8") as file:
json.dump(payload, file, indent=2)


def main() -> None:
parser = argparse.ArgumentParser(
description="Generate (t, o) crop keyframes for sports footage using SAM 3."
)
parser.add_argument("--source_video_path", type=str, required=True)
parser.add_argument(
"--sport",
type=str,
default="football",
choices=list(SPORTS.keys()),
help="Sport type. Default: football.",
)
parser.add_argument(
"--output_path",
type=str,
help="Where to write keyframes JSON. Prints to stdout if omitted.",
)
parser.add_argument("--model_path", type=str, default="sam3.pt")
parser.add_argument("--device", type=str, default="cuda")
parser.add_argument("--crop_width", type=int, default=1080)
parser.add_argument("--margin", type=int, default=32)
parser.add_argument("--smoothing_alpha", type=float, default=0.25)
parser.add_argument("--max_speed", type=float, default=480.0)
parser.add_argument(
"--epsilon_frac",
type=float,
default=0.008,
help="RDP tolerance as fraction of frame width (default 0.008 = 0.8%%).",
)
args = parser.parse_args()

sport = SPORTS[args.sport]

generator = KeyframeGenerator(
sport=sport,
model_path=args.model_path,
device=args.device,
crop_width_px=args.crop_width,
margin_px=args.margin,
smoothing_alpha=args.smoothing_alpha,
max_speed_px_per_s=args.max_speed,
epsilon_frac=args.epsilon_frac,
)

keyframes = generator.generate(args.source_video_path)

if args.output_path:
write_output(args.output_path, keyframes)
else:
for keyframe in keyframes:
timestamp, offset = keyframe.as_pair()
print(f"{timestamp:.3f},{offset}")


if __name__ == "__main__":
main()
Loading