yomu 1.2.0
yomu: ^1.2.0 copied to clipboard
Pure Dart QR code and barcode reader library with zero dependencies.
CHANGELOG #
1.2.0 #
Features #
DecodeEffortreplaces thetryHarderflag: the retry ladder was a boolean, with one fast point, one exhaustive point and nothing between them. It is now a three-leveleffortparameter, split where the cost jumps: between stages that reuse the binarized image and stages that rebuild it from the source pixels. Successful scans are unaffected at every level.DecodeEffort.fast: no retries. 83.1% of the fixture corpus (167/201), 2.6ms on a textured Full HD frame holding no code.DecodeEffort.balanced: corner grid search, despeckle and tolerant finder. 93.5% (188/201), 18.0ms.DecodeEffort.thorough(default): adds the full-resolution retry and the threshold sweep. 95.5% (192/201), 51.3ms.balancedrecovers 21 of the 25 codesthoroughadds overfast, for a third of the cost on a textured frame.
Yomu.responsivepreset: all formats atDecodeEffort.balanced, for streams that can afford more per frame thanYomu.realtime.- Alternate-threshold retry sweep: when every stage at the configured
binarizerThresholdfails,decodere-binarizes at 0.6 / 1.0 / 1.1 and re-runs the ladder. Codes that fail by shifting contrast rather than destroying it (screen moire, heavy low-light sensor noise, casual-scan blur) come out clean at a different factor.gaussian_noise_120,moire_0.8andcomposite_scan_blur_5.5go from undecodable to decoding. decodeAllthreshold sweep: a sheet whose codes have differing contrast (one clean, one occluded) is re-scanned at the alternate factors when a pass decodes fewer codes than it detected, and the best pass wins. Passes are compared rather than merged, so two codes carrying the same text on one sheet are both preserved.tryHarderis deprecated but still honored:falsemaps toDecodeEffort.fast,truetoDecodeEffort.thorough, andyomu.tryHarderstill reads back, so existing call sites keep compiling and keep their behavior.DecodeEffort.thoroughis slower than the oldtryHarder: trueon images holding no code at all: a heavily textured Full HD frame costs 51ms against ~20ms, the extra work being the threshold sweep.Yomu.responsiveandYomu.realtimeare the ways to decline that cost.
Performance #
- Block-averaged binarizer: the adaptive threshold now comes from an integral image of per-block (4x4) mean luminance instead of a per-pixel one. The averaging window is at least 40px wide, so the threshold surface varies far more slowly than the block grid; quantizing it to blocks costs no meaningful accuracy while cutting the per-pixel work to one load and one compare.
- Branchless thresholding:
luminance <= thresholdis now the sign bit ofluminance - (threshold + 1)rather than anif. Binarizing a photograph otherwise means one unpredictable branch per pixel, and the misprediction dominated the compare; removing it cut the threshold pass by 3.4x on high-entropy input. - Whole-pixel image conversion: RGBA/BGRA to luminance (and the fused downsample) reads one 32-bit word per pixel instead of three bounds-checked bytes, falling back to per-byte access for unaligned buffers and strides that are not a whole number of pixels.
- Binarization was ~65% of decode time and now costs roughly one sequential pass over the image (1.3x a bare read loop, against 4.2x before). On the fixture corpus (AOT): standard QR -30%, high-version QR -44%, uneven lighting -40%, Full HD / 4K -39%, barcodes -22%. Per image: 4K 3.51ms -> 2.12ms, Full HD 1.99ms -> 1.24ms, version 7 2.70ms -> 1.57ms.
- Table-driven data masks: six of the eight mask patterns built their 32-bit words a bit at a time, two integer modulos per module, and unmasking runs twice per decode attempt since the second XOR is what restores the matrix. Every mask condition is periodic - 12 rows by 3 word alignments covers all of them - so those words are constants now, 1.1 KiB in total. Generating the mask words for a version 40 symbol: 23.5us -> 1.8us. On the fixture corpus (AOT): images holding no code -6.5%, distorted -4.1%, whole corpus -4.4%.
Fixes #
- Finder pattern run lengths no longer wrap at 255: the run lengths behind the 1:1:3:1:1 test were counted in a byte, but a run is bounded by the image - the white margin around a code on a megapixel frame passes 255 pixels easily. A real finder pattern was never at risk (that would need a module wider than 255 pixels, which downsampling rules out), but a run that is nothing like one could read as one:
black(10) gap(266) black(30) white(10) black(10)is a textbook 1:1:3:1:1 once the gap is taken modulo 256. Such a candidate still had to survive decoding, so this cost work on noisy input rather than producing wrong results.
Test fixtures #
gaussian_noise_120,moire_0.8andcomposite_scan_blur_5.5moved fromfixtures/unsupported_imagestofixtures/distorted_images, and the stress generator gained the next rung on each axis so the boundary is pinned from above again (moire 0.9, composite scan blur 6.0, low-light noise sigma 170).- Low-light noise is the one probabilistic axis: each sigma draws a single noise field, so a fixture near the transition asserts its own draw rather than the decoder's limit. Over 20 independent draws per sigma the decode rate runs 110 -> 100%, 130 -> 75%, 150 -> 50%, 170 -> 5%, so the rungs are taken from the flat ends (sigma 120 decodes, sigma 170 does not) instead of the 130-160 band.
- Of the 198 images that existed before, three more decode (189 -> 192); the corpus is now 201 images as the ladders were extended.
1.1.0 #
Features #
- Try-Harder Mode (default on):
Yomu.decodenow runs escalating retry strategies when the fast path fails, significantly improving the detection rate (fixture corpus: 84.3% -> 95.5%, 167/198 -> 189/198). Successful scans are unaffected; retries only run on images the fast path cannot decode.- Corner grid search: per-axis dimension candidates plus a grid search of the bottom-right corner rescue perspective-distorted codes.
- Despeckle retry: a word-parallel 3x3 majority filter (
BitMatrix.majority3x3) recovers codes under salt & pepper noise (validated up to 20% pixel noise). - Tolerant finder: clusters raw row-scan hits without the strict vertical cross-check, recovering slanted finder patterns under strong perspective.
- Full-resolution retry: re-runs detection without downsampling when a downsampled pass fails, recovering small codes in high-resolution frames.
- Retries are bounded by a deterministic work budget and grid-search deduplication, so undecodable inputs cannot make the failure path pathologically slow.
- Set
tryHarder: falsefor the previous fast-only behavior (latency-critical per-frame scanning).
decodeAllretry passes: multi-code scanning applies the same strategy. Detected-but-undecodable codes get the corner-grid rescue, and a pass that finds nothing escalates to despeckle and a full-resolution pass (a noisy 3-code sheet and two 90px codes in a 4K frame go from 0 to fully decoded).Yomu.realtimepreset: all formats withtryHarderdisabled, tuned for per-frame camera scanning where a missed frame is cheaper than a slower failure path.- With
tryHarderenabled, a detected-but-undecodable QR code now falls through to barcode scanning instead of propagating aDecodeException.
Test fixtures #
- Fixture ladders now bracket the current capability boundary on both sides, with boundary-pinning tests on each side (see the Supported Image Classes table in the README).
- New distortion axes derived from the modern imaging pipeline: low-light Gaussian noise, JPEG quantization artifacts, specular glare, screen moire, and a composite casual-scan recipe (mild perspective + lighting gradient + blur) that demonstrates composition lowering the single-axis boundary.
- The legacy
perspective_{x,y}fixtures above 0.2 cropped the finder patterns out of the canvas (invalid test images); they are replaced by a padded transform that keeps the code fully visible. fixtures/unsupported_imagesnow contains only images beyond the capability boundary; everything rescued by the retry strategies moved tofixtures/distorted_images.
1.0.0 #
Initial stable release.
Features #
- Pure Dart Implementation: A zero-dependency QR code and barcode reader library. No native code required, making it highly portable across Flutter, Web, and Server-side Dart.
- QR Code Support:
- Full support for QR Code versions 1 to 40.
- Supports all error correction levels (L, M, Q, H).
- Robust multi-QR detection and decoding in a single image.
- High resilience against perspective distortion, rotation, and uneven lighting.
- 1D Barcode Support:
- Retail: EAN-13 (including JAN), EAN-8, UPC-A.
- Industrial: Code 128, Code 39, ITF (Interleaved 2 of 5), Codabar.
- High Performance:
- Specifically optimized for AOT compilation and performance.
- Efficiently handles high-resolution images (> 1MP) using internal fused downsampling and conversion.
- Capable of real-time decoding on mobile and desktop platforms.
- Flexible Image API: Platform-agnostic
YomuImagecontainer supporting various pixel formats including RGBA, BGRA, Grayscale, and YUV420 (camera stream Y-plane).