# Courtvision indexed tracking format v2

Courtvision separates game metadata and native identity strings from indexed
observations. It stores source pose, centroids, ball, possession, clock flags,
revisions and raw event fields. Files may be gzip-wrapped (`.cvpose.gz`).
The browser also reads legacy v1 files and exports raw-layout v2 files.

## Binary envelope

All integer headers and float32 samples are little-endian.

| Byte offset | Contents |
| --- | --- |
| 0 | 8-byte ASCII magic: `CVPOSE2` followed by newline |
| 8 | uint32 metadata byte length, padded to a multiple of 4 |
| 12 | uint32 observation count N |
| 16 | uint32 total sample-value count F |
| 20 | UTF-8 JSON metadata, space padded |
| After metadata | N+1 uint32 offsets, in float32 values |
| After offsets | F float32 values, with optional byte shuffle |

The final offset equals F. Each record has `28 + 91 × person_count` values.
Absent scalar/coordinate observations use NaN; metadata uses JSON null.

| Header index | Contents |
| --- | --- |
| 0 | Seconds relative to `originUTC` |
| 1 | Source frame number, if supplied |
| 2–6 | Period, game clock seconds, shot clock seconds, home score, away score |
| 7–9 | Ball X/Y/Z, feet |
| 10 | Person count |
| 11 | Possession player slot, indexing `players` |
| 12 | Possession team slot, indexing `teams` |
| 13 | Source attack direction: +1 / −1 |
| 14 | Source game-active flag: 1 / 0 |
| 15–17 | Ball orientation rotation vector, radians |
| 18–20 | Ball spin vector, radians per second |
| 21–22 | Game/shot clock running flags: 1 / 0 |
| 23 | Observation kind: 1 continuous tracking, 2 event snapshot |
| 24 | Source revision |
| 25 | Feed priority: processed 3, delayed 2, live 1, snapshot −1 |
| 26 | Source possession count |
| 27 | Source ball confidence, when explicitly supplied |

Each person adds 91 values: metadata player slot, centroid X/Y/Z, then X/Y/Z
for each of the 29 joints. Native IDs remain exact strings in metadata; UUIDs
and NBA IDs are never stored as lossy float32 identity numbers.

Joint order: midHip, neck, nose, lEye, rEye, lEar, rEar, lShoulder,
rShoulder, lElbow, rElbow, lWrist, rWrist, lHip, rHip, lKnee, rKnee,
lAnkle, rAnkle, lHeel, rHeel, lBigToe, rBigToe, lSmallToe, rSmallToe,
lThumb, rThumb, lPinky, rPinky.

Coordinates use a center-court origin: X longitudinal, Y lateral, Z up.
Rendering maps to Three.js `(X, Z, -Y)`. Known source length units are converted
to feet; absent native unit declarations use the documented SI default.
Float32 conversion introduces numerical rounding, independently of source
physical measurement accuracy. Ball orientation is a rotation vector, not Euler
angles. Missing rotation/spin is not synthesized.

Metadata includes `version:2`, `frameHeader:28`, `personStride:91`, `joints`,
`units:"feet"`, `originUTC`, `gameKey`, native game IDs, players, teams, court,
competition, feed lineage, source units, direction history, events and warnings.
Events retain raw fields, event UTC, nearest-pose snapshot time, participants,
source frame basis and optional imported metrics. Event IDs include role and
nearest-frame identity so separate passer/receiver/deflector observations survive.

For `encoding:"float32"` (also the default when absent), sample bytes are raw
IEEE 754 values. For `encoding:"float32-byte-shuffle"`, only the sample-data
region is shuffled: all byte-0 values for F floats, then all byte-1, byte-2 and
byte-3 values. Restore byte `p` of float `i` from byte `p*F+i`. The offset table
and metadata are unchanged. This rearrangement is exactly reversible and helps
gzip compress smoothly varying coordinates. Unknown encodings are rejected.

## Conversion

Python 3.10+. JSON/JSONL, `.output`, extracted folders and ZIP need only the
standard library. The converter does not change source files.

```sh
python pack_pose.py game.jsonl --out game.cvpose.gz
python pack_pose.py game.output --out events.cvpose.gz
python pack_pose.py extracted_folder --out game.cvpose.gz
python pack_pose.py game.zip --out game.cvpose.gz
```

For 7z, install libarchive-c and system libarchive; the py7zr fallback uses a
temporary extraction folder and requires space for the uncompressed source.

```sh
pip install libarchive-c
python pack_pose.py game.7z --out game.cvpose.gz
# Portable fallback when system libarchive is unavailable:
pip install py7zr
```

Use `--no-shuffle` for the raw float32 layout. For separately indexed segments:

```sh
python pack_pose.py game.7z --chunks-dir game --chunk-seconds 30
python pack_pose.py mixed_games_folder --out-dir games
```

Select the generated compact segments together in Load game. A manifest is
included for external tools; the browser reads the selected segment files.
Mixed games require `--out-dir` or `--chunks-dir` and remain separate.
The converter holds normalized float32 data in memory while packing; it is not
a server streaming service. It reads original native feeds, not existing compact
files. Browser exports can combine and re-export compact input files.

## Display and measurement

Optional smoothing interpolates continuous observations at most 100 ms apart.
It does not create absent players, fill long gaps or interpolate event snapshots.
Clock-only samples retain their clocks. Snapshot capture time can differ from the
event's official UTC; both are retained. A saved clip retains the original UTC
origin, so it can be merged correctly with other segments from the same game.

Avatars are illustrative body geometry articulated by observed joints. Joint
validity measures finite coordinates, not tracking confidence. Centroid-only
observations use location markers without an invented body pose. Ground speed
uses approximately 100 ms of continuous centroid displacement. Knee and elbow
readouts use the 3D included angle: 180° means straight. Static hip height and
pinned-pose hip delta are not jump-height detections. Imported proprietary metrics
are displayed only when present; their raw source values remain available.

A matching game ID is required for automatic feed overlays. Marking files with
a different ID namespace can be explicitly attached to the current game. UTC is
preferred. Only an unambiguous period/game-clock match can align a marking with
no UTC. Different source frame-number systems are never assumed equivalent.
Raw event snapshots remain available in exported metadata when continuous pose
is present, but they do not replace continuous observations in the playback index.
