Open-source match tracking for amateur football: every player tracked, team heatmaps, possession and distance from
a single Veo or YouTube camera
Below: 1 min of a Cape Town 5-a-side, 1799 frames, processed
in 56 s on one GPU. Source on GitHub
Dusk slice
11:07 to 12:07 of hhaWMwLlugE, 1799 frames at 1280x720
Possession
Grey
46%
Pink
22%
Unresolved
31%
Proxy: in each frame with a ball detection (YOLO COCO 'sports ball'), the team of the nearest player (foot point) within 3 player-heights owns the ball; frames without a ball inherit the last owner for up to 1 s, otherwise count as unresolved. Ball nearest a goalkeeper/other-kit player counts as unresolved. Ball detected in 76% of frames.
Distance covered
Grey
416 m
Pink
199 m
No homography. Player foot-point tracks in image pixels, camera pan removed by global phase correlation, smoothed (9-frame mean), steps faster than 0.5 frame-widths/s dropped. Metres = pixels / (frame width / 25 m), i.e. assumes the visible pitch width is 25 m and ignores zoom and perspective. Expect ±50%.
Heatmaps
Grey
Less timeMore time
Pink
Less timeMore time
Positions are in the camera's view, not on the pitch: the camera pans to follow play, so left and right are
edges of the frame, not ends of the pitch.
Roster
A-1
A-5
A-4
A-2
A-6
A-3
A-11
A-7
A-8
A-10
A-9
B-2
B-1
B-4
B-3
B-5
O-1
O-2
O-5
O-4
O-3
Identity is capped by the footage: this pipeline resolved 21 stable identities; on the reference night slice it found ~23 stable vs ~14–16 true players. Team-level stats do not depend on perfect identity.
Pipeline footyviz 0.2.0; detector yolo11m.pt (ultralytics 8.4.174); tracker ByteTrack (supervision 0.30.9); re-ID BoT-SORT + OSNet x1_0 msmt17 (boxmot 16.0.11); vision-LLM off. Run time 56 s for
1 min of footage.
Floodlights, near-identical kits and players in hoodies. Team stats hold up; player identity is where the pipeline
hits the limit of the footage.
0:00 to 1:00 of hhaWMwLlugE, 1800 frames at 1280x720
Possession
Grey
37%
Pink
26%
Unresolved
37%
Proxy: in each frame with a ball detection (YOLO COCO 'sports ball'), the team of the nearest player (foot point) within 3 player-heights owns the ball; frames without a ball inherit the last owner for up to 1 s, otherwise count as unresolved. Ball nearest a goalkeeper/other-kit player counts as unresolved. Ball detected in 63% of frames.
Distance covered
Grey
378 m
Pink
189 m
No homography. Player foot-point tracks in image pixels, camera pan removed by global phase correlation, smoothed (9-frame mean), steps faster than 0.5 frame-widths/s dropped. Metres = pixels / (frame width / 25 m), i.e. assumes the visible pitch width is 25 m and ignores zoom and perspective. Expect ±50%.
Heatmaps
Grey
Less timeMore time
Pink
Less timeMore time
Positions are in the camera's view, not on the pitch: the camera pans to follow play, so left and right are
edges of the frame, not ends of the pitch.
Roster
A-1
A-5
A-7
A-2
A-3
A-8
A-6
A-4
B-4
B-3
B-1
B-6
B-9
B-7
B-13
B-8
B-12
B-2
B-10
B-5
B-11
O-1
O-3
O-5
O-6
O-2
O-4
Identity is capped by the footage: this pipeline resolved 27 stable identities; on the reference night slice it found ~23 stable vs ~14–16 true players. Team-level stats do not depend on perfect identity.
Pipeline footyviz 0.2.0; detector yolo11m.pt (ultralytics 8.4.174); tracker ByteTrack (supervision 0.30.9); re-ID BoT-SORT + OSNet x1_0 msmt17 (boxmot 16.0.11); vision-LLM off. Run time 56 s for
1 min of footage.
git clone https://github.com/dan-slater/footyviz && cd footyviz
curl -LsSf https://astral.sh/uv/install.sh | sh # skip if you already have uv
uv venv --python 3.12 .venv
uv pip install -e .
.venv/bin/python -c "import torch; print('GPU' if torch.cuda.is_available() else 'CPU only')"
.venv/bin/python -m fv.job --url "https://www.youtube.com/watch?v=hhaWMwLlugE" --start-s 667 --end-s 727 --out out/my_match
.venv/bin/python -m json.tool out/my_match/stats.json | head -60
Swap in your own match URL and a window of up to 180 s. An NVIDIA GPU with CUDA is strongly recommended; --device cpu works but is slow. The optional vision-LLM roster review needs your own Gemini or OpenAI key in .env (GEMINI_API_KEY or OPENAI_API_KEY); it is off by default.