DEV Community

Matiwos Kebede
Matiwos Kebede

Posted on

How I Built a High‑Performance OMR and QR Scanner for Android (Open‑Source)

How I Built a High‑Performance OMR Scanner for Android (Open‑Source)

Introduction

Scanning printed voting cards, surveys, and bubble sheets on Android has always been a challenge. Most existing solutions are either expensive, require an internet connection, or are not accurate enough for real‑world use.

That's why I built OpenScanVision – an open‑source Android library that combines Optical Mark Recognition (OMR) and QR code decoding, working entirely offline.

The Problem

When I started this project, I needed a scanner that could:

  • Detect a card in real‑time from a camera feed.
  • Correct perspective distortion (cards are rarely perfectly aligned).
  • Accurately extract filled bubbles (even in uneven lighting).
  • Decode QR codes to identify the card.
  • Work offline – no internet connection required.
  • Be easy to integrate into any Android app.

Existing libraries were either closed‑source, inaccurate, or tightly coupled to specific UI frameworks. I wanted something modular, accurate, and open.

The Solution: OpenScanVision

OpenScanVision is an Android library that solves these problems with a clean, modular architecture.

Tech Stack

Component Technology
Language Kotlin
Image Processing OpenCV (contrib)
QR Decoding Google ML Kit
Camera CameraX
UI (Sample App) Jetpack Compose
Publishing JitPack

How It Works – The Scanning Pipeline

1. Frame Acquisition

CameraX provides YUV frames at a configurable resolution (default 640×360). The Y (luminance) plane is extracted into an OpenCV grayscale Mat for tracking.

2. ArUco Marker Tracking

The card has 4 ArUco markers (IDs 0–3) printed in the corners. OpenCV detects these markers in real‑time. A Kalman filter smooths the tracking and predicts marker positions when they are temporarily occluded.

3. Homography & Perspective Correction

Once all 4 markers are stable, a homography matrix is computed. This maps the markers to their reference positions, allowing the card to be warped to a canonical template (850×540 pixels).

4. QR Decoding

The QR code is cropped from the original camera frame using the homography – not from the warped image. This preserves sharpness for ML Kit. The crop is enhanced (contrast, denoise, resize) before decoding.

5. OMR Extraction

The warped image is preprocessed with CLAHE (contrast enhancement) and a median blur (denoise). Each bubble is sampled using a weighted disk. A per‑group z‑score classification determines which bubbles are filled. An inner‑core fill ratio rejects false positives from paper grain or printed outlines.

6. Strict Capture Logic

Auto‑capture triggers only when:

  • All 4 markers are stable.
  • A valid QR code with a recognised prefix (e.g., VX, AGN) is decoded.

This eliminates false positives and ensures every scan is high‑quality.

Architecture

The library is split into two modules:

openscanvision/
├── openscanvision-core/          # Core library (OMR + QR engine)
│   └── src/main/java/.../core/
│       ├── OpenScanVision.kt     # Public API facade
│       ├── ScanOptions.kt        # Builder pattern config
│       ├── ScanResult.kt         # Sealed result class
│       └── internal/             # Implementation (hidden)
├── sample/                       # Reference app (demo)
└── tools/                        # Supporting utilities
Enter fullscreen mode Exit fullscreen mode

The core library has zero UI dependencies – no Compose, no CameraX, no Android Views. This means you can use it in any Android project, even headless services.

Integration Example

Adding OpenScanVision to your project takes just a few lines:

1. Add JitPack to your repositories

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Add the dependency

// app/build.gradle.kts
dependencies {
    implementation("com.github.MatiwosKebede:OpenScanVision:openscanvision-core:v1.0.0")
}
Enter fullscreen mode Exit fullscreen mode

3. Scan a card

// Initialize once
OpenScanVision.initialize(context)

// Scan a frame
suspend fun scanCard(bitmap: Bitmap) {
    val result = OpenScanVision.scanFromFrame(bitmap)
    when (result) {
        is ScanResult.Success -> {
            println("Filled: ${result.filledIndices}")
            println("Confidence: ${result.confidence}")
            println("QR: ${result.qrPayload}")
        }
        is ScanResult.Error -> {
            println("Failed: ${result.javaClass.simpleName}")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it.

Performance & Accuracy

Metric Value
Average latency per frame < 150 ms on modern devices
Accuracy (well‑printed cards) > 99%
False positive rate < 0.5%
False negative rate < 2%

What's Next?

The roadmap for 2025 includes:

  • Multi‑frame averaging for improved accuracy.
  • Scan history with Room database.
  • Export results as CSV.
  • QR‑only / OMR‑only scanning modes.

Contribute

OpenScanVision is MIT‑licensed and open to contributions. If you're interested:

  • Star the repo on GitHub ⭐
  • Try the sample app and report issues.
  • Open a Pull Request with improvements.

Conclusion

Building OpenScanVision taught me a lot about computer vision, Android optimisation, and open‑source maintainership. I hope it helps other developers build better scanning apps.

If you have questions, feedback, or ideas – open an issue on GitHub or leave a comment below.

GitHub: MatiwosKebede/openscanvision

🙏 Acknowledgments

Built with dedication by Matiwos Kebede.

📌 What to Do Next

  1. Copy the article above.
  2. Go to Dev.to → Click "New Post".
  3. Paste the content.
  4. Add tags: android, kotlin, opencv, omr, scanning.
  5. Publish!

This article will attract developers who are looking for an OMR, QR and OMR and QR together solution or want to learn about Android + OpenCV. 🚀

Top comments (0)