Sports Performance Analysis · output contract
Internal · confidential · not for distributionThe file the tactical model consumes: every detected person and the ball, per frame, in metres on the pitch. Everything else in the pipeline exists to produce this. Back to the overview.
One object. Metadata at the top, then frames keyed by the original 0-based frame index of
the source video. 694 KB for 30 seconds.
{ "source_tracks": "data/tracks_60_90_stitched.json", "source_identity": "data/identity_60_90_stitched.json", "homography": "data/homographies_60_90.json", "pitch_m": [102.0, 68.0], // length, width. AUTHORITATIVE "pitch_length_source": "calibration", // or "default 105, NOT fitted" "origin": "centre spot; ...", // prose, and currently stale "position": "bottom centre of the detection box, mapped through the ground-plane homography", "src_fps": 30.0, "frame_stride": 3, // so 10 Hz "frames": { "1800": { // original frame index, string key "players": [ {"id": 1, "xy": [-0.11, 13.91], "label": "denmark", "label_src": "track", "on_pitch": true, "px": [1735.7, 762.4]} // ... one object per detected person in this frame ], "ball": {"xy": [-23.75, -4.62], "px": [794.4, 460.4], "source": "detected", "det": "yolo", "ground_plane_assumed": true} }, "1896": { "players": [ /* ... */ ], "ball": null // key ALWAYS present; null is a gap } } }
The block above scrolls sideways on a phone; the page does not.
Top level.
| Field | Type | Meaning |
|---|---|---|
pitch_m |
[number, number] | Length and width in metres of the model pitch the coordinates are expressed on.
Read this, do not assume 105 × 68. Currently
[102.0, 68.0]. |
pitch_length_source |
string | "calibration" means length was fitted as a free parameter and
checked against the centre circle. The other value is "default 105, NOT fitted",
which is a warning, not a measurement. |
origin |
string | Prose description of the coordinate frame. Currently stale — see the note below. |
position |
string | Prose statement of what point on a player is reported. Bottom centre of the box. |
src_fps, frame_stride |
number | Source frame rate and sampling stride. 30.0 / 3 is 10 Hz.
Divide a frame key by src_fps for seconds into the source video. |
source_tracks, source_identity, homography |
string | Provenance: the three sidecars this file was joined from, so any row can be traced back to the stage that produced it. |
frames |
object | Keyed by original 0-based frame index as a string. Keys are sparse: only sampled frames appear. |
A player record.
| Field | Type | Meaning |
|---|---|---|
id |
integer | Track ID, stable within this window only. Not contiguous and not
dense — the current file has 56 distinct IDs with values up to 455. Never assume
id indexes an array. |
xy |
[number, number] | Position in metres. x runs along the length, y across
the width, origin at the centre spot. Rounded to 2 dp. |
px |
[number, number] | The same point in source video pixels (1920 × 1080), before the homography. Kept so a row can be drawn back on the frame it came from, which is how every claim on the overview page was checked. |
label |
string | One of germany, denmark, referee,
keeper, hivis, unknown.
A colour claim, not a role claim. hivis is ball crew and stewards;
unknown is a torso window that straddled two people and was deliberately not guessed. |
label_src |
string | "track" when the label comes from the whole track’s colour fit,
"detection" when that track was found to contain two different people and this single
detection was labelled on its own. |
on_pitch |
boolean | false when xy falls outside the playing surface.
The row is kept regardless. |
The ball record, or null.
| Field | Type | Meaning |
|---|---|---|
xy, px |
[number, number] | As above: metres, and the source pixel it came from. |
source |
string | "detected" or "interpolated".
Always branch on this. In the current window: 209 detected, 47 interpolated,
44 frames null. |
det |
string or null | Which candidate source found it: "yolo" or "blob".
null on an interpolated frame. |
ground_plane_assumed |
boolean | Always true. The ball is projected as if it were rolling on the
grass. The field most likely to be ignored and most likely to hurt — see below. |
The homography maps the ground plane, and a box centre is roughly chest height, which projects metres
further from camera than the player is actually standing. The error grows with distance, so it is a
systematic bias, not noise, and it cannot be averaged away. Anything re-deriving a
position from px must use the same convention.
Three completely different things produce an out-of-bounds coordinate: a player genuinely standing over
the touchline, a detection on a steward behind the hoardings, and a calibration error. Clipping silently
converts the second and third into plausible-looking football. on_pitch carries the answer
and the caller decides.
In the current window 51 of 5,607 rows are out of bounds (0.91%), and they are not evenly spread: they cluster on hi-vis tracks behind the boards, which is the detection problem the overview page describes rather than a calibration problem.
Ball crew and stewards are tracked and labelled hivis. Dropping them here would hide a
detection problem from anyone reading this file. Filter them out at the point of use, and know that you
are doing it.
Every stage in this pipeline writes its own sidecar keyed by the original 0-based frame index, with coordinates in source pixels and the sampling stride recorded in the file. Two independently written components can therefore be joined without coordination beyond this contract.
The origin string is stale, and pitch_m is the truth.
It currently reads "centre spot; x along length -52.5..52.5, y across width -34..34", which
is the hardcoded regulation half-length. This ground fitted at 102 m, so the real range is
roughly ±51.
Compute half-length as pitch_m[0] / 2 and half-width as pitch_m[1] / 2.
Do not parse the sentence. It is a comment that was not updated when length became a fitted
parameter, and it is exactly the kind of thing a consumer reads once and hardcodes.
A homography maps one plane to another. The ball’s xy is therefore where the camera
ray through the ball meets the grass, which for a ball in the air is somewhere beyond where the
ball is. ground_plane_assumed is true on every ball record because nothing in
this pipeline estimates ball height.
A long pass or a cross will read as travelling further than it did, and the error is largest in exactly the events a tactical model cares about most. If the model needs airborne ball position, that is a separate piece of work, not a tuning parameter.
An id is stable only for as long as the tracker holds the person. There are
4 hard cuts in 180 seconds of this footage and every one resets identity
completely, and a single fast pan at t = 83.4 s does the same thing inside this very
window. Treat an ID as an identity within a shot, never across the file.
Between 16 and 23 of the 22 players are in frame at any moment. The absence of a player from a frame means only that he was not visible. It is not evidence about where he was. Anything that reconstructs a full team state has to infer the rest, and nothing here does.
There is no ground truth for this footage, so there is no measured recall for detection, no measured position error in metres, and no identity metric beyond self-consistency. 99.1% of positions landing inside the pitch says the calibration is not absurd; it does not say a player is where the file says he is.
The independent checks that do exist are physical rather than labelled: the centre circle back-projects to 9.12 m against a 9.15 m Law value, and detection boxes turned into player heights give a median of 1.75 m over 297 boxes.
pitch_m[0] was fitted. pitch_m[1] was not: 68 m is an assumption carried
straight into every y. The uncertainty is small near the middle and about a body width at
the edges, which is where wide players and the offside line live.
Identification originally ran before track stitching, so a merged track inherited the label of
whichever fragment was its root. One Denmark player with 117 detections came out unknown and
drew no icon at all. Re-running identification on the repaired tracks took unresolved detections from
125 to 8 (0.14%).
The current file’s 56 tracks carry 51 track-level labels, 44 high confidence and 7 low, plus 5 labelled per detection. That set has not been re-checked crop by crop; the “110 of 110 correct by eye” verification belongs to the earlier, pre-stitch run.
| Quantity | Value | Note |
|---|---|---|
| Window | t = 60–90 s | frames 1800 to 2697, stride 3 |
| Sampled frames | 300 | 10 Hz from a 30 fps source |
| Player rows | 5,607 | every detection the tracker confirmed |
| Distinct track IDs | 56 | for about 20 people on camera; see the overview |
| Rows inside the pitch | 99.1% | 51 flagged out of bounds, none clipped |
| Unresolved labels | 8 | 0.14% of detections, down from 125 |
| Ball detected | 209 | 69.7% of frames |
| Ball interpolated | 47 | short runs inside a linked segment only |
| Ball null | 44 | 14.7%; all 44 have the ball touching a player |
| File size | 694 KB | for 30 seconds at 10 Hz |
The 56 IDs are the default tracker plus offline stitching, which is what this window was built with. The best measured configuration gives 24 on a comparable window and has not yet been re-run over this one — see track identity on the overview.