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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pt filter=lfs diff=lfs merge=lfs -text
*.onnx filter=lfs diff=lfs merge=lfs -text
Binary file added data/COLLABORA_02_RGB.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added data/Chinedu-Obasi_2684938.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions demo/football/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Football demo

Real-time football broadcast overlay: **detection → tracking → overlay**
(`pyml_yolo`/`pyml_objectdetector` -> `pyml_tracker` -> `pyml_football_overlay`).

The overlay draws a foot ellipse per player coloured by team (red/blue, voted
from jersey hue), a gold ellipse for referees, motion trails (off by default),
and a focal-player HUD with headshot, ball contacts, and distance travelled.
Players whose team isn't decided yet (and unclassifiable kits, e.g. the
goalkeeper) are left unmarked rather than drawn in a placeholder colour. The
ball is tracked for contact counting but its marker is off by default.

## Run

```bash
# file -> annotated MP4
demo/football/run.sh
demo/football/run.sh 08fd33_4.mp4 demo/football/out.mp4 1280x720

# file -> live on-screen
demo/football/run.sh display
demo/football/run.sh display 08fd33_4.mp4 1280x720

# live camera -> on-screen
demo/football/run.sh camera /dev/video0
```

## Environment knobs

| Var | Default | Meaning |
|------------|---------|---------|
| `BACKEND` | `pt` | `pt` = PyTorch `pyml_yolo`; `fp16` = ONNX FP16 via `pyml_objectdetector` (CUDA). |
| `INTERVAL` | `3` | Run detection every Nth frame; the tracker/overlay still update every frame, so it stays smooth at ~N× less inference cost. The main real-time lever. |

```bash
BACKEND=fp16 demo/football/run.sh display # faster inference path
INTERVAL=5 demo/football/run.sh display # detect every 5th frame
INTERVAL=1 demo/football/run.sh # detect every frame (max accuracy)
```


156 changes: 156 additions & 0 deletions demo/football/onnx_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
# Run a video through the ONNX (fp16) football pipeline.
#
# detector (onnx) -> pyml_tracker -> pyml_football_overlay
#
# Usage:
# python demo/football/onnx_loop.py INPUT.mp4 # live display, looping
# python demo/football/onnx_loop.py INPUT.mp4 OUTPUT.mp4 # write annotated mp4
# (self-contained: finds the repo venv + plugins and re-execs into them)
import os
import sys
import glob

REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
VENV = os.path.join(REPO, ".venv")
MODEL = os.path.join(REPO, "models/football/football_fp16.onnx")
os.environ["GST_PLUGIN_PATH"] = (
os.path.join(REPO, "plugins") + os.pathsep + os.environ.get("GST_PLUGIN_PATH", "")
)
if not os.environ.get("_ONNX_LOOP_REEXEC") and os.path.isdir(VENV):
os.environ["VIRTUAL_ENV"] = VENV
os.environ["PATH"] = (
os.path.join(VENV, "bin") + os.pathsep + os.environ.get("PATH", "")
)
libs = sorted(
set(
glob.glob(
os.path.join(
VENV, "lib", "python*", "site-packages", "nvidia", "*", "lib"
)
)
)
)
if libs:
os.environ["LD_LIBRARY_PATH"] = os.pathsep.join(
[*libs, os.environ.get("LD_LIBRARY_PATH", "")]
)
os.environ["_ONNX_LOOP_REEXEC"] = "1"
pybin = os.path.join(VENV, "bin", "python")
exe = pybin if os.path.exists(pybin) else sys.executable
os.execv(exe, [exe, *sys.argv])

import gi # noqa: E402

gi.require_version("Gst", "1.0")
from gi.repository import Gst, GLib # noqa: E402

Gst.init(None)


def on_message(bus, message, loop, pipeline, do_loop):
t = message.type

if t == Gst.MessageType.EOS:
if do_loop:
# Display mode: seek back to the start to loop the clip.
print("Looping...")
if not pipeline.seek_simple(
Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 0
):
print("Failed to seek back to start", file=sys.stderr)
loop.quit()
else:
# mp4 mode: end of file, the muxer has finalized the file.
loop.quit()

elif t == Gst.MessageType.ERROR:
err, debug = message.parse_error()
print(f"ERROR: {err}", file=sys.stderr)
if debug:
print(f"DEBUG: {debug}", file=sys.stderr)
loop.quit()


def main():
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} INPUT.mp4 [OUTPUT.mp4]", file=sys.stderr)
print(
" no OUTPUT -> live display (looping); OUTPUT -> write annotated mp4",
file=sys.stderr,
)
sys.exit(1)
video = os.path.abspath(sys.argv[1])
out = os.path.abspath(sys.argv[2]) if len(sys.argv) > 2 else None

# Shared detection + overlay chain. Feed the ORIGINAL resolution:
# pyml_objectdetector letterboxes to the model's 640 internally for
# inference and maps boxes back, so the overlay stays full-res.
chain = (
f"filesrc location={video} ! "
"decodebin ! videoconvert ! video/x-raw,format=RGB ! "
"queue max-size-buffers=8 max-size-time=0 max-size-bytes=0 ! "
"pyml_objectdetector engine-name=onnx "
f" model-name={MODEL} device=cuda:0 "
" input-format=nchw post-process=anchor_free interval=1 "
" confidence=0.1 nms-iou=0.7 ! "
"queue max-size-buffers=8 max-size-time=0 max-size-bytes=0 ! "
"pyml_tracker tracker-type=bytetrack new-track-confidence=0.25 ! "
"videoconvert ! video/x-raw,format=RGBA ! "
"queue max-size-buffers=8 max-size-time=0 max-size-bytes=0 ! "
"pyml_football_overlay class-names=ball,goalkeeper,player,referee "
" team-colors=true trails=false show-ids=false show-labels=false "
" draw-from-detections=true min-confidence=0 merge-iou=0.5 "
" position-smoothing=0.7 highlight-focal=false ! "
)
if out:
pipeline_description = (
chain + "queue max-size-buffers=8 max-size-time=0 max-size-bytes=0 ! "
"videoconvert ! openh264enc ! h264parse ! mp4mux ! "
f"filesink location={out}"
)
do_loop = False
else:
# Pre-roll buffer absorbs inference jitter for smooth real-time display.
pipeline_description = (
chain + "queue max-size-buffers=600 max-size-time=0 max-size-bytes=0 "
" min-threshold-buffers=30 ! "
"videoconvert ! autovideosink sync=true"
)
do_loop = True

print(pipeline_description)
print(f"writing -> {out}" if out else "live display (looping)")

try:
pipeline = Gst.parse_launch(pipeline_description)
except GLib.Error as e:
print(f"Failed to create pipeline: {e}", file=sys.stderr)
sys.exit(1)

loop = GLib.MainLoop()

bus = pipeline.get_bus()
bus.add_signal_watch()
bus.connect("message", on_message, loop, pipeline, do_loop)

pipeline.set_state(Gst.State.PLAYING)

try:
loop.run()
except KeyboardInterrupt:
if out:
# Finalize the mp4 on Ctrl-C: send EOS and wait for the muxer to
# flush its trailer, otherwise the file is left unplayable.
pipeline.send_event(Gst.Event.new_eos())
bus.timed_pop_filtered(
5 * Gst.SECOND, Gst.MessageType.EOS | Gst.MessageType.ERROR
)
finally:
pipeline.set_state(Gst.State.NULL)
if out:
print(f"Done: {out}")


if __name__ == "__main__":
main()
97 changes: 97 additions & 0 deletions demo/football/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Football broadcast-overlay demo.
#
# Ppipeline:
# detector -> pyml_tracker (ByteTrack) -> pyml_football_overlay
#
# Usage:
# demo/football/run.sh [INPUT.mp4] [OUTPUT.mp4] [WxH] # file -> annotated mp4
# demo/football/run.sh display [INPUT.mp4] [WxH] # file -> live on-screen
# demo/football/run.sh camera [/dev/videoN] [WxH] # live camera -> on-screen
set -euo pipefail

REPO="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$REPO"
source .venv/bin/activate
export GST_PLUGIN_PATH="$REPO/plugins:${GST_PLUGIN_PATH:-}"

BACKEND="${BACKEND:-pt}"
INTERVAL="${INTERVAL:-3}" # run detection every Nth frame; tracker/overlay stay per-frame
CONF="${CONF:-0.1}" # detector confidence threshold (low = more detections)
IOU="${IOU:-0.7}" # NMS IoU (ultralytics/football_analyzer default)
NEWTRACK="${NEWTRACK:-0.25}" # min confidence to START a new track (ByteTrack gate; kills ghosts)
DRAWCONF="${DRAWCONF:-0}" # min confidence to DRAW a detection (0 = draw all; raise to trim weak boxes)
MERGE="${MERGE:-0.5}" # collapse overlapping boxes (lower=merge more; 0 disables) so one player=one circle
SMOOTH="${SMOOTH:-0.6}" # temporal EMA on circle positions (0=off, higher=smoother but more lag)
CLASSES="ball,goalkeeper,player,referee"
TRACK="pyml_tracker tracker-type=bytetrack new-track-confidence=$NEWTRACK"
# Detection-based overlay: circles sit on the raw per-frame detections (no
# tracking drift/phantoms/doubles); merge collapses overlaps and
# position-smoothing low-passes the positions. DRAWCONF defaults 0 so no
# detection is hidden; the tracker still runs so the HUD keeps its stats.
OVERLAY="pyml_football_overlay class-names=$CLASSES team-colors=true trails=false show-ids=false show-labels=false draw-from-detections=true min-confidence=$DRAWCONF merge-iou=$MERGE position-smoothing=$SMOOTH highlight-focal=false"

if [[ "$BACKEND" == "fp16" ]]; then
export LD_LIBRARY_PATH="$(python -c "import os,nvidia,glob;b=os.path.dirname(nvidia.__file__);print(':'.join(sorted(set(glob.glob(b+'/*/lib')))))"):${LD_LIBRARY_PATH:-}"
DETECT="pyml_objectdetector engine-name=onnx model-name=models/football/football_fp16.onnx device=cuda:0 input-format=nchw post-process=anchor_free interval=$INTERVAL"
IN_FMT="RGB"; FORCE_SQUARE=1
else
DETECT="pyml_yolo model-name=models/football/football device=cuda:0 interval=$INTERVAL confidence=$CONF nms-iou=$IOU"
IN_FMT="RGBA"; FORCE_SQUARE=0
fi

POST_DETECT="$TRACK"
[[ "$IN_FMT" == "RGB" ]] && POST_DETECT="$TRACK ! videoconvert ! video/x-raw,format=RGBA"

# A queue at each stage boundary turns the serial chain into a threaded
# pipeline: while inference runs on frame N, the sink renders N-1 and the
# decoder reads N+1. Nothing is dropped (leaky=no, the default).
Q="queue max-size-buffers=8 max-size-time=0 max-size-bytes=0"
# Pre-roll buffer before the display sink: build a head start of processed
# frames so real-time playback (sync=true) rides out per-frame inference
# jitter without stuttering. Smooths jitter, not a sustained throughput
# deficit -- if inference can't keep up on average, playback just lags
# (still no drops). Lower INTERVAL/raise the head start if it falls behind.
PREROLL="queue max-size-buffers=600 max-size-time=0 max-size-bytes=0 min-threshold-buffers=30"

# detector -> tracker -> overlay, with a thread boundary at each hop.
CHAIN="$Q ! $DETECT ! $Q ! $POST_DETECT ! $Q ! $OVERLAY"

MODE="${1:-file}"
if [[ "$MODE" == "camera" ]]; then
DEV="${2:-/dev/video0}"; SIZE="${3:-1280x720}"
[[ "$FORCE_SQUARE" == "1" ]] && SIZE="640x640"
W="${SIZE%x*}"; H="${SIZE#*x}"
echo "[$BACKEND] live camera $DEV @ ${W}x${H} -> autovideosink (needs a display)"
exec gst-launch-1.0 -e \
v4l2src device="$DEV" ! videoconvert ! videoscale \
! "video/x-raw,width=${W},height=${H},format=${IN_FMT}" \
! $CHAIN \
! $Q ! videoconvert ! autovideosink sync=false
elif [[ "$MODE" == "display" ]]; then
IN="${2:-data/soccer_tracking.mp4}"
SIZE="${3:-1280x720}"
[[ "$FORCE_SQUARE" == "1" ]] && SIZE="640x640"
W="${SIZE%x*}"; H="${SIZE#*x}"
[[ -f "$IN" ]] || { echo "input not found: $IN" >&2; exit 1; }
echo "[$BACKEND] '$IN' @ ${W}x${H} -> live display (real-time, sync=true)"
exec gst-launch-1.0 -e \
filesrc location="$IN" ! decodebin ! videoconvert ! videoscale \
! "video/x-raw,width=${W},height=${H},format=${IN_FMT}" \
! $CHAIN \
! $PREROLL ! videoconvert ! autovideosink sync=true
else
IN="${1:-data/soccer_tracking.mp4}"
OUT="${2:-demo/football/out.mp4}"
SIZE="${3:-1280x720}"
[[ "$FORCE_SQUARE" == "1" ]] && SIZE="640x640"
W="${SIZE%x*}"; H="${SIZE#*x}"
[[ -f "$IN" ]] || { echo "input not found: $IN" >&2; exit 1; }
echo "[$BACKEND] '$IN' @ ${W}x${H} -> '$OUT'"
gst-launch-1.0 -e \
filesrc location="$IN" ! decodebin ! videoconvert ! videoscale \
! "video/x-raw,width=${W},height=${H},format=${IN_FMT}" \
! $CHAIN \
! $Q ! videoconvert ! openh264enc ! h264parse ! mp4mux ! filesink location="$OUT"
echo "Done: $OUT"
fi
3 changes: 3 additions & 0 deletions models/football/football.onnx
Git LFS file not shown
3 changes: 3 additions & 0 deletions models/football/football.pt
Git LFS file not shown
3 changes: 3 additions & 0 deletions models/football/football_fp16.onnx
Git LFS file not shown
3 changes: 3 additions & 0 deletions models/football/football_int8.onnx
Git LFS file not shown
Loading
Loading