hermitkk's picture
Upload README.md with huggingface_hub
2de4f66 verified
|
Raw
History Blame Contribute Delete
5.76 kB
---
license: agpl-3.0
tags:
- image-classification
- onnx
- pytorch
- omr
- checkbox-detection
- plom
pipeline_tag: image-classification
---
# OMR Checkbox Classifier
A lightweight binary CNN that classifies scanned exam answer-sheet checkboxes as **filled** or **empty**. Trained on synthetic crops generated from PDF answer-sheet templates, with augmentation to simulate real scan noise, skew, and marking styles.
Developed for the [Plom Project](https://gitlab.com/plom/plom) β€” an open-source platform for automated grading of paper exams.
- **Developer**: Chenfeng Xu, Undergraduate Research Assistant, UBC
- **Supervisors**: Prof. Andrew Rechnitzer & Prof. Colin B. MacDonald
- **License**: AGPL-3.0 (to comply with Plom Project requirements)
---
## Model Details
| Property | Value |
|---|---|
| **Architecture** | SmallOMRNet (custom lightweight CNN) |
| **Input** | 1 Γ— 64 Γ— 64 grayscale image |
| **Output** | Scalar sigmoid probability of checkbox being filled |
| **Task** | Binary classification: `filled` / `empty` |
| **Export formats** | ONNX (`omr_model.onnx`) |
| **Parameters** | ~120k |
### Architecture
SmallOMRNet uses depthwise separable convolutions (DSConv) for efficiency. Each DSConv block is a depthwise conv + BN + ReLU followed by a pointwise conv + BN + ReLU.
```
Input: (1, 64, 64)
β†’ Conv2d(1β†’24, 3Γ—3, stride=2) + BN + ReLU # (24, 32, 32)
β†’ DSConv(24β†’32) # (32, 32, 32)
β†’ DSConv(32β†’48, stride=2) # (48, 16, 16)
β†’ DSConv(48β†’64) # (64, 16, 16)
β†’ DSConv(64β†’96, stride=2) # (96, 8, 8)
β†’ DSConv(96β†’128) # (128, 8, 8)
β†’ AdaptiveAvgPool2d(1) β†’ Flatten # (128,)
β†’ Linear(128β†’64) + ReLU + Dropout(0.20)
β†’ Linear(64β†’1) # logit
β†’ Sigmoid # fill probability ∈ [0, 1]
```
---
## Training
### Dataset
Synthetic checkbox crops generated from PDF answer-sheet templates using the scripts in this repository. Each crop is a padded region around a single checkbox (pad ratio 0.35), rendered at 300 DPI from the template PDF.
- **Classes**: `filled` (label 1) / `empty` (label 0)
- **Image size**: 64 Γ— 64 grayscale
- **Normalisation**: mean=0.5, std=0.5
### Augmentation (training only)
| Transform | Parameters |
|---|---|
| RandomAffine | rotate Β±8Β°, translate Β±6%, scale 92–108%, shear Β±5Β° |
| RandomPerspective | distortion 0.18, p=0.25 |
| ColorJitter | brightness 0.15, contrast 0.15 |
| GaussianBlur | kernel 3, Οƒ ∈ [0.1, 1.0] |
| RandomErasing | p=0.10, scale 2–10% |
### Hyperparameters
| Parameter | Value |
|---|---|
| Optimizer | AdamW |
| Learning rate | 0.001 |
| Weight decay | 0.0001 |
| LR schedule | CosineAnnealingLR |
| Loss | BCEWithLogitsLoss (class-balanced pos_weight) |
| Batch size | 128 |
| Max epochs | 20 |
| Early stopping | patience=5, monitor=ROC-AUC |
| Random seed | 42 |
---
## Inference
### ONNX (recommended)
```python
import cv2
import numpy as np
import onnxruntime as ort
IMG_SIZE = 64
MEAN, STD = 0.5, 0.5
session = ort.InferenceSession("omr_model.onnx", providers=["CPUExecutionProvider"])
input_name = session.get_inputs()[0].name
def predict(crop_bgr: np.ndarray) -> float:
"""Return fill probability for a single BGR checkbox crop."""
gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY)
resized = cv2.resize(gray, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_LINEAR)
arr = (resized.astype(np.float32) / 255.0 - MEAN) / STD
batch = arr[np.newaxis, np.newaxis, :, :] # (1, 1, 64, 64)
logit = session.run(None, {input_name: batch})[0]
return float(1.0 / (1.0 + np.exp(-logit))) # sigmoid
crop = cv2.imread("path/to/checkbox_crop.png")
prob = predict(crop)
print("filled" if prob >= 0.5 else "empty", f" prob={prob:.3f}")
```
### Decision thresholds
| Parameter | Default | Env var override |
|---|---|---|
| Fill threshold | 0.50 | `OMR_THRESHOLD` |
| Uncertain margin | 0.10 | `OMR_UNCERTAIN_MARGIN` |
| Min confidence | 0.55 | `OMR_MIN_CONFIDENCE` |
A prediction is flagged `uncertain` when `|prob - threshold| < uncertain_margin` or `confidence < min_confidence`, where `confidence = 1 - normalised_entropy(prob)`.
---
## Repository Contents
| Path | Description |
|---|---|
| `omr_model.onnx` | Trained model β€” primary deployment artifact |
| `config/config.yaml` | Full pipeline config: template variants, detector settings, training hyperparameters |
| `data/raw/template/` | Answer-sheet template PDFs (6 variants: 2-/4-/6-choice, mixed, assembled, math quiz) |
| `data/template_map/` | JSON coordinate maps for each template variant |
---
## Template Variants
Six answer-sheet layouts are supported. Each variant has a corresponding template PDF and a pre-built template map (checkbox coordinate file).
| Variant | Choices per question | Template PDF |
|---|---|---|
| `2choice` | A, B | `template_2choice.pdf` |
| `4choice` | A, B, C, D | `template_4choice.pdf` |
| `6choice` | A, B, C, D, E, F | `template_6choice.pdf` |
| `mixed` | variable | `template_mixed.pdf` |
| `assembled_4choice` | A, B, C, D | `template_4choice_assembled.pdf` |
| `math_quiz` | A, B, C, D | `exam_math_quiz.pdf` |
---
## Intended Use
Designed for automated grading pipelines that pre-crop answer-sheet checkboxes (after scan alignment and template matching) and need a fast, CPU-friendly classifier to decide filled vs. empty. Intended to run as part of the [plom-digit-recognition-server](https://gitlab.com/plom/plom).
**Out of scope**: general object detection, handwriting recognition, forms with non-checkbox mark types.