§1The problem
One phone can't capture the whole pitch. With two, you have to align them in time and in image, stitch them without a visible seam, and still decide where to point the camera with nobody operating it.
§2What I built
- SyncThe two phones are synchronised by sound.
- StitchingLens and angle calibration from the image, stitched into a cylindrical panorama with colour correction.
- Virtual cameraBall detection with YOLO and a ball track; a virtual camera with zoom and smoothing that looks ahead.
- CLIResumable stages (each stage writes its own file) and JSON progress for the apps to follow.
- Re-renderA command to redo only the camera with a different zoom.
§3The hard parts
5 entries- 01
Speed
The panorama went from 37-46 to about 120 frames per second; the bottleneck was the loop waiting for the GPU on every frame.
- 02
Sync drift between the phones' clocks
About 140 ppm, corrected with windows measured across the game.
- 03
Parallax and seam
Measuring the difference in the overlap band and correcting only near the seam.
- 04
Numerical traps
A lens inversion that folds over, and infinite values turning into NaN in the black bands.
- 05
Orientation and encoder limits
Each phone's orientation comes from the app's marker, not just the grass; the GPU's 4096 px H.264 limit was solved with HEVC.
§4Result
- frames/s for the panorama (was 37-46)
- ~120
- clock drift corrected between phones
- ~140 ppm
- detection processing per game
- ~1h10
Runs on my own computer's GPU: the 2-camera panorama at about 120 frames/s, and detection takes about 1h10 of processing per game. Other apps call it the same way they would call ffmpeg.
§5Stack
- Python
- PyTorch (CUDA)
- YOLO11 (ultralytics)
- OpenCV (SIFT)
- ffmpeg with NVENC
- HEVC