sportcv-ballnet
The ball head from sportcv: a small heatmap network that finds a pickleball in a 1080p broadcast-style frame.
It exists because the alternative did not fit. WASB was the plan, and at the resolution the ball needs it measured 58.6 ms per frame β 175% of a 33.37 ms budget. This head runs the same job at 9.88 ms (FP16, RTX 3060) and 2.50 ms on TensorRT FP16 (RTX A6000), which is what makes a full analysis pipeline finish faster than the clip plays on one 6 GB laptop GPU.
What it is
| Architecture | BallNet, width 32, output stride 4, single heatmap + sub-pixel offset |
| Input | full frame padded to a multiple of 32 (1920Γ1088 for 1080p) |
| Output | one heatmap; decoded to a peak position in pixels plus a confidence |
| Trained | 80 epochs (checkpoint at epoch 65), ~30 min on one A40, $0.35 |
Measured
On a held-out rally the model chose nothing on:
| Metric | Value |
|---|---|
| Median peak error | 1.73 px |
| Within 8 px | 94.5% |
| Confidence, ball present | 0.90 median |
| Confidence, ball absent | 0.07 median |
Against all labels including the training window: 1.31 px median, 97% within 8 px. Over a 25.6-minute span the head was never trained on, it puts a trajectory on 76β89% of in-rally frames in every minute, median 80% β and the minute it was trained on is not the best one.
The raw detection rate across all frames is 47%, which says nothing about the model: the ball is mostly absent when nobody is playing.
Training data, and why it is this small
396 annotated frames from a single 60-second window β about 1.2% of that window β of one Creative Commons match. Labelling is the expensive input, so the project measured how little would do rather than assuming.
Source footage: *2026.07.25 WD Open β Sabrina Lam + Grace Thomas vs Lingzhe Xu
- Margit Aardmaa*, by @pickleball4you on YouTube, licensed CC BY 3.0. https://www.youtube.com/watch?v=T5rmWjvt8Os
Limitations β read this before using it
- It is overfitted to one match, on purpose. One ball, one set of lights, one camera, one court. It is not a general sports-ball detector and was never trying to be. Treat any other footage as unmeasured until you measure it.
- Pickleball only. No other sport has been run through it.
- The crowd wears the ball's colour, and the court crop was wrong for a while because the ball flies above the court. Both are documented failure modes in the project's DESIGN.md.
- The head always produces a peak, so a confidence threshold is not optional.
sp-detectuses 0.35; the distribution is cleanly bimodal (p25 0.047, p75 0.89).
Files
| File | sha256 | What |
|---|---|---|
ballnet.pt |
2081bc6cβ¦57a8ca |
the checkpoint sp-detect loads |
ballnet.onnx |
962d7a4aβ¦0f9d3083 |
FP32 ONNX, the portable artifact |
ballnet.fp16.onnx |
4641d2ccβ¦13c1e |
FP16 ONNX for strongly-typed TensorRT |
Full hashes are pinned in
src/sportcv/core/weights.py
and checked on every load. A number is evidence only if the artifact behind it
can be identified, and a filename identifies nothing.
No TensorRT engine is published here. An engine is tied to a TensorRT
version and, without the Ampere-plus flag, to one GPU architecture, so it would
be a 50 MB file that silently fails to load for most people. Build your own
from ballnet.fp16.onnx; sportcv's exporter checks parity where it is used β
in pixels of decoded peak movement against the 1.73 px held-out error β
rather than as a tensor difference nobody can act on. Measured at 0.0008 px,
which is 0.04% of the model's own error.
Use
uv run sp-detect data/<video>.mp4 # resolves and verifies these weights
Or directly:
import torch
from sportcv.core.ballnet import BallNet
from sportcv.core.weights import resolve
blob = torch.load(resolve("ballnet.pt"), map_location="cpu", weights_only=False)
model = BallNet(width=blob["width"]).eval()
model.load_state_dict(blob["state_dict"])
Licence
The weights and the code are Apache-2.0. The footage they were trained on
is CC BY 3.0 and attribution to @pickleball4you is required in any output
derived from it β YouTube grants 3.0, never 4.0, and the attribution must name
the licence the licensor actually granted.
Citation
@software{condados_sportcv_2026,
author = {Condados, Luis},
title = {sportcv: offline sports video analytics from one fixed camera},
year = {2026},
url = {https://github.com/CondadosAI/sportcv}
}
The architecture owes its framing to the WASB paper, which is the baseline it replaced: Tarashima et al., Widely Applicable Strong Baseline for Sports Ball Detection and Tracking, BMVC 2023. https://arxiv.org/abs/2311.05237