|
| 1 | +//! EAN/UPC decoding from per-scanline sub-pixel edge widths (the industry-standard |
| 2 | +//! way real scanners read the retail family off a camera frame). |
| 3 | +//! |
| 4 | +//! Where [`decode`](super::decode) consumes a clean, hard-quantized [`LinearPattern`] |
| 5 | +//! bool grid, this path consumes the raw run *widths* (in pixels) of one image scanline |
| 6 | +//! — as produced by [`crate::scan1d::scan_edges`] — and classifies each digit from its |
| 7 | +//! four element widths normalized to that digit's own local 7-module width. Measuring |
| 8 | +//! the module scale per digit tolerates the slowly-varying pitch a curved surface |
| 9 | +//! imposes, and reading widths (not a global integer grid) recovers the single-module |
| 10 | +//! guards and elements that hard quantization merges under optical blur. |
| 11 | +//! |
| 12 | +//! The recovered digits, variant and check digit are assembled into exactly the |
| 13 | +//! [`Symbol`] the structural decoder would produce for the same code, so both paths are |
| 14 | +//! interchangeable and the lossless re-encode invariant holds. |
| 15 | +
|
| 16 | +use super::tables::{EAN13_FIRST_PARITY, check_digit, l_widths, upce_expand, upce_from_parity}; |
| 17 | +use super::{EanMeta, EanVariant}; |
| 18 | +use crate::image::GrayFrame; |
| 19 | +use crate::scan1d::{ScanOptions, scan_edges}; |
| 20 | +use crate::segment::Segment; |
| 21 | +use crate::symbol::{Symbol, SymbolMeta}; |
| 22 | +use crate::symbology::Symbology; |
| 23 | + |
| 24 | +/// Maximum acceptable per-digit width-match score (sum of squared module-count |
| 25 | +/// deviations over the four elements). Clean digits score ~0; this rejects noise. |
| 26 | +const MAX_DIGIT_SCORE: f32 = 1.4; |
| 27 | +/// Maximum relative deviation of a guard element from the guard's mean element width. |
| 28 | +const GUARD_TOL: f32 = 0.55; |
| 29 | + |
| 30 | +/// Decode every EAN/UPC main variant from one scanline's run widths, trying both scan |
| 31 | +/// directions. Returns the assembled [`Symbol`] for the first variant that validates |
| 32 | +/// (guards, parity and check digit), or `None`. |
| 33 | +pub fn decode_edges(edges: &[f32]) -> Option<Symbol> { |
| 34 | + if edges.len() < 3 { |
| 35 | + return None; |
| 36 | + } |
| 37 | + let forward: Vec<f32> = edges.windows(2).map(|w| w[1] - w[0]).collect(); |
| 38 | + let mut reversed = forward.clone(); |
| 39 | + reversed.reverse(); |
| 40 | + for widths in [&forward, &reversed] { |
| 41 | + if let Some(sym) = decode_widths(widths) { |
| 42 | + return Some(sym); |
| 43 | + } |
| 44 | + } |
| 45 | + None |
| 46 | +} |
| 47 | + |
| 48 | +/// Try each main variant against a run-width sequence that starts with a bar. |
| 49 | +fn decode_widths(w: &[f32]) -> Option<Symbol> { |
| 50 | + let (variant, digits) = [try_ean13(w), try_ean8(w), try_upce(w)] |
| 51 | + .into_iter() |
| 52 | + .flatten() |
| 53 | + .next()?; |
| 54 | + Some(Symbol::new( |
| 55 | + symbology_of(variant), |
| 56 | + vec![Segment::numeric(digits)], |
| 57 | + SymbolMeta::Ean(EanMeta { |
| 58 | + variant, |
| 59 | + addon: None, |
| 60 | + }), |
| 61 | + )) |
| 62 | +} |
| 63 | + |
| 64 | +fn symbology_of(variant: EanVariant) -> Symbology { |
| 65 | + match variant { |
| 66 | + EanVariant::Ean13 => Symbology::Ean13, |
| 67 | + EanVariant::Ean8 => Symbology::Ean8, |
| 68 | + EanVariant::UpcA => Symbology::UpcA, |
| 69 | + EanVariant::UpcE => Symbology::UpcE, |
| 70 | + EanVariant::Ean2 => Symbology::Ean2, |
| 71 | + EanVariant::Ean5 => Symbology::Ean5, |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +fn to_ascii(vals: &[u8]) -> Vec<u8> { |
| 76 | + vals.iter().map(|&v| b'0' + v).collect() |
| 77 | +} |
| 78 | + |
| 79 | +/// Whether `n` guard elements are all within tolerance of their common mean width. |
| 80 | +fn guard_ok(elems: &[f32]) -> bool { |
| 81 | + let mean = elems.iter().sum::<f32>() / elems.len() as f32; |
| 82 | + if mean <= 0.0 { |
| 83 | + return false; |
| 84 | + } |
| 85 | + elems.iter().all(|&e| (e - mean).abs() <= GUARD_TOL * mean) |
| 86 | +} |
| 87 | + |
| 88 | +/// Classify a 4-element digit from its widths. `left` selects the left half (L/G, parity |
| 89 | +/// recovered) versus the right half (R). Returns `(digit, is_even, score)`; even is |
| 90 | +/// always `false` for right digits. |
| 91 | +fn classify(w: &[f32], left: bool) -> Option<(u8, bool, f32)> { |
| 92 | + let total: f32 = w.iter().sum(); |
| 93 | + if total <= 0.0 { |
| 94 | + return None; |
| 95 | + } |
| 96 | + let unit = total / 7.0; |
| 97 | + let norm = [w[0] / unit, w[1] / unit, w[2] / unit, w[3] / unit]; |
| 98 | + let score_against = |pat: [u8; 4]| -> f32 { |
| 99 | + (0..4) |
| 100 | + .map(|i| { |
| 101 | + let d = norm[i] - pat[i] as f32; |
| 102 | + d * d |
| 103 | + }) |
| 104 | + .sum() |
| 105 | + }; |
| 106 | + let mut best: Option<(u8, bool, f32)> = None; |
| 107 | + for d in 0..10u8 { |
| 108 | + let l = l_widths(d); |
| 109 | + // R widths equal L widths; G widths are L reversed. Left digits try L and G; |
| 110 | + // right digits try only R. |
| 111 | + let cands: &[(bool, [u8; 4])] = if left { |
| 112 | + &[(false, l), (true, [l[3], l[2], l[1], l[0]])] |
| 113 | + } else { |
| 114 | + &[(false, l)] |
| 115 | + }; |
| 116 | + for &(even, pat) in cands { |
| 117 | + let s = score_against(pat); |
| 118 | + if best.is_none_or(|(_, _, bs)| s < bs) { |
| 119 | + best = Some((d, even, s)); |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | + best.filter(|&(_, _, s)| s <= MAX_DIGIT_SCORE) |
| 124 | +} |
| 125 | + |
| 126 | +/// EAN-13 / UPC-A: 59 runs (start `101`, six left digits, centre `01010`, six right |
| 127 | +/// digits, end `101`). Searches a small start offset for the guard. |
| 128 | +fn try_ean13(w: &[f32]) -> Option<(EanVariant, Vec<u8>)> { |
| 129 | + at_offsets(w, 59, |r| { |
| 130 | + if !guard_ok(&r[0..3]) || !guard_ok(&r[27..32]) || !guard_ok(&r[56..59]) { |
| 131 | + return None; |
| 132 | + } |
| 133 | + let mut left = [0u8; 6]; |
| 134 | + let mut parity = [false; 6]; |
| 135 | + for i in 0..6 { |
| 136 | + let (d, even, _) = classify(&r[3 + i * 4..7 + i * 4], true)?; |
| 137 | + left[i] = d; |
| 138 | + parity[i] = even; |
| 139 | + } |
| 140 | + let first = (0..10u8).find(|&f| EAN13_FIRST_PARITY[f as usize] == parity)?; |
| 141 | + let mut right = [0u8; 6]; |
| 142 | + for i in 0..6 { |
| 143 | + let (d, _, _) = classify(&r[32 + i * 4..36 + i * 4], false)?; |
| 144 | + right[i] = d; |
| 145 | + } |
| 146 | + let mut vals = Vec::with_capacity(13); |
| 147 | + vals.push(first); |
| 148 | + vals.extend_from_slice(&left); |
| 149 | + vals.extend_from_slice(&right); |
| 150 | + if check_digit(&vals[..12]) != vals[12] { |
| 151 | + return None; |
| 152 | + } |
| 153 | + if first == 0 { |
| 154 | + Some((EanVariant::UpcA, to_ascii(&vals[1..]))) |
| 155 | + } else { |
| 156 | + Some((EanVariant::Ean13, to_ascii(&vals))) |
| 157 | + } |
| 158 | + }) |
| 159 | +} |
| 160 | + |
| 161 | +/// EAN-8: 43 runs (start `101`, four left L digits, centre `01010`, four right digits, |
| 162 | +/// end `101`). |
| 163 | +fn try_ean8(w: &[f32]) -> Option<(EanVariant, Vec<u8>)> { |
| 164 | + at_offsets(w, 43, |r| { |
| 165 | + if !guard_ok(&r[0..3]) || !guard_ok(&r[19..24]) || !guard_ok(&r[40..43]) { |
| 166 | + return None; |
| 167 | + } |
| 168 | + let mut vals = [0u8; 8]; |
| 169 | + for i in 0..4 { |
| 170 | + let (d, even, _) = classify(&r[3 + i * 4..7 + i * 4], true)?; |
| 171 | + if even { |
| 172 | + return None; // EAN-8 left digits are all odd (L). |
| 173 | + } |
| 174 | + vals[i] = d; |
| 175 | + } |
| 176 | + for i in 0..4 { |
| 177 | + let (d, _, _) = classify(&r[24 + i * 4..28 + i * 4], false)?; |
| 178 | + vals[4 + i] = d; |
| 179 | + } |
| 180 | + if check_digit(&vals[..7]) != vals[7] { |
| 181 | + return None; |
| 182 | + } |
| 183 | + Some((EanVariant::Ean8, to_ascii(&vals))) |
| 184 | + }) |
| 185 | +} |
| 186 | + |
| 187 | +/// UPC-E: 33 runs (start `101`, six L/G digits carrying number-system+check parity, end |
| 188 | +/// guard `010101`). |
| 189 | +fn try_upce(w: &[f32]) -> Option<(EanVariant, Vec<u8>)> { |
| 190 | + at_offsets(w, 33, |r| { |
| 191 | + if !guard_ok(&r[0..3]) || !guard_ok(&r[27..33]) { |
| 192 | + return None; |
| 193 | + } |
| 194 | + let mut payload = [0u8; 6]; |
| 195 | + let mut parity = [false; 6]; |
| 196 | + for i in 0..6 { |
| 197 | + let (d, even, _) = classify(&r[3 + i * 4..7 + i * 4], true)?; |
| 198 | + payload[i] = d; |
| 199 | + parity[i] = even; |
| 200 | + } |
| 201 | + let (ns, check) = upce_from_parity(&parity)?; |
| 202 | + if check_digit(&upce_expand(ns, &payload)) != check { |
| 203 | + return None; |
| 204 | + } |
| 205 | + let mut vals = Vec::with_capacity(8); |
| 206 | + vals.push(ns); |
| 207 | + vals.extend_from_slice(&payload); |
| 208 | + vals.push(check); |
| 209 | + Some((EanVariant::UpcE, to_ascii(&vals))) |
| 210 | + }) |
| 211 | +} |
| 212 | + |
| 213 | +/// Try `f` on `w[o..o+len]` for a few even start offsets (skipping spurious leading |
| 214 | +/// runs) and return its first success. |
| 215 | +fn at_offsets<F>(w: &[f32], len: usize, f: F) -> Option<(EanVariant, Vec<u8>)> |
| 216 | +where |
| 217 | + F: Fn(&[f32]) -> Option<(EanVariant, Vec<u8>)>, |
| 218 | +{ |
| 219 | + if w.len() < len { |
| 220 | + return None; |
| 221 | + } |
| 222 | + let max_off = (w.len() - len).min(6); |
| 223 | + (0..=max_off).step_by(2).find_map(|o| f(&w[o..o + len])) |
| 224 | +} |
| 225 | + |
| 226 | +/// Robustly read an EAN/UPC symbol from a frame by decoding every fine scanline |
| 227 | +/// ([`scan_edges`]) and taking the value that the most scanlines agree on. Consensus |
| 228 | +/// across scanlines is what makes a curved, blurred capture — where each individual line |
| 229 | +/// corrupts different thin features — decode reliably, and it also suppresses the rare |
| 230 | +/// false positive a single noisy line might slip past the checksum. |
| 231 | +pub fn scan(frame: &GrayFrame<'_>, opts: &ScanOptions) -> Option<Symbol> { |
| 232 | + let mut tally: Vec<(String, usize, Symbol)> = Vec::new(); |
| 233 | + for cand in scan_edges(frame, opts) { |
| 234 | + let Some(sym) = decode_edges(&cand.edges) else { |
| 235 | + continue; |
| 236 | + }; |
| 237 | + let key = format!("{:?}|{}", sym.symbology, sym.text().unwrap_or_default()); |
| 238 | + if let Some(entry) = tally.iter_mut().find(|(k, _, _)| *k == key) { |
| 239 | + entry.1 += 1; |
| 240 | + } else { |
| 241 | + let mut sym = sym; |
| 242 | + sym.location.get_or_insert(cand.location); |
| 243 | + tally.push((key, 1, sym)); |
| 244 | + } |
| 245 | + } |
| 246 | + tally |
| 247 | + .into_iter() |
| 248 | + .max_by_key(|(_, n, _)| *n) |
| 249 | + .map(|(_, _, s)| s) |
| 250 | +} |
0 commit comments