@@ -91,29 +91,38 @@ lighting and cylinder curvature; clean-render envelope in `tests/harness_image.r
9191real-capture envelope (bottle and can labels down to ≈4.6 px/module) in
9292` tests/real_world.rs ` .
9393
94- ² 1D detection is the shared ** ` scan1d ` ** luminance front-end (edge detection →
95- run-length → normalized ` LinearPattern ` ), feeding each symbology's decoder. The
96- end-to-end pixel→symbol path is verified for Code 128, Code 39 and EAN-13 in
97- ` tests/scan1d_pipeline.rs ` ; it applies to any standard bar/space linear code.
98- Through ` pipeline::scan_1d ` the read is rotation-complete: mirrored patterns cover
99- 180°, and any other angle is handled by estimating the crop's dominant texture
100- orientation and derotating before the rescan (` tests/rotated1d.rs ` ). The shared
101- ` pipeline::scan_1d ` / ` scan_all ` entry points (and so the CLI and the demo) currently
102- try the Code 128, EAN/UPC, Code 93, Code 39, ITF and Codabar decoders; the other
103- linear decoders are driven by handing ` scan1d::scan_lines ` candidates to
104- ` scan1d::try_decode ` . DataBar (finder-pattern based) and Pharmacode need dedicated
105- samplers.
106-
107- ³ Data Matrix ships an image sampler: Otsu binarization → largest-component isolation
108- → solid-L finder + timing-edge line fitting → perspective corners → ` imgproc ` grid
94+ ² 1D detection is the shared ** ` scan1d ` ** luminance front-end. Each scan line is a
95+ band-averaged profile (real pixels, no interpolation) thresholded against a sliding
96+ local mid-level, then ** cut into spans** at every run too wide to be a symbol element —
97+ so a label edge, a dark surface beyond the label, print beside the code or a second
98+ code on the same line do not disturb the read — and each span is quantized on its own
99+ with a joint module-width / bar-vs-space-bias fit. Scan lines run at any angle, straight
100+ out of the frame. ` pipeline::scan_1d ` tries horizontal, then the crop's
101+ edge-orientation peaks, then vertical; ` pipeline::scan_1d_at ` takes a known reading
102+ axis (the locator reports one), refined by profile sharpness. Readings are accepted by
103+ ** consensus** , not on a single decode: at least two scan lines must agree, the span must
104+ show real quiet zones, the payload must be plausibly long for its symbology (only
105+ Code 128, Code 93 and EAN/UPC carry a check character), and a reading that is a fragment
106+ of a longer, equally supported one is dropped. EAN/UPC additionally has a width-ratio
107+ reader for curved and blurred captures. The shared entry points currently try the
108+ Code 128, EAN/UPC, Code 93, Code 39, ITF and Codabar decoders; the other linear decoders
109+ are driven by handing ` scan1d::scan_spans ` candidates to ` scan1d::try_decode ` . DataBar
110+ (finder-pattern based) and Pharmacode need dedicated samplers. End-to-end coverage:
111+ ` tests/live_pipeline.rs ` , ` tests/scan1d_pipeline.rs ` , ` tests/rotated1d.rs ` .
112+
113+ ³ Data Matrix ships an image sampler: Otsu binarization → the largest few dark
114+ components tried in turn (so a dark surface or print beside the code does not displace
115+ it) → solid-L finder + timing-edge line fitting → perspective corners → ` imgproc ` grid
109116sampling, with symbol size and grid chirality confirmed by Reed–Solomon. Robust to
110117any-angle rotation, scale, blur/noise and (size-graded) tilt; envelope in
111118` tests/datamatrix_image.rs ` .
112119
113120⁴ PDF417 (finder-less) is located by its start/stop guard columns → RANSAC edge fit →
114121affine grid sampling → RS-corrected decode. Handles upright, rotation ≤±10°, scale,
115122blur and noise; perspective keystoning is out of scope (no interior anchor). Envelope
116- in ` tests/pdf417_image.rs ` .
123+ in ` tests/pdf417_image.rs ` . On the live path the locator reports a PDF417 as a linear
124+ region with its axis, and ` pipeline::scan_linear_at ` derotates the crop by it first, so
125+ located symbols read at any rotation.
117126
118127⁶ Aztec, Micro QR and rMQR ship image samplers built on their native fiducials:
119128Aztec's bullseye (equal-run cross-section scan → the enclosed light annulus floods
@@ -225,44 +234,70 @@ assert_eq!(encoder.encode(&decoded).unwrap(), encoding);
225234
226235For a camera feed you rarely want to fully decode every frame. The ` detect ` module
227236provides a fast, symbology-agnostic ** locator** that returns the * position* of every
228- code (a bounding quad + coarse family guess) in a single downscaled pass, ** without
229- decoding** — the cheap stage of the two-stage ` Detect ` → ` Analyze ` pipeline. It is
230- recall-biased but structurally picky: concentric-finder detection backs matrix
231- guesses, scene-sized merged blobs, text rows/columns masquerading as barcodes and
232- non-code ink densities are gated out, overlapping duplicates of one physical code are
233- suppressed, and a close-up code that defeats the texture pass is still reported from
234- its finder hits alone:
237+ code (a quad + coarse family guess) in a single downscaled pass, ** without decoding** —
238+ the cheap stage of the two-stage ` Detect ` → ` Analyze ` pipeline.
239+
240+ It box-averages the frame down, thresholds it against its * local* neighbourhood (so a
241+ lighting falloff, a shadow or a code on a dark product still binarizes), and sums each
242+ tile's gradients into a structure tensor. A tile whose edges all share one direction is
243+ a patch of bars — ** at any rotation** — and such tiles cluster only with neighbours on
244+ the same axis, so a barcode separates from the print around it and comes back as a box
245+ * aligned with the code* plus its ** reading axis** (` Location::rotation ` ). Incoherent
246+ edge-dense tiles are matrix/texture; concentric-finder hits (cross-checked on both axes
247+ and a diagonal) vouch for QR/Aztec. It is recall-biased but structurally picky: linear
248+ regions need a barcode's worth of unbroken bars filling their box, finderless matrix
249+ guesses need two well-separated tones at code-like ink coverage, scene-sized blobs and
250+ duplicates are dropped, and a close-up code that defeats the texture pass is still
251+ reported from its finders alone.
235252
236253``` rust
237254use anyd :: detect :: {locate, LocateOptions };
238-
239- let candidates = locate (& frame , & LocateOptions :: default ());
240- for c in & candidates {
241- // c.location — where the code is; c.symbology — family guess.
242- // Hand off to the matching sampler/decoder only for new regions.
255+ use anyd :: pipeline :: {scan_all, scan_linear_at};
256+
257+ for c in locate (& frame , & LocateOptions :: default ()) {
258+ // c.location.outline — where; c.symbology — family guess; for a linear region
259+ // c.location.rotation — the axis to scan along. Crop the outline's bounds (plus a
260+ // margin) and decode just that:
261+ // linear → scan_linear_at(&crop, axis) (1D readers, then PDF417 family)
262+ // matrix → scan_all(&crop)
243263}
244264```
245265
266+ ` detect::FrameDetector ` wraps this as a ` Detect ` that also reuses ` Hints ` : a candidate
267+ is matched to a previously decoded symbol when it sits where that symbol was * and* looks
268+ like it (a noise- and shift-tolerant ink fingerprint), so known codes skip re-analysis
269+ without a swapped-in code inheriting the old value.
270+
246271Benchmark (` cargo bench --bench detect ` , dependency-free, ` std::time::Instant ` ),
247272locating ** all** codes of all families per frame vs. a full QR decode:
248273
249274| resolution | locate median | locate FPS | full QR decode | detect speedup |
250275| -----------:| --------------:| -----------:| ---------------:| ---------------:|
251- | 640×480 | 0.29 ms | ~ 3400 | 12.1 ms | ~ 41× |
252- | 1280×720 | 0.70 ms | ~ 1430 | 14.1 ms | ~ 20× |
253- | 1920×1080 | 1.53 ms | ~ 650 | 17.4 ms | ~ 11× |
276+ | 640×480 | 0.43 ms | ~ 2300 | 4.5 ms | ~ 10× |
277+ | 1280×720 | 1.24 ms | ~ 800 | 6.6 ms | ~ 5× |
278+ | 1920×1080 | 2.75 ms | ~ 360 | 10.1 ms | ~ 4× |
279+
280+ Those frames are pristine renders on white, which any locator finds. The number that
281+ matters is end-to-end on camera-like scenes — codes planted off-centre in a 720p frame,
282+ rotated 0–90°, on a page, a dark surface or amid print, under a 2:1 lighting falloff,
283+ blurred and noisy — replayed through the same locate → crop → decode loop a front-end
284+ runs (` cargo run --release --example liveeval ` ):
285+
286+ | | read rate | wrong values | decode time / frame |
287+ | ---| ---:| ---:| ---:|
288+ | 1D (EAN-13, Code 128, Code 39, ITF) | 97.5% | 0 | ~ 50 ms crop batch |
289+ | 2D (QR, Aztec, PDF417, Data Matrix) | 83% | 0 | ~ 120 ms whole-frame pass |
254290
255- Recall 100% and false-positive rate 0% on the benchmark's planted-code frames.
256- Locating is comfortably real-time at every resolution; decoding then runs only on the
257- regions the locator hands off (and ` Hints ` lets already-decoded codes be skipped
258- across frames).
291+ Data Matrix is the weak one (its sampler needs a clean surround); QR and Aztec are at
292+ 97–100%. ` tests/live_pipeline.rs ` pins this behaviour in CI, including the things that
293+ must * not* read: text, half an EAN-13 (structurally a UPC-E), ITF fragments.
259294
260295The demo page shows the intended live wiring: the main thread runs ` locate ` on a
261- half-resolution grab ~ 30×/s, a crop worker decodes located regions off-thread —
262- routed by family, so linear crops take the fast 1D path ( ` scan_1d ` is
263- rotation-complete via mirrored patterns and gradient-orientation derotation) — a
264- second worker sweeps the full frame for self-localizing 2D codes, and decoded locks
265- are position-tracked across frames so the overlay follows the code between reads.
296+ half-resolution grab ~ 30×/s; a crop worker decodes located regions off-thread, linear
297+ crops first and along their reported axis; a second worker reads self-localizing 2D
298+ codes off the frame, alternating the half-resolution frame with the central half at
299+ full resolution; and decoded locks are position-tracked across frames so the overlay
300+ follows the code between reads.
266301
267302## Command-line tool (` anyd ` )
268303
0 commit comments