Skip to main content

M3: The Static Camera Isn't

Trevor McCormick
Data Product @ Disney+

That clip up top: every serve the pipeline felt sure about, clip after clip — which player served, and which side of the court, deuce or ad, flashed the instant the toss goes up.

Sixty broadcast clips are sitting there ready to be charted, and every one starts with the same problem: telling an actual tennis player apart from everyone else standing near a court who isn't one.

Previously: the last post sliced a highlights reel into sixty complete-point clips using screen-to-court math as a camera fingerprint. That math, the homography — matching the court's painted lines to its real dimensions to turn screen position into court position, from Pixels to Meters — carries a free trick: median-blend many frames of the same court and the players, who never stand still for long, vanish into a photo of an empty court. Two posts later, that trick already found both players in one rally well enough to call every forehand and backhand. Doing it across sixty new clips would otherwise mean paying SAM 3 — the pay-per-prompt "find that object" AI that tracked a ball for about sixty cents in the second post — roughly $0.15 a clip, $9 for the batch, every rerun.

Comparing two photos of the court

This session tried to cash in that free trick everywhere: build the empty-court photo per clip, then flag whatever pixel changed as a player. One catch — both photos need the same camera position. A broadcast camera looks fixed, but any camera operator worth their job nudges the shot to keep the rally centered, the way your own eyes would tracking the ball from courtside. A pixel-by-pixel comparison notices every nudge: the baseline in one photo no longer sits on the baseline in the other. It's like tracing a picture on an Etch A Sketch, then shaking the toy before checking your work — nothing lines up.

The fix: stabilization, nudging every frame back into line with the others before comparing anything, so the same pixel always means the same spot on the court. More on why the empty-court trick needs a still camera: Court Vision in Plain English.

The camera that wasn't sitting still

That math, first fit two posts back on a sixteen-second rally, found almost no camera movement — so the code kept one screen-to-court conversion for the whole clip. A full point can run a minute or more, and these sixty clips run that long. Aligning every frame of a clip to its own first frame and measuring the gap told a different story:

Line plot of camera x and y shift across a point clip, showing a spike to 32 pixels mid-point

The broadcast camera, measured, over one point: a sharp jump — as much as 32 pixels — mid-point, on top of slow drift throughout.

The camera operator was following the play — invisible to a viewer, fatal to the comparison. It blurs the empty-court photo, and near the far baseline, thirty-some pixels of drift is worth meters of court-position error. Every frame now gets stabilized first.

The far player who turned into a ghost

With frames stabilized, the near player — big, always moving — showed up in essentially 100% of frames; the far player, barely 35%. The empty-court photo had swallowed him.

He's small, and barely moves between points, so a photo built from one clip bakes in a faint standing copy of him — the same trick that erased a stationary line judge two posts ago.

The fix: build the photo from five clips instead of one — this clip plus its four nearest neighbors, aligned and blended by taking the middle value at every pixel again. Players stand somewhere different every point, so five ghostly copies never land on the same pixel twice, and the blend votes them all out. Far-player coverage: 35% to 96%. Near player: still ~100%. Cost: nothing.

Everyone near the court looks like a player

The first serve-detector run flagged several clips with the far player standing 13.5 meters from the court's center, on a court only 10.97 meters wide. Nobody serves from outside their own court.

Those weren't players. They were ballkids, crouched past the doubles sidelines inside the patch of court scanned for the far player, shifting weight just enough to out-move a motionless server. Third case of mistaken identity: the very first session swept spare-ball-holding ballkids into "the tennis ball," and two posts ago a stationary line judge got swept into "the empty court." To a pixel-by-pixel comparison, anyone standing near a court looks like they belong on it. Fix: the far-side scan region is now cut tight to the court; the near side stays generous.

Guessing who served, from posture alone

With players tracked, the serve should be readable from posture: the server hugs the center mark, the returner waits wide — which side of the mark gives away deuce or ad, for free — and the toss stretches the server's outline to full reach, so blob height should spike right at the toss.

Version one checked only distance to the center mark, and failed fast: some clips open mid-rally with no toss to find, some end on a leftover zoom fragment, and — the costly lesson — returners often stand nearly as far from the mark as a server does, so a return swing can read as a toss.

Five-frame strip of a detected serve: toss rising, full reach at the marked frame, racket drop after

Right: a clean toss, full stretch, on the flagged frame.

Five-frame strip showing a player swinging a return, wrongly flagged as a serve

Wrong: the flagged "server" is the returner, off the mark, mid-swing — every check in version one said yes.

Version two clears several checks before it commits — tracked early and reliably, right distance from center mark and baseline, a genuine height spike — or it refuses, with reasons attached, instead of guessing.

Result: 40 of 60 clips get a confident serve call; the other 20 are refused, on the record — clips that open mid-rally or rejoin mid-point, plus two long clips too thin on far-player tracking to trust, including the marathon 49-shot rally from the last post. Spot-checking a sample of the confident calls by eye: 6 of 8 named the right server, 5 of 8 found a usable toss moment — honest, not perfect. Geometry alone tops out here; a serve is, definitionally, the ball leaving the hand and crossing the net, and once the ball itself is tracked over these clips, it settles what posture can only guess at.

The scoreboard

What got checkedResult
Far player, before the fixseen in 35% of frames — mostly a ghost
Far player, after the fixseen in 96% of frames — essentially solved
Near player, throughoutseen in ~100% of frames — never a problem
Clips confident enough for a serve call40 of 60
Clips that honestly refused to guess20 of 60
Spot-check: correct server named6 of 8
Spot-check: usable serve moment found5 of 8

The takeaway

A camera that looks perfectly still to a viewer at home can still wreck a pixel-by-pixel comparison — so before comparing two photos of the court, make sure they were taken from exactly the same seat.

For the technical reader

Alignment: each frame is registered to its clip's first frame using ECC (Enhanced Correlation Coefficient) — an alignment method — restricted to pure translation (x/y shift, no rotation/scale). point_29 is the 32px-spike example, now the mandatory first step before plate-building, background subtraction, or homography projection. The free trick cashed in this session: frame-minus-median already IS a player detector, left over from M1's clean-plate trick. Ghost-plate fix: a 5-clip window (target + 4 nearest temporal neighbors), each ECC-aligned before median-merging; zero added API cost, pure numpy.

Serve detector v1: single heuristic, distance-to-center-mark only. point_33: clean, correctly-flagged case. point_40: failure case, flagged "server" is the returner, mid-swing, off-hash, passing every v1 check anyway. v2: four required gates — (1) early-clip track coverage, (2) distance to center mark, (3) distance to own baseline, (4) a genuine local peak in blob height. Any failed gate produces an explicit no_confident_serve result with reasons attached, not a forced guess. Refusal log flags cold-opens, mid-point rejoins, and two long clips with sparse early far-tracks — including the 49-shot rally from the cutting-room post (cv-06).

Also queued: fps as a real parameter — this reel runs 25fps against the original clip's 30fps, and velocity thresholds are secretly per-frame numbers.

What's next

Session tally: player positions across all sixty clips, serve details on two of three, three wrong assumptions caught — for $0.00. Project total: still about seventy-five cents, all spent in the very first tracked-rally session. SAM 3, the paid tracker, has exactly one job left: finding the ball.

Next: a ball detector on these same sixty clips. Then the serve call, hits, bounces, and shot letters should click into a complete point, written in the Match Charting Project's own notation — the human-made answer key this project is chasing. The one working chart string so far, from two posts ago, was just the middle of a rally. The next one should have a beginning, a middle, and an end.