A two-class real-time object detector for volleyball footage: person and ball.
It is a fine-tune of D-FINE-S (10 M parameters, Apache-2.0)
trained on frames from youth club matches filmed with a wide-angle camera mounted on the referee stand, plus beach
and grass sessions from the same camera rig.
The model was built to power automatic highlight detection in a volleyball recording app: it finds every person in the frame and the ball, and the app decides who is a player from the court region.
| class | what it means |
|---|---|
| person | any person: players, referees, coaches, spectators |
| ball | a volleyball, in the air or on the ground |
It does not distinguish players from spectators. Filter by court region downstream if you need players only.
The ball is small: in 4K footage from a ref-stand camera it is often 20 to 40 pixels across. The model was trained on 640×640 tiles cut from 720 to 1040 px windows of 4K frames, so it should be run the same way:
predict.py in this repo does exactly that.
from transformers import AutoImageProcessor, AutoModelForObjectDetection
from PIL import Image
import torch
repo = "grdesignbuild/volleyball-person-ball-detector"
processor = AutoImageProcessor.from_pretrained(repo)
model = AutoModelForObjectDetection.from_pretrained(repo).eval()
img = Image.open("frame.jpg").convert("RGB") # a tile or a small image
inputs = processor(images=img, return_tensors="pt")
with torch.no_grad():
out = model(**inputs)
res = processor.post_process_object_detection(out, threshold=0.4, target_sizes=[img.size[::-1]])[0]
for score, label, box in zip(res["scores"], res["labels"], res["boxes"]):
print(model.config.id2label[label.item()], round(score.item(), 2), [round(v) for v in box.tolist()])
Person and Volleyball), with every accepted ball box on the
ref-stand frames reviewed by hand (4,077 of 6,086: 910 removed as ceiling lights, coolers, cups, bags and logos; 456
blurred out as ignore regions) and every accepted ball box on the end-line frames reviewed by hand (136: 77 were
three ceiling-light fixtures repeated across frames, 44 real balls kept, 15 far-court specks blurred out). Person
boxes are the teacher's, unreviewed. Tiles with people but no ball are sampled at 55% so shoes, knee pads and hair are
seen as background often; end-line tiles are repeated three times so they are not drowned by the ref-stand ones.Validation is against the teacher's pseudo-labels on held-out ref-stand recordings, with the ball-label cleaning applied to the validation side. These numbers are comparable to the 2026-09-08 release (same validation labels), not to the first release (0.759 mAP@0.5 against uncleaned labels).
| metric | value |
|---|---|
| mAP@0.5 | 0.776 |
| mAP@0.5:0.95 | 0.611 |
| AP@0.5:0.95 person | 0.674 |
| AP@0.5:0.95 ball | 0.547 |
| mAP small objects | 0.427 |
Training: fine-tuned from the 2026-09-08 release for 1 epoch, batch 8, AdamW (lr 5e-5, backbone 5e-6), cosine schedule, on an Apple M5 GPU.
| release | ball claims | of which real balls | person detections |
|---|---|---|---|
| 2026-09-03 | 74 | 7 | 1,950 |
| 2026-09-08 | 7 | 7 | 1,975 |
| 2026-09-08 end-line (this one) | 7 | 7 | 2,083 |
| threshold | 2026-09-08 real balls / light-fixture hits | this release |
|---|---|---|
| 0.40 | 4 / 8 | 4 / 5 |
| 0.50 | 3 / 5 | 4 / 0 |
| 0.60 | 1 / 0 | 3 / 0 |
Every real ball scores higher than under the previous release; the ceiling-light false positive on that venue's pillar disappears above 0.47. Run this model with tiles of ~640 px on frames around 2000 px wide, not the 4K recipe above.
person at low confidence; a 0.4 threshold is a good default.Apache-2.0, same as the base model. Built with 🤗 Transformers.
coreml/VolleyballDetector.mlpackage/Data/com.apple.CoreML/weights/weight.bin 41 MBmodel.safetensors 41 MB