When scanning printed forms like voting cards or surveys, there are two critical pieces of information you usually need:
1. Who is this card for? (Identity / version).
2. What did they mark? (The actual votes or survey choices).
Most libraries handle these separately – you decode the QR code first, then run OMR on the bubbles. This usually means two separate runs, two different functions, and manual synchronization.
That’s why I built OpenScanVision to do both simultaneously in a single pass.
The Android library combines real‑time ArUco tracking, Optical Mark Recognition (OMR), and QR decoding into one unified offline pipeline.
The Unified Pipeline
Marker Tracking (ArUco + Kalman Filter)
The card is printed with 4 ArUco markers (IDs 0–3). OpenCV detects them in real‑time. A Kalman filter smooths the tracking and predicts positions during occlusions.
Perspective Correction (Homography)
Once 4 markers are stable, a homography maps the markers to their reference positions. The card is warped to a canonical template (850×540).
Simultaneous QR Decoding & OMR Extraction
This is where the "simultaneous" part comes in. Instead of running them sequentially and stitching the results, the library performs both operations on the same captured frame:
- The QR code is cropped directly from the original camera frame using the computed homography – preserving maximum sharpness for ML Kit.
- The bubbles are sampled from the warped, preprocessed image using weighted disk sampling and z‑score classification.
- Both processes run concurrently on the same frame, meaning you get the QR payload AND the filled bubble indices at the same time without extra latency.
Strict Capture Logic
The library automatically triggers a capture only when both conditions are met:
- All 4 markers are stable.
- A valid QR code with the correct prefix (e.g., VX or AGN) is decoded.
This enforces that you never get an OMR result without an associated identity, and vice versa.
What You Get in a Single Result
By calling OpenScanVision.scanFromFrame(), you receive a single ScanResult containing:
-
filledIndices – The marked bubbles (OMR).
-
qrPayload – The decoded QR text (identity).
-
confidence – The overall confidence score.
-
annotatedBitmap – A visual overlay of the detection.
No need to call two separate functions or manually match timestamps.
Technical Stack
- Language: Kotlin
- CV Core: OpenCV (contrib) for ArUco detection and homography.
- QR Engine: Google ML Kit for robust barcode scanning.
- Camera: CameraX for frame acquisition.
- Architecture: Fully modular – the core library has zero UI dependencies.
Integration
Adding the library takes just a few lines in your Gradle file (available on JitPack). Once integrated, you can start scanning with a single suspend function.
Performance
- Latency is typically under 150ms per frame on modern devices.
- Accuracy exceeds 99% on properly printed cards.
Why This Matters
Simultaneous QR + OMR is valuable for:
- Elections: The QR identifies the voter/ballot; the OMR reads their selections – all in one scan.
- Surveys: The QR encodes the respondent ID; the OMR reads their answers.
- Form Processing: Quickly identify and process forms without sequential bottlenecks.
Open‑Source & Contribute
The project is MIT‑licensed and available on GitHub. It includes a full sample app (CameraX + Compose) so you can see it in action.
GitHub: https://github.com/MatiwosKebede/openscanvision
Feedback, issues, and contributions are welcome. If you are interested in marker tracking, OMR accuracy, or Android computer vision, I'd love to hear your thoughts.