|
38 | 38 | //! (~4.5 px/module up to 2×), blurred (σ ≤ ~1.5 px), noisy (σ ≤ ~25 luminance), |
39 | 39 | //! brightness/contrast shifted, and under perspective tilt whose tolerance shrinks with |
40 | 40 | //! symbol size (≈ 0.20 at dim 10–12, ≈ 0.10 at dim 22, ≈ 0.05 at the multi-region |
41 | | -//! sizes). Because the background is assumed to contain a single symbol on a clean quiet |
42 | | -//! zone, every dark pixel is taken to belong to the code. |
| 41 | +//! sizes). The symbol is taken to be one of the largest dark connected components (its |
| 42 | +//! solid L finder spans the symbol's full height and width, so the component's box |
| 43 | +//! covers the symbol); each of the largest few plausibly shaped components is tried in |
| 44 | +//! turn, so a dark background around the label or print beside the code does not |
| 45 | +//! displace it — but dark clutter *inside* the symbol's box still skews the corners. |
43 | 46 | //! |
44 | 47 | //! Binarization is tried global-Otsu first, then — only if that fails to decode — an |
45 | 48 | //! adaptive (Bradley local-mean) fallback, so a symbol under a lighting gradient or glare |
@@ -186,28 +189,28 @@ fn solve_with( |
186 | 189 | bin: &BinaryImage, |
187 | 190 | threshold: u8, |
188 | 191 | ) -> Result<(Located, Symbol)> { |
189 | | - let geom = extract_geometry(frame, bin, threshold)?; |
190 | 192 | let decoder = DataMatrixDecoder::new(); |
191 | | - |
192 | | - // Try the nearest valid square sizes, and both grid handednesses, returning the |
193 | | - // first that survives Reed–Solomon. RS makes a false accept astronomically |
194 | | - // unlikely, so the first success is the true reading. |
195 | | - for &dim in &candidate_sizes(geom.n_est) { |
196 | | - for swap in [false, true] { |
197 | | - let dst = corner_targets(&geom, swap); |
198 | | - let src = grid_corners(dim); |
199 | | - let Ok(h) = Homography::from_correspondences(src, dst) else { |
200 | | - continue; |
201 | | - }; |
202 | | - let matrix = |
203 | | - crate::imgproc::sample::sample_grid(frame, &h, dim, geom.threshold, QUIET_ZONE); |
204 | | - if let Ok(symbol) = decoder.decode_matrix(&matrix) { |
205 | | - let located = Located { |
206 | | - matrix, |
207 | | - corners: dst, |
208 | | - dim, |
| 193 | + for geom in extract_geometries(frame, bin, threshold)? { |
| 194 | + // Try the nearest valid square sizes, and both grid handednesses, returning |
| 195 | + // the first that survives Reed–Solomon. RS makes a false accept astronomically |
| 196 | + // unlikely, so the first success is the true reading. |
| 197 | + for &dim in &candidate_sizes(geom.n_est) { |
| 198 | + for swap in [false, true] { |
| 199 | + let dst = corner_targets(&geom, swap); |
| 200 | + let src = grid_corners(dim); |
| 201 | + let Ok(h) = Homography::from_correspondences(src, dst) else { |
| 202 | + continue; |
209 | 203 | }; |
210 | | - return Ok((located, symbol)); |
| 204 | + let matrix = |
| 205 | + crate::imgproc::sample::sample_grid(frame, &h, dim, geom.threshold, QUIET_ZONE); |
| 206 | + if let Ok(symbol) = decoder.decode_matrix(&matrix) { |
| 207 | + let located = Located { |
| 208 | + matrix, |
| 209 | + corners: dst, |
| 210 | + dim, |
| 211 | + }; |
| 212 | + return Ok((located, symbol)); |
| 213 | + } |
211 | 214 | } |
212 | 215 | } |
213 | 216 | } |
@@ -235,7 +238,10 @@ fn locate_geometry_with( |
235 | 238 | bin: &BinaryImage, |
236 | 239 | threshold: u8, |
237 | 240 | ) -> Result<Located> { |
238 | | - let geom = extract_geometry(frame, bin, threshold)?; |
| 241 | + let geom = extract_geometries(frame, bin, threshold)? |
| 242 | + .into_iter() |
| 243 | + .next() |
| 244 | + .ok_or_else(|| Error::undecodable("no Data Matrix geometry recovered"))?; |
239 | 245 | let dim = candidate_sizes(geom.n_est)[0]; |
240 | 246 | let dst = corner_targets(&geom, false); |
241 | 247 | let src = grid_corners(dim); |
@@ -274,25 +280,50 @@ fn corner_targets(geom: &Geometry, swap: bool) -> [Point; 4] { |
274 | 280 |
|
275 | 281 | /// Extract corners, the L orientation, a size estimate and the binarization threshold |
276 | 282 | /// from a supplied binarization `bin` (and its global sampling `threshold`). |
277 | | -fn extract_geometry(frame: &GrayFrame<'_>, bin: &BinaryImage, threshold: u8) -> Result<Geometry> { |
| 283 | +/// Dark components tried as the symbol, largest first. |
| 284 | +const MAX_COMPONENTS: usize = 6; |
| 285 | + |
| 286 | +/// Symbol geometries for the largest plausible dark components, in that order. |
| 287 | +fn extract_geometries( |
| 288 | + frame: &GrayFrame<'_>, |
| 289 | + bin: &BinaryImage, |
| 290 | + threshold: u8, |
| 291 | +) -> Result<Vec<Geometry>> { |
278 | 292 | if frame.width() < MIN_FRAME || frame.height() < MIN_FRAME { |
279 | 293 | return Err(Error::undecodable( |
280 | 294 | "frame too small for a Data Matrix symbol", |
281 | 295 | )); |
282 | 296 | } |
283 | 297 |
|
284 | | - // The solid L finder forms the largest dark component; its bounding box covers the |
285 | | - // whole symbol (the L spans a full column and a full row), so restrict the corner |
286 | | - // search to it and drop any stray noise elsewhere. |
287 | | - let comps = connected_components(bin, Connectivity::Eight); |
288 | | - let symbol = comps |
| 298 | + // The solid L finder is one dark component whose bounding box covers the whole |
| 299 | + // symbol (the L spans a full column and a full row). On a clean crop it is the |
| 300 | + // largest component; in a scene the dark surface behind the label or a block of |
| 301 | + // print beside it can be larger, so the largest few that could be a symbol — a |
| 302 | + // box at least ten modules of a pixel each way, not a thin rule — are all tried. |
| 303 | + let mut comps = connected_components(bin, Connectivity::Eight); |
| 304 | + comps.sort_by_key(|c| core::cmp::Reverse(c.area)); |
| 305 | + let geoms: Vec<Geometry> = comps |
289 | 306 | .iter() |
290 | | - .max_by_key(|c| c.area) |
291 | | - .ok_or_else(|| Error::undecodable("no dark region found"))?; |
292 | | - if symbol.area < 16 { |
293 | | - return Err(Error::undecodable("dark region too small for a symbol")); |
| 307 | + .filter(|c| { |
| 308 | + let (w, h) = (c.bounds.width(), c.bounds.height()); |
| 309 | + c.area >= 16 && w >= MIN_FRAME && h >= MIN_FRAME && w <= 4 * h && h <= 4 * w |
| 310 | + }) |
| 311 | + .take(MAX_COMPONENTS) |
| 312 | + .filter_map(|c| extract_geometry(frame, bin, threshold, &c.bounds).ok()) |
| 313 | + .collect(); |
| 314 | + if geoms.is_empty() { |
| 315 | + return Err(Error::undecodable("no dark region shaped like a symbol")); |
294 | 316 | } |
295 | | - let b = symbol.bounds; |
| 317 | + Ok(geoms) |
| 318 | +} |
| 319 | + |
| 320 | +/// Symbol geometry for the dark component bounded by `b`. |
| 321 | +fn extract_geometry( |
| 322 | + frame: &GrayFrame<'_>, |
| 323 | + bin: &BinaryImage, |
| 324 | + threshold: u8, |
| 325 | + b: &crate::imgproc::components::BoundingBox, |
| 326 | +) -> Result<Geometry> { |
296 | 327 | let pad = 2usize; |
297 | 328 | let x0 = b.min_x.saturating_sub(pad); |
298 | 329 | let y0 = b.min_y.saturating_sub(pad); |
|
0 commit comments