Archery Target Detection
Upload a photo of an archery target and get back the score of every arrow.
Overview
A FastAPI service I built on my own for the Archery Tournament Platform, our team project for COS20031. The scoring app sends it a photo of a target and the range being shot. The service asks a hosted detection model to find the target and the arrows, then returns one score per arrow for that end. It also saves confirmed ends into the shared MySQL database as pending scores for a recorder to verify.
At a glance
- Language
- Python 3.12
- Framework
- FastAPI, Pydantic
- Detection
- Roboflow hosted model, called with inference-sdk
- Database
- MySQL through an aiomysql pool
- Packaging
- Docker image and Makefile
- Part of
- Archery Tournament Platform
From photo to scores
- Photo + range ID
- POST /detect
- Hosted model
- Keep confident hits
- Fit to arrows per end
- Scores as JSON
What it shows
- Layered structure. Routes, controllers, services and repositories each have one job. One async MySQL connection pool is created at startup and shared by every repository.
- Confidence filtering. A target must be detected with at least 0.6 confidence, and an arrow with more than 0.5. Weak detections are ignored.
- Fits the result to the range. The number of arrows per end comes from the range in the database. The scores are sorted from high to low, extra ones are dropped and missing ones are filled with 0.
- Useful errors. If no target is found, the API answers 400 and asks for another angle or lighting. If the range is unknown, it answers 500 with a clear message.
- Validated score entry. The staging route checks the request with Pydantic: IDs must be positive and the arrow list cannot be empty. X is stored as 10 with an X flag, and M is stored as 0.
- No leftover files. The uploaded photo is saved only for the detection call and deleted straight after.
How it works
- The React app posts the photo to /api/archer/{range_id}/detect.
- The service saves the file briefly, sends it to the hosted model and reads back the predictions.
- It keeps the target and the confident arrow detections, looks up how many arrows an end has in that range, and returns the scores as a JSON list.
- After the archer checks the scores, the app posts the end to /api/archer/arrows, and each arrow is inserted into arrowStaging with the status pending.
From OpenCV to a hosted model
The first version was my own OpenCV pipeline: blur, brightness and contrast checks, cropping, resizing and an adaptive Canny edge detector, each with unit tests. The results depended heavily on the lighting, and I did not have the time to train a deep learning model myself. So I removed the pipeline and switched to a pre-trained model hosted on Roboflow. The service now focuses on the API, the database and the scoring rules.
The API
POST /api/archer/{range_id}/detect
body: photo file (multipart)
returns: [10, 9, 9, 7, 0, 0] (one score per arrow in an end)POST /api/archer/arrows
body: { roundID, participationID, distance, endOrder, arrows: ["X", "9", "M"] }
returns: 201, arrows saved as pending