Download Tracking
Download raw player and ball tracking data.
Performance Note
This endpoint streams large files and may take several seconds to respond. Results are deterministic for a given analysis — we recommend caching the downloaded file on your end to avoid repeated transfers.
Request
GET /analyses/{id}/tracking/download?format=parquet
Query Parameters
| Parameter | Type | Default | Options | Description |
|---|---|---|---|---|
format |
string | parquet |
parquet, json |
Output format |
Response
Returns a file download:
- Parquet: Binary file, ~20-50 MB (recommended)
- JSON: Text file, ~200-500 MB
Data Schema
| Column | Type | Description |
|---|---|---|
timestamp |
int32 | Time in milliseconds |
role |
string | player, goal_keeper, ball, referee |
player_id |
string | Player identifier (e.g., "0-10") or null |
player_id_conf |
float16 | Confidence of player identification (0-1) |
x |
int16 | X position in centimeters |
y |
int16 | Y position in centimeters |
bbox_x |
int16 | Pixel X location of player in the video. Center of the player bounding box. Null on projected rows |
bbox_y |
int16 | Pixel Y location of player in the video. Bottom of the player bounding box. Null on projected rows |
status |
string | How this row was produced — see Row Status below |
overlap_partner_id |
string | On detected_overlap rows: the player this detection overlaps with |
position_conf |
float | On projected rows: confidence in the projected position (0-1) |
identity_conf |
float | On projected rows: confidence in the player identity (0-1) |
New in analysis version 0.5.36
The status, overlap_partner_id, position_conf, and identity_conf columns are present for analyses processed with version 0.5.36 or later. Older analyses do not include them.
Row Status
| Status | Meaning |
|---|---|
detected |
Player directly detected in the video frame |
detected_overlap |
Detected, but overlapping another player (overlap_partner_id says which) — identity attribution is less certain |
projected |
Player not visible in the frame; position projected by the tracking model |
projected_nocam |
Player outside all camera coverage; position projected with the highest uncertainty |
Off-Camera Players (Projected Rows)
From analysis version 0.5.36, the file covers the full squad: alongside the 25 FPS detection rows it includes projected rows (at 5 Hz) for players the camera does not currently see. Projected rows have pitch coordinates (x, y) but no pixel coordinates (bbox_x, bbox_y are null), and carry position_conf / identity_conf so you can gate on trust.
If you want the previous behavior (only players actually seen on camera), filter on status:
detected = df[df['status'].str.startswith('detected', na=False)]
Sample Data
timestamp role player_id x y status
3309680 player 0-4 6606 3256 detected
3309680 player 0-10 4520 4100 detected
3309680 player 1-3 2210 1180 projected
3309680 ball null 5200 3400 detected
3309720 player 0-4 6610 3259 detected
Understanding the Data
Coordinate System
- Origin (0,0) is bottom-left corner of pitch
- X: 0 to 10500 (centimeters) = 0 to 105 meters, left to right
- Y: 0 to 6800 (centimeters) = 0 to 68 meters, bottom to top
See Pitch Coordinates for details.
Frame Rate
Data is recorded at 25 FPS (frames per second):
- Timestamps increment by 40ms between frames
- 1 second = 25 data points per tracked entity
- Projected rows (off-camera players, version
0.5.36+) are sampled at 5 Hz (200ms steps)
Player Identification
player_id: Assigned when jersey is detected with confidenceplayer_id_conf: Higher = more certain identificationnullplayer_id: Player detected but jersey not readable
Working with the Data
Python (Parquet)
import pandas as pd
# Load tracking data
df = pd.read_parquet('tracking.parquet')
# Keep detection-backed rows only (0.5.36+ also contains projected rows,
# which would distort distance calculations)
if 'status' in df.columns:
df = df[df['status'].str.startswith('detected', na=False)]
# Filter to specific player
player_10 = df[df['player_id'] == '0-10']
# Convert to meters
player_10['x_m'] = player_10['x'] / 100
player_10['y_m'] = player_10['y'] / 100
# Calculate distance traveled
player_10 = player_10.sort_values('timestamp')
player_10['dx'] = player_10['x_m'].diff()
player_10['dy'] = player_10['y_m'].diff()
player_10['dist'] = (player_10['dx']**2 + player_10['dy']**2)**0.5
total_distance = player_10['dist'].sum()
print(f"Player 0-10 traveled {total_distance:.0f} meters")
JavaScript (JSON)
// Note: JSON files are large, consider streaming
const response = await fetch(
`/api/v1/analyses/${id}/tracking/download?format=json`,
{ headers }
);
const tracking = await response.json();
// Group by timestamp for frame-by-frame analysis
const frames = {};
tracking.forEach(row => {
if (!frames[row.timestamp]) frames[row.timestamp] = [];
frames[row.timestamp].push(row);
});
Format Comparison
| Aspect | Parquet | JSON |
|---|---|---|
| File size | 20-50 MB | 200-500 MB |
| Load time | Fast | Slow |
| Streaming | No | Yes |
| Language support | Python, R, Spark | Universal |
| Recommended for | Analysis | Web apps |
Example
curl -X GET "https://aiontheball.nl/api/v1/analyses/1246/tracking/download?format=parquet" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-o tracking.parquet
curl -X GET "https://aiontheball.nl/api/v1/analyses/1246/tracking/download?format=json" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-o tracking.json
import requests
import pandas as pd
from io import BytesIO
response = requests.get(
'https://aiontheball.nl/api/v1/analyses/1246/tracking/download',
headers={'Authorization': f'Bearer {token}'},
params={'format': 'parquet'}
)
df = pd.read_parquet(BytesIO(response.content))
print(f"Loaded {len(df)} tracking points")
Large Files
Tracking files can be large. For web applications, consider:
- Using the Summary endpoint for aggregated stats
- Streaming JSON responses
- Server-side processing with Parquet