Automated grading of bubble-sheet (OMR) exams with a real computer-vision pipeline. Upload scanned answer sheets, a YOLOv8 model detects each filled bubble, the answers are compared against a key, and you get per-student scores as CSV — from the command line or a small Flask web app.
Grading multiple-choice exams by hand is slow, error-prone, and doesn't scale. Classic OMR scanners need expensive dedicated hardware and rigidly pre-printed sheets. This project replaces that with a trained object detector that reads ordinary scanned sheets (PDF or image) and grades them automatically.
The core is a YOLOv8n detector fine-tuned to recognise marked bubbles. Each bubble is classified into one of six classes:
a · b · c · d · e · none
For every page the model emits one detection per question; the detected option is matched against the answer key to compute correctness and a final score.
PDF / image ──► page split ──► YOLOv8 bubble detection ──► compare vs key ──► CSV
(pdf2image) (ultralytics) (utils/ans.py)
| Component | File | Role |
|---|---|---|
| Web server | app.py |
Flask upload UI + /upload, /download_csv routes |
| JSON API | api.py |
FastAPI service returning structured results + OpenAPI docs |
| Live demo | app_gradio.py |
Gradio app for Hugging Face Spaces |
| Standalone CLI | grade.py |
Grade sheets without Flask |
| Grading core | utils/ans.py |
Detect → compare → score |
| PDF splitter | utils/pdf_splitter.py |
Multi-page PDF → page images |
| Inference helpers | models/yolo_model.py |
Raw box-level detections |
| Weight loader | models/weights.py |
Auto-downloads best.pt from Releases |
| Training / eval | train.py, val.py, test.py |
Reproduce or extend the model |
The 6 MB trained model is published as a GitHub Release asset
(v1.0-weights)
and downloaded automatically on first run — it is intentionally not committed to
git.
Trained for 80 epochs (YOLOv8n, 640px, batch 16) on a custom answer-sheet dataset. Validation results:
| Metric | Value |
|---|---|
| mAP@50 | 0.536 |
| mAP@50-95 | 0.283 |
| Precision | 0.47 |
| Recall | 0.54 |
These are honest first-pass numbers from a small dataset. The
noneclass and blank bubbles dominate errors — see Limitations.
git clone https://github.com/hp-8/bubblesheet-ai.git
cd bubblesheet-ai
pip install -r requirements.txt # also needs poppler for pdf2imageCommand line (model auto-downloads on first run):
python grade.py --sheets exam.pdf --key answer_keys/ans.txt --out scores.csvMultiple sheets or a folder of images work too:
python grade.py --sheets a.pdf b.pdf scans/ --key answer_keys/ans.txtWeb app:
python app.py # then open http://127.0.0.1:5000Upload your answer-sheet PDFs and a key file (one lowercase answer per line), then download the results CSV.
JSON API (typed, OpenAPI docs at /docs):
uvicorn api:app --reload
curl -F "answer_key=@answer_keys/ans.txt" -F "files=@exam.pdf" \
http://127.0.0.1:8000/gradeLive demo (Gradio — runs locally or on Hugging Face Spaces):
python app_gradio.pySee docs/HUGGINGFACE.md for one-push Spaces deployment.
python train.py # trains YOLOv8n on dataset.yaml
python val.py # evaluates and writes predictions.csvPoint dataset.yaml at your own labelled data (classes: a,b,c,d,e,none).
Kept deliberately honest:
- Student-ID OCR (
models/id_detection.py, Tesseract) reads the printed ID from the top strip of each sheet. It needs the Tesseract binary installed (brew install tesseract); without it, grading falls back to a placeholder id so output stays well-formed. - Metrics reflect a small training set; more labelled data would lift mAP substantially.
MIT



