THE LABEL-FREE CHAIN

Six stages, one broadcast feed, and no target-match labels at inference.

A badminton broadcast is a single moving camera, two players in motion blur, and a shuttle that is four pixels wide at the back of the court. COURTSIDE turns that into a scouting report: who moved where, who hit what, who was under pressure, and how every point ended.

The interesting constraint is that target-match annotations are reserved for evaluation. Rally boundaries, contacts, shot types, scores, and which end each player is on all have to come from the broadcast and court calibration. Annotations from other matches train the shot classifier; the target match’s annotations measure what it recovered and what it missed.

SCOPE
Solo build
design, CV, backend, frontend
PROCESSING
Resumable
saved tracks reused across analyses
PYTHON
Stage CLIs
vision, scoring and analytics
MATCHES
4 singles + doubles
human labels for the singles matches
01

Player tracking

0.57 m
WHAT’S HARD

There is no court-line detector. Every detection is projected through one homography calibrated by hand from four outer corners — and the corners sit on the outer edge of a painted line, so reading them off a gridded crop beats every automatic pick tried.

THE APPROACH

Pose gives ankles, but ankles float above the floor and bias depth. The ground point blends the ankle down toward the box bottom (0.65 of the way), which is what pulls far-court error from metres to centimetres.

0.57 m median position error vs 1058 labeled strokes, India Open
IN CONTEXTnear court 0.549 m, far 0.585 m
YOLO11x-pose · ByteTrack · hand-calibrated homography
02

Shuttle tracking

99.8%
WHAT’S HARD

The frame offset between our video and the labels is not one number. Player feet are insensitive — their error curve is flat over ±10 frames — but the shuttle moves 20+ px per frame and pins the true contact sharply, and the right offset differs per match.

THE APPROACH

Two offsets are stored per match: a global +6 for the (flat) player tracks, and a fitted per-match shuttle offset used everywhere contact timing matters.

99.8% of 980 labeled hit points found across the full broadcast
IN CONTEXT115 px median residual — the same scale as the human click noise
TrackNetV3 on MPS · 128,400 frames
03

Hit detection

F1 88.8
WHAT’S HARD

A smash off a descending lift has no 2D direction change at all. Detecting contacts by looking for the shuttle to turn around misses the single most important shot in the sport.

THE APPROACH

Two per-frame signals are unioned over ±4-frame velocities: an acceleration term (|Δv| ≥ 30 px/f) that catches smashes, and a reversal term weighted by speed that catches slow net play. Serves can’t kink — they start from rest — so they get a dedicated motion-onset pass.

F1 88.8 contacts found within ±6 frames of the India Open labels
IN CONTEXT86.7% precision · 91.0% recall
acceleration + reversals · serve onset · wrist motion
04

Shot classification

75.7%
WHAT’S HARD

Drives and pushes look nearly identical from geometry alone. Position, landing and timing get you to the mid-80s on easy classes and stall on the ones that decide rallies — you need the body, not the trajectory.

THE APPROACH

BST-0 reads our YOLO pose and TrackNetV3 shuttle windows. A stacked classifier combines its probabilities with court geometry and estimated contact height. Training uses detected contacts from other matches, so it sees the timing noise present when the system is deployed.

75.7% 10-class agreement on matched India Open contacts
IN CONTEXTThe stacked classifier trains on other matches; missed contacts are excluded here
BST-0 + geometry + estimated contact height
05

Rally segmentation

F1 97.6
WHAT’S HARD

Shuttle visibility is useless as a rally signal. TrackNetV3 fires happily on replays and warm-ups — 83–94% visible during rallies versus 77–82% during the gaps. The two distributions barely separate.

THE APPROACH

Rallies are camera runs where both players hold in-court tracks (98% of rally frames, 7–18% of gaps), with bounding-box height bands self-calibrated off the dominant cluster to throw out close-ups and zoomed replays, then split at dead-shuttle restarts.

F1 97.6 rally-window agreement against India Open annotations
IN CONTEXTScore totals help identify missing rallies; equal counts do not guarantee alignment
camera runs · restart splitting · score-driven rescue
06

Score OCR

80 events
WHAT’S HARD

The BWF score digits are about 12 pixels tall. OCR engines return noise. And the overlay updates two to five seconds late — sometimes fifteen-plus seconds behind, when the broadcast cuts to a replay.

THE APPROACH

Digit templates are bootstrapped from a labeled match. A rule-constrained decoder repairs score sequences; delayed events are associated with already-ended rallies. The next server and service-court parity provide independent checks. Correct final scores do not make every inferred rally winner exact.

80 events score changes read from the India Open broadcast
IN CONTEXTScoring rules repair missed readings; per-rally attribution remains approximate
digit templates · rule-constrained score decoding · serve parity
THE NUMBER THAT ACTUALLY MATTERS

Measure the whole pipeline, including the contacts it misses.

Shot accuracy on matched contacts excludes undetected strokes. The Validation view also reports full stroke accuracy: the contact must be found, its hitter must be correct, and its shot type must match the label. The classifier is trained on other matches, while detectors and pretrained models have separate development histories. Compare all four singles matches before treating an improvement as general.

SEE MATCH VALIDATION →
THINGS THAT DIDN’T WORK

Discarded approaches

Most of the build time went here. Each of these looked obviously correct on paper and failed against the footage for a specific, measurable reason.

Pixel court-line check for segmentation
Corners sit on the outer edge of the painted line and per-line offsets run 2–5 px. The check rejected real rallies as often as replays.
Serve-stance gate
Tracks are missing entirely in the half-second before the serve in 56 of 84 rallies — there is no stance to detect.
Static-rest detection to split rallies
A lob leaves the top of the frame for up to 62 frames mid-rally, which is indistinguishable from a dead shuttle.
Attributing the server from the first detected contact
The contact detector misses far-side serves — they sit at the window edge as an extremum. The server is derived from the score instead: the winner serves next.
A scoreboard gate on singles rally segmentation
It helped doubles and hurt singles: the set-start graphic is absent for a beat and one tournament’s badge defeats the digit templates. The real fix was hardening the OCR, not gating on it.
RUNNING IT

Scale and shape

5
matches parsed end to end
singles + a doubles broadcast
~828k
per-frame track rows
position + 17 keypoints, in DuckDB
4.5 h
longest single tracking run
166,650 frames, resumable 20k chunks

Tracking is measured in MPS-hours, so every long job is chunked and crash-resumable, and parsed data is durable — the database is the source of truth and a match is never re-parsed. The dashboard you’re reading is a fully static export: all analytics are precomputed into JSON at build time, so there is no server, no API, and no cold start.

BUILT WITH

Stack

Vision
YOLO11x-pose, ByteTrack, TrackNetV3, BST-0, OpenCV homography
Runtime
PyTorch on Apple-silicon MPS, chunked + resumable batch jobs
Data
DuckDB as the source of truth, keyed by match — parsed once, never re-parsed
Web
Next.js static export, TypeScript, Tailwind, hand-written SVG charts (no chart library)
Validation
ShuttleSet22 annotations — classifier training on other matches and evaluation on the target match