# Table Tennis Analyzer

> Turns broadcast match footage into player tracking, score charts, and heatmaps

Give it a table tennis video and it returns an annotated replay, a score chart read straight off the scoreboard, and a position heatmap, with no manual tagging.

- Year: 2026
- Tags: Computer Vision, YOLOv8, OCR, React
- Page: https://www.rondahan.com/projects/table-tennis-analyzer
- Repo: https://github.com/rondahan04/TableTennisAnalyzer

## Numbers

- **2** - models (1 custom-trained)
- **3** - outputs per match: video, chart, heatmap
- **0** - manual tagging required

## What it is

Feed it a table tennis video and it produces match analytics without anyone tagging events by hand. It tracks both players and the ball, reads the scoreboard directly off the screen, and returns an annotated replay, a score progression chart and a position heatmap. A small React page pulls those outputs together into a match recap.

## How it works

1. **Track the players** - YOLOv8-pose gives full skeletons rather than boxes. The table splits the frame, so the leftmost and rightmost centroids identify the two players without needing a tracker with identity memory.
2. **Detect the ball with a custom model** - Frames are extracted from source video, labelled in Roboflow, and used to train a dedicated YOLO detector. This is the part that decides whether the whole pipeline works.
3. **Read the scoreboard** - EasyOCR runs on a cropped scoreboard region, sampled every N frames rather than every frame, and reconstructs the score over time.
4. **Throw out the frames that lie** - A validity check drops replays, crowd shots and graphics before they inject nonsense readings, and a frozen-frame detector ends the pass early instead of grinding through a static end card.
5. **Collapse to one series** - Readings become a single monotonic number, games won times ten plus points, which makes the chart trivial to plot and makes a bad OCR read obvious as a spike.
6. **Render the outputs** - Ball trails are coloured blue to yellow to red by inferred speed. A second pass goes back over the segment and cuts a 15 second clip around the fastest shot.

## Flow

Three passes run over the same footage and answer different questions. The validity filter sits between detection and output so replays and crowd shots never reach the score series.

- Match video
- three passes → YOLOv8-pose (both players) + Custom YOLO (the ball) + EasyOCR (scoreboard, every N frames)
- discard non-gameplay → Validity filter (replays, crowd, graphics)
- collapse to one series → (games x 10) + points (monotonic, spikes = bad reads)
- Annotated video (skeletons, speed-coloured trail) + Score chart + Position heatmap

## Technical notes

1. **The ball needed a custom detector** - A table tennis ball is a handful of pixels moving fast enough to motion-blur. No off-the-shelf model finds it reliably, so building a dataset was unavoidable.
2. **OCR instead of rally detection** - The score is already rendered on screen every frame. Reading it is a far smaller problem than inferring points from play.
3. **Sampling every N frames** - A score changes a few dozen times in a match. Running OCR on all of them is almost entirely wasted compute.
4. **Two passes for the fastest shot** - You cannot know the maximum speed until the whole segment has been scanned, so the clip is cut on a second pass.

## Stack

- **Vision:** Ultralytics YOLOv8, OpenCV, EasyOCR
- **Data:** NumPy, SciPy, Matplotlib, Roboflow
- **Frontend:** Vite, React, TypeScript, Chart.js

## Repo layout

- `table_tennis_analyzer.py` - Main pipeline: pose, ball trail, scoreboard OCR
- `generate_ball_csv.py` - Per-frame ball track, despiked and gap-filled
- `ball_track_helpers.py` - Shared ball inference, device pick, smoothing
- `generate_heatmap.py` - Renders player density over a gameplay frame
- `train_ball_model.py` - Trains the custom YOLO ball detector
- `extract_frames.py` - Pulls evenly spaced frames for labelling
- `web/` - React recap site, charts the score CSV

## Honestly

I no longer have the trained ball detector. It was not committed and the local copy is gone, so the pipeline cannot be re-run end to end without training a new one from the labelled dataset. Everything shown here is output it produced while it existed. No automated tests either: the pipeline was validated by watching the annotated video, which catches real errors but is not a substitute for a test suite.

---

Machine-readable: [llms.txt](https://www.rondahan.com/llms.txt) · [sitemap.xml](https://www.rondahan.com/sitemap.xml) · Markdown variants at [/md](https://www.rondahan.com/md) or via `Accept: text/markdown`.
