# datamatrix-reader ![License](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square) ![Python](https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square) ![Decoder](https://img.shields.io/badge/decoder-zxing--cpp-orange?style=flat-square) ![Detector](https://img.shields.io/badge/detector-YOLOv8%20%C2%B7%20ONNX-9cf?style=flat-square) [![Tests](https://github.com/WSILabs/datamatrix-reader/actions/workflows/tests.yml/badge.svg)](https://github.com/WSILabs/datamatrix-reader/actions/workflows/tests.yml) An **adaptive, source-agnostic DataMatrix reader** designed and tested on pathology slide labels — built on **zxing-cpp**, and optimized for the typical DataMatrix on a pathology slide imaged with a whole-slide scanner. It has been benchmarked on real clinical slides against libdmtx, raw (unassisted) zxing-cpp, and the commercial Dynamsoft Barcode Reader: | decoder | decode rate | |---|---:| | **datamatrix-reader** | **100%** | | Dynamsoft (commercial) | 98% | | zxing-cpp (raw) | 87% | | libdmtx (raw) | 72% | The decisive gap is the **broken-finder / damaged-timing tail**: this library reconstructs the canonical finder ("L") and timing pattern from the intact data modules and hands the clean symbol to zxing's Reed–Solomon stage — reading codes that even the commercial decoder misses, and never guessing a payload (every decode is ECC-validated). Note that this library is optimized for typical WSI-imaged slide labels: well- and evenly-lit captures, roughly **1200×850 px** label crops, near-cardinal orientation (any 90° rotation; in-plane skew within ~±15°), and reasonably-sized modules (**≳5 px each** — ~9 px/module on the WSI corpus). It has been hardened for the real defects whole-slide scanners actually encounter: faint or over-inked print, horizontal line-printing defects, glare, and especially the damaged finder ("L") and timing patterns that zxing alone struggles with. It is not optimized for oblique, heavily skewed or perspective-warped, heavily obscured, or poorly-lit barcodes. It also prioritizes **recovery over throughput** — built for single-shot or batch label decoding, not real-time / high-fps applications. It does two jobs: 1. **Single-code decode** — read the one DataMatrix on a slide label (the common case). 2. **DataMatrix engine** — inside a larger label reader: return *every* DataMatrix (with location), surface QR/Aztec as tagged hints, and stay in its lane (no OCR / 1D / layout — that's the host's job). Every decode is **ECC200/Reed–Solomon validated**, so a bad fit fails closed — the reader never mis-reads. ## How it works A miss-driven ladder: cheap things first, expensive recovery only when needed. ```mermaid flowchart TD img(["image"]) --> raw["zxing raw decode"] raw -->|"miss"| yolo["YOLO detector
localizes the code(s)"] yolo --> gate{"format-gate
zxing on tight crop"} gate -->|"undecoded / faint"| casc["ink-thickening cascade
CLAHE+upscale → Otsu → grow ink → Sauvola"] casc -->|"broken finder / timing"| rep["registration repair
sample grid → repaint L/timing → zxing ECC"] raw -->|"hit · p50 ~4 ms"| done(["✓ decoded · ECC-validated"]) gate -->|"DataMatrix"| done casc -->|"hit"| done rep -->|"recovered"| done gate -->|"QR / Aztec"| hint(["tagged non-DM hint"]) rep -->|"unrecoverable"| none(["✗ no decode"]) classDef ok fill:#1f883d,stroke:#1a7f37,color:#ffffff classDef tag fill:#bf8700,stroke:#9a6700,color:#ffffff classDef bad fill:#cf222e,stroke:#a40e26,color:#ffffff class done ok class hint tag class none bad ``` - **zxing-first.** On real captures a modern decoder beats hand-tuned preprocessing; preprocessing only runs on a miss, so the fast path stays a few milliseconds. - **Ink-thickening cascade** (`preprocess.py`) recovers faint / poorly-printed codes by progressively laying down ink at escalating strength. The costly 4×-upscale stages are gated on measured pixels-per-module, so well-sampled codes skip them. - **Registration repair** (`register.py`) handles broken *borders*: when the finder L or timing is damaged but the data modules survive, it reconstructs the module grid, repaints the canonical finder/timing, and lets zxing do the ECC — recovering codes a decoder alone can't, without ever guessing a payload. - **Learned detector** (`detect.py`): a YOLOv8-nano ONNX model (run via onnxruntime, no torch at inference) localizes DataMatrix and rejects look-alikes (QR/Aztec/mesh). It **ships in the wheel** and onnxruntime is a core dependency, so the detector is always on (a classical texture/gradient proposer in `locate.py` stays as an internal safety net). ## Install The learned detector ships with the package and its runtime (onnxruntime) is a core dependency, so a plain install gives you the full reader — no extras required. ```bash # pip — install straight from git, no clone needed pip install "datamatrix-reader @ git+https://github.com/WSILabs/datamatrix-reader" # uv uv pip install "datamatrix-reader @ git+https://github.com/WSILabs/datamatrix-reader" # conda / anaconda — make the env, then pip-install into it (the standard pattern) conda create -n dmr python=3.11 && conda activate dmr pip install "datamatrix-reader @ git+https://github.com/WSILabs/datamatrix-reader" ``` Core deps (numpy, opencv-python-headless, zxing-cpp, onnxruntime) install automatically. Extras: `[yolo-train]` = ultralytics (detector training/export, dev only); `[tools]` = pillow (GUI helper tools). ## Usage ```python import cv2 from datamatrix_reader.reader import Reader reader = Reader() # 1) single code — the fast path res = reader.read(cv2.imread("label.png")) # BGR or grayscale if res.ok: print(res.payload, res.stage, res.box) # bytes, how it was found, (x0,y0,x1,y1) # 2) every 2D code on a mixed label out = reader.read_all(cv2.imread("label.png")) for c in out.datamatrix: # each: payload, quad (4x2), box, format, stage print("DM", c.payload, c.box) for c in out.other_2d: # QR/Aztec as routable hints (payload may be None) print("hint", c.format, c.box) ``` - `read()` → `ReadResult(payload, stage, elapsed_ms, quad)` with `.ok` and `.box` (axis-aligned bbox derived from the quad). `stage` is how it decoded: `raw` · `gate` · `clahe` · `thick_u{f}_i{it}` · `sauv` · `autoreg`. - `read_all()` → `ReadAllResult(datamatrix, other_2d, elapsed_ms)` with `.payloads`. Each entry is a `Code(payload, quad, format, stage)` with `.box`. Quads/boxes are in **original-image coordinates**. **Validating payloads** is an application concern — the reader returns whatever decodes. `validate.py` offers helpers you apply yourself: ```python from datamatrix_reader.validate import RegexValidator ok = RegexValidator(r'^[A-Z]\d{2}-\d{5}-[A-Z]\d$') accession = res.payload if (res.ok and ok(res.payload)) else None ``` ## Design principles - **Robustness from coverage, not tuned constants.** A single pipeline bakes in assumptions some unseen input breaks. The ladder spans the variation instead, and every rung exits only on a *validated* decode — so there's little to overfit. - **Recovery over speed.** The clean-code fast path is raw zxing (p50 ≈ 4 ms), but recovery runs without a per-call deadline — the cascade and repair will spend real effort to read a degraded code (the worst broken-border label takes ~0.2 s on the WSI corpus). This is a single-shot / batch decoder, not a real-time one. - **Synthetic for generalization, real for confirmation.** Tune against parametric synthetic scenes (`synth.py`) that bracket the real failure axes (scale, rotation, ink gain/dropout, clutter); treat the real corpus as held-out confirmation, never the tuning target. Judge by the *worst* stratum, not the mean. ## Layout ``` src/datamatrix_reader/ reader.py public API: Reader.read / read_all -> ReadResult / ReadAllResult register.py unified pipeline (_collect) + registration repair + region detectors preprocess.py ink-thickening cascade stages (px/module-gated upscale) detect.py YOLO detector (onnxruntime, core) + format gate locate.py decoder-free region proposer (internal safety net) synth.py parametric synthetic label scenes for generalization testing validate.py optional payload validators (application-layer) models/dm_yolo.onnx the shipped detector weights tools/ validation harnesses (validate_full / read_all / synth / pathology), viz_search (animate the registration search), YOLO train/export, smoke_install (verify a built wheel ships + loads the model) tests/ synthetic unit tests ``` ## Validation ```bash pip install -e . python -m pytest -q # unit tests python -m tools.validate_full # read() over the real WSI corpus (correctness + timing) python -m tools.validate_read_all # read_all() multi-code coverage python -m tools.validate_synth # synthetic localization + decode rate ``` The real WSI corpus lives outside the package (real clinical captures, kept local — not committed); only code and the detector *weights* ship. Validated: **WSI 404/404, WRONG = 0**, p50 ~4 ms.