Skip to content

Commit c879d0f

Browse files
MagicalTuxclaude
andcommitted
datamatrix: try the largest few dark components as the symbol
The sampler took the single largest dark connected component to be the symbol, so a dark surface behind the label or a block of print beside the code displaced it and nothing decoded. Try the largest few plausibly shaped components in turn (the L finder spans the whole symbol, so its component's box covers it). liveeval: ground-truth-crop reads 33% -> 53%. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 451ea38 commit c879d0f

2 files changed

Lines changed: 67 additions & 48 deletions

File tree

‎examples/liveeval.rs‎

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,9 @@ fn codes() -> Vec<Code> {
101101
sprite: |s| {
102102
let e = Pdf417Encoder::new();
103103
let sym = e.build_text("PDF417 LIVE", pdf417::EcLevel::new(2).unwrap());
104-
let img = render(&e.encode(&sym.unwrap()).unwrap(), s);
105-
// Rows are rendered one module tall; real symbols use ~3 modules per row.
106-
stretch_y(&img, 3)
104+
render(&e.encode(&sym.unwrap()).unwrap(), s)
107105
},
108-
scales: [2, 3],
106+
scales: [3, 5],
109107
},
110108
Code {
111109
name: "ean13",
@@ -164,16 +162,6 @@ fn codes() -> Vec<Code> {
164162
]
165163
}
166164

167-
fn stretch_y(img: &GrayImage, k: usize) -> GrayImage {
168-
let mut out = GrayImage::filled(img.width(), img.height() * k, 255);
169-
for y in 0..out.height() {
170-
for x in 0..out.width() {
171-
out.set(x, y, img.get(x, y / k));
172-
}
173-
}
174-
out
175-
}
176-
177165
/// Paste `sprite` rotated by `deg` about the canvas centre `(cx, cy)`; returns the
178166
/// axis-aligned bounds of the pasted sprite.
179167
fn paste(canvas: &mut GrayImage, sprite: &GrayImage, cx: f32, cy: f32, deg: f32) -> [f32; 4] {

‎src/codes/datamatrix/sample.rs‎

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@
3838
//! (~4.5 px/module up to 2×), blurred (σ ≤ ~1.5 px), noisy (σ ≤ ~25 luminance),
3939
//! brightness/contrast shifted, and under perspective tilt whose tolerance shrinks with
4040
//! 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.
4346
//!
4447
//! Binarization is tried global-Otsu first, then — only if that fails to decode — an
4548
//! adaptive (Bradley local-mean) fallback, so a symbol under a lighting gradient or glare
@@ -186,28 +189,28 @@ fn solve_with(
186189
bin: &BinaryImage,
187190
threshold: u8,
188191
) -> Result<(Located, Symbol)> {
189-
let geom = extract_geometry(frame, bin, threshold)?;
190192
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;
209203
};
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+
}
211214
}
212215
}
213216
}
@@ -235,7 +238,10 @@ fn locate_geometry_with(
235238
bin: &BinaryImage,
236239
threshold: u8,
237240
) -> 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"))?;
239245
let dim = candidate_sizes(geom.n_est)[0];
240246
let dst = corner_targets(&geom, false);
241247
let src = grid_corners(dim);
@@ -274,25 +280,50 @@ fn corner_targets(geom: &Geometry, swap: bool) -> [Point; 4] {
274280

275281
/// Extract corners, the L orientation, a size estimate and the binarization threshold
276282
/// 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>> {
278292
if frame.width() < MIN_FRAME || frame.height() < MIN_FRAME {
279293
return Err(Error::undecodable(
280294
"frame too small for a Data Matrix symbol",
281295
));
282296
}
283297

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
289306
.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"));
294316
}
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> {
296327
let pad = 2usize;
297328
let x0 = b.min_x.saturating_sub(pad);
298329
let y0 = b.min_y.saturating_sub(pad);

0 commit comments

Comments
 (0)