Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SnapGrade

Generate, scan, and grade multiple-choice exams — without a Scantron machine.

SnapGrade turns a stack of filled-in answer sheets into scored, exportable results in minutes: print sheets with a barcode/QR baked in, scan or photograph them once filled out, and get back per-question, per-student results as Excel — no proprietary hardware, no per-sheet licensing, no manual bubble-counting.

An independent portfolio project — built solo, from scratch, with public libraries only.

Status: the full pipeline is live end to end — generate print-ready sheets, grade scanned/photographed sheets or a re-uploaded PDF, and export Excel results, all driven from the desktop app itself (see Architecture for how it's built and calibrated). See Roadmap for what's next.

Why this exists

Grading multiple-choice exams by hand doesn't scale, and dedicated OMR/Scantron hardware is expensive, proprietary, and often locked to one paper format. SnapGrade is the middle ground: any printer, any scanner or phone camera, and a small desktop app that handles sheet design, optical mark recognition, and results export end to end.

How it works

Two modes share one generate → grade → export pipeline:

  • Anonymous — no student data at generation time. Each sheet gets a sequential ID encoded in a barcode; grading matches results back to that ID only.
  • Roster — import a student list first (Excel). Each sheet is prefilled with a student's name/ID, and the barcode encodes their student ID instead.
  1. Generate — fill in exam details and produce print-ready sheets. Bubble-grid layout (columns, rows, bubble size) is computed automatically from the question/option counts — never shrunk below a fillable size, never overflowing the page. Sheets export as individual PDFs or as one combined, multi-page PDF for a whole batch/roster in a single print job.
  2. Grade — upload a correction matrix (Excel) and the scanned/photographed sheets (a folder of images, or a single PDF — rasterized page by page automatically). Each sheet's barcode resolves its identity, its QR resolves the exact grid layout to expect, and every bubble is classified by ink-fill ratio — calibrated against measured data, not guesses. Anything ambiguous (two filled options on one question) is flagged for review, never silently guessed. One unreadable sheet doesn't abort the rest of the batch.
  3. Export — one Excel file: a Summary sheet (one row per student, totals + score) and a Detail sheet (one column per question, correct/incorrect colored automatically when a correction matrix is used), merged with roster data if provided.

Full walkthrough with screens: Usage Guide. Design decisions and how the OMR pipeline was actually calibrated: Architecture.

Engineering highlights

The parts of this project that were actually hard, condensed to one line each — full reasoning for each is linked:

  • Shared geometry, not duplicated math — the generator and grader both build the exact same SheetGeometry from a spec; the grader never re-detects where a bubble is, only what's drawn there, so the two halves of the app can't silently drift apart as either one changes. → architecture
  • Calibrated against measured data, not assumptions — the OMR fill-threshold (0.8) came from actually measuring ink-fill ratios on a known-marks test fixture, not a guess; a crossed-out bubble measurably carries more ink than expected, which is exactly the kind of thing you only catch by measuring. → architecture
  • Never silently guesses — two filled options on one question is flagged AMBIGUOUS for human review, never resolved by picking one; a filled-then-crossed bubble correctly resolves back to a clean answer instead of a false ambiguous flag.
  • One computation, two consumers — the Grade screen's live results table and the exported Excel Summary sheet compute their numbers from the same function, so the UI and the spreadsheet can never quietly disagree.
  • The app's theme, never the OS's — Qt6 silently adapts unstyled widget chrome to the OS's light/dark setting independently of an app's own stylesheet; every theme here is pinned to render consistently regardless of system appearance. → architecture

Walkthrough

1. Generate — fill in the exam and produce sheets

Pick Anonymous or Roster mode, describe the exam, and choose where the PDFs go.

Generate screen, filled in

Click Generate sheets and every sheet renders, writes to individual + combined PDFs, and the first sheet previews right there — school/exam name, barcode, QR, and a bubble grid sized automatically for the question/option count.

Generate screen after generating, showing the preview

Switching to Roster mode swaps the batch-size/starting-ID fields for a roster import — one sheet per row, prefilled with that student's name and ID instead of a sequential number.

Generate screen in Roster mode

2. Print, administer, collect

Print the generated sheets, run the exam, collect the filled-in sheets. No special paper or hardware required — a laser/inkjet printer and a normal scanner or phone camera are enough.

3. Grade — upload the correction matrix and scans

Upload the answer key and the scanned sheets (folder of images or one PDF), optionally a roster to attach names to the results, and choose where to export.

Grade screen, filled in

Click Grade & export and every sheet is graded and written straight to Excel — the same per-student totals appear right in the app as a results table.

Grade screen after grading, showing the results table

4. What you actually get

The rendered sheet — barcode (identity) top-right, QR (grid metadata) bottom-right, a "how to mark" legend, and a bubble grid that never shrinks below a fillable size:

Anonymous-mode sheet sample Roster-mode sheet sample
Anonymous — sequential ID only Roster — prefilled name/ID/class

And the Excel export — a Summary sheet with one row per student, and a Detail sheet with one column per question, correct/incorrect colored automatically.

The bubble-classification logic isn't guesswork — it was tuned against a fixture with known marks at known questions, until every mark state below resolved correctly:

Calibration fixture with a filled answer (Q17), a partial fill (Q3), a crossed-out mark (Q9), and two filled options on one question (Q25)

Q3 — partial fill (correctly reads as blank), Q9 — crossed out (correctly reads as blank, not a second vote), Q17 — filled then crossed on a second option (correctly resolves to one clean answer), Q25 — two options genuinely filled (correctly flagged AMBIGUOUS, never guessed).

Theming

Five built-in themes — Paper (default), Graphite, Meadow, Chalkboard, Dusk — switchable live from the sidebar. All are generated from one token set + one QSS template (src/snapgrade/ui/theme.py), so adding a new theme is a new set of color values, not a new stylesheet. Every widget follows the app's chosen theme only, never the OS's light/dark setting.

Paper theme Graphite theme Meadow theme
Paper Graphite Meadow
Chalkboard theme Dusk theme
Chalkboard Dusk

Tech stack

Concern Library
Desktop UI PySide6
Icons qtawesome
OMR / barcode decoding OpenCV, pyzbar
Sheet generation (QR/barcode) qrcode, python-barcode
Print-ready PDF export ReportLab
Scanned-PDF rasterization pypdfium2
Excel import/export openpyxl
Packaging uv

Getting started

uv sync
uv run snapgrade

(or uv run python -m snapgrade)

Project structure

src/snapgrade/
├── app.py                    # QApplication bootstrap / entry point
├── ui/                        # desktop UI — theming, reusable widgets, screens, form-state persistence
├── sheets/                     # sheet geometry, specs, mockup renderer, roster import, QR/barcode codecs
└── grading/                     # OMR grader, code decoding, bubble classification, scan loading, Excel export

See Architecture for what each package actually does and why it's shaped this way.

Roadmap

  1. UI foundation — theming, reusable widgets, Generate/Grade screen layouts.
  2. Sheet geometry + generation — dynamic bubble-grid layout, barcode/QR design, print-ready PDF export.
  3. OMR grading core — identity/metadata decoding, ink-ratio bubble classification, validated against a calibration fixture.
  4. Excel I/O — roster import (prefill at generation, ID matching at export), correction-matrix import, and a Summary + per-question Detail results export.
  5. Wire the UI to the generation/grading engines — Generate and Grade now drive the real pipeline end to end, with live previews, results tables, and error feedback.
  6. Real deskew/boundary-detection preprocessing for actual photographed sheets (today's ImagePreprocessor is a documented no-op — grading assumes a clean, correctly-sized scan).
  7. Multi-select answers (e.g. exporting "AB" for one question) — the Detail sheet already renders multiple letters for an ambiguous cell; this would extend it to correction-matrix answers too.

Docs

  • Usage Guide — the end-to-end workflow, screen by screen.
  • Architecture — how the sheet-geometry engine and OMR pipeline actually work, including the real calibration data behind the grading thresholds.

About

Desktop app for generating, scanning, and grading multiple-choice exam sheets via OMR, with Excel results export — built from scratch in PySide6 + OpenCV.

Topics

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages