Tennis ball bounce detector
Decides whether the ball touched the court, from the shape of its trajectory.
HistGradientBoostingClassifier over 16 scale-free trajectory features.
Measured results
Trained on the TrackNet tennis dataset: 95 clips, 10 professional matches, 920 labelled bounces.
| split | precision | recall | F1 | PR-AUC |
|---|---|---|---|---|
| held-out clips | 0.888 | 0.944 | 0.915 | 0.956 |
| leave-one-match-out | 0.951 |
Leave-one-match-out is the honest number: clips from one match share a court, a camera and a pair of players, so a clip-level split lets the model see the same camera during training. 0.951 across ten unseen matches says it transfers.
Why a classifier and not a threshold
The geometric rule this replaces used a single prominence threshold on the ball's height signal. Swept against ground truth, no value of that threshold found every point - so the parameter had no correct setting and the model was wrong, not the tuning.
The bug worth knowing about
The candidate proposal stage originally offered only frames where the ball's projected court y was a local maximum. Against 136 real labelled bounces that is true 2% of the time: a bounce is a corner in the trajectory, not a peak, because the height signal also carries down-court travel and that term usually dominates. Proposal recall was 34.6%. Proposing on the upward kink in image y instead - normalised by the segment's median speed, so it is resolution independent - took it to 99.3%. No classifier can recover an event that was never proposed.
Input
16 features per candidate, in this order:
image_kink, height_prominence_left, height_prominence_right, height_curvature, reversal_sharpness, speed_before, speed_after, speed_ratio, direction_change_deg, vertical_velocity_before, vertical_velocity_after, vertical_velocity_flip, court_y_at_candidate, distance_inside_court_m, interpolated_share, mean_confidence
Court-space features are in metres and need a court homography.
import joblib
model = joblib.load("bounce_model.joblib")
probability = model.predict_proba(features)[:, 1]
Requires scikit-learn 1.9+ (pickled there; older versions load with a warning and may give different answers).
Training data
TrackNet tennis dataset - 81 broadcast clips, 10 matches, 30 fps, per-frame ball
coordinates with a status column where 2 marks ground contact.