Sports Performance Analysis · output contract

Internal · confidential · not for distribution

positions_60_90.json

The 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.

Shape

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.

Fields

Top level.

FieldTypeMeaning
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.

FieldTypeMeaning
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.

FieldTypeMeaning
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.

What a consumer can rely on

A player’s position is the bottom centre of the box, not the centre

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.

Out-of-pitch positions are flagged, never clipped

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.

Non-players are carried through with their label, not dropped

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.

Frame keys are original indices, and coordinates are source pixels

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.

What you must not assume

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 ball in flight is projected as if it were rolling

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.

Track IDs do not survive a camera cut, and re-identification is not built

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.

A frame does not contain all 22 players, and never will from this feed

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.

No figure in this file is an accuracy figure

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 width was assumed, so across-pitch coordinates carry about 1.5 m near the touchlines

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.

A label is a colour claim, and it is only as good as the run that produced it

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.

The current file, in numbers

QuantityValueNote
Windowt = 60–90 sframes 1800 to 2697, stride 3
Sampled frames30010 Hz from a 30 fps source
Player rows5,607every detection the tracker confirmed
Distinct track IDs56for about 20 people on camera; see the overview
Rows inside the pitch99.1%51 flagged out of bounds, none clipped
Unresolved labels80.14% of detections, down from 125
Ball detected20969.7% of frames
Ball interpolated47short runs inside a linked segment only
Ball null4414.7%; all 44 have the ball touching a player
File size694 KBfor 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.