use super::DataMatrixDecoder;
use super::placement::QUIET_ZONE;
use super::tables::all_squares;
use crate::error::{Error, Result};
use crate::geometry::{Location, Point, Quad};
use crate::image::GrayFrame;
use crate::imgproc::binary::BinaryImage;
use crate::imgproc::components::{Connectivity, connected_components};
use crate::imgproc::homography::Homography;
use crate::imgproc::line::{Line, fit_line_least_squares};
use crate::imgproc::sample::sample_bilinear;
use crate::imgproc::threshold::{otsu_binarize, otsu_threshold};
use crate::output::BitMatrix;
use crate::pipeline::{Candidate, Hints};
use crate::symbol::Symbol;
use crate::symbology::Symbology;
use crate::traits::{Analyze, Detect};
#[derive(Debug, Default, Clone, Copy)]
pub struct DataMatrixScanner;
impl DataMatrixScanner {
pub fn new() -> Self {
DataMatrixScanner
}
}
impl Detect for DataMatrixScanner {
fn detect(&self, frame: &GrayFrame<'_>, _hints: &Hints) -> Vec<Candidate> {
match locate_geometry(frame) {
Ok(loc) => vec![Candidate {
location: loc.as_location(),
symbology: Some(Symbology::DataMatrix),
fingerprint: None,
known: None,
}],
Err(_) => Vec::new(),
}
}
}
impl Analyze for DataMatrixScanner {
fn analyze(&self, frame: &GrayFrame<'_>, candidate: &Candidate) -> Result<Symbol> {
if let Some(known) = &candidate.known {
return Ok(known.clone());
}
scan(frame)
}
}
pub fn scan(frame: &GrayFrame<'_>) -> Result<Symbol> {
solve(frame).map(|(_, symbol)| symbol)
}
pub fn sample_grid(frame: &GrayFrame<'_>) -> Result<BitMatrix> {
solve(frame).map(|(located, _)| located.matrix)
}
struct Located {
matrix: BitMatrix,
corners: [Point; 4],
dim: usize,
}
impl Located {
fn as_location(&self) -> Location {
let [tl, tr, _, bl] = self.corners;
let rotation = (tr.y - tl.y).atan2(tr.x - tl.x);
let side = (tl.distance(tr) + tl.distance(bl)) / 2.0;
Location {
outline: Quad::new(self.corners),
rotation: Some(rotation),
module_size: Some(side / self.dim as f32),
}
}
}
struct Geometry {
bl: Point,
tl: Point,
br: Point,
tr: Point,
threshold: u8,
n_est: usize,
}
const MIN_FRAME: usize = 10;
fn solve(frame: &GrayFrame<'_>) -> Result<(Located, Symbol)> {
let geom = extract_geometry(frame)?;
let decoder = DataMatrixDecoder::new();
for &dim in &candidate_sizes(geom.n_est) {
for swap in [false, true] {
let dst = corner_targets(&geom, swap);
let src = grid_corners(dim);
let Ok(h) = Homography::from_correspondences(src, dst) else {
continue;
};
let matrix =
crate::imgproc::sample::sample_grid(frame, &h, dim, geom.threshold, QUIET_ZONE);
if let Ok(symbol) = decoder.decode_matrix(&matrix) {
let located = Located {
matrix,
corners: dst,
dim,
};
return Ok((located, symbol));
}
}
}
Err(Error::undecodable("no candidate Data Matrix grid decoded"))
}
fn locate_geometry(frame: &GrayFrame<'_>) -> Result<Located> {
let geom = extract_geometry(frame)?;
let dim = candidate_sizes(geom.n_est)[0];
let dst = corner_targets(&geom, false);
let src = grid_corners(dim);
let h = Homography::from_correspondences(src, dst)
.map_err(|_| Error::undecodable("degenerate Data Matrix corners"))?;
let matrix = crate::imgproc::sample::sample_grid(frame, &h, dim, geom.threshold, QUIET_ZONE);
Ok(Located {
matrix,
corners: dst,
dim,
})
}
fn grid_corners(dim: usize) -> [Point; 4] {
let d = dim as f32;
[
Point::new(0.0, 0.0),
Point::new(d, 0.0),
Point::new(d, d),
Point::new(0.0, d),
]
}
fn corner_targets(geom: &Geometry, swap: bool) -> [Point; 4] {
if swap {
[geom.br, geom.tr, geom.tl, geom.bl]
} else {
[geom.tl, geom.tr, geom.br, geom.bl]
}
}
fn extract_geometry(frame: &GrayFrame<'_>) -> Result<Geometry> {
if frame.width() < MIN_FRAME || frame.height() < MIN_FRAME {
return Err(Error::undecodable(
"frame too small for a Data Matrix symbol",
));
}
let threshold = otsu_threshold(frame);
let bin = otsu_binarize(frame);
let comps = connected_components(&bin, Connectivity::Eight);
let symbol = comps
.iter()
.max_by_key(|c| c.area)
.ok_or_else(|| Error::undecodable("no dark region found"))?;
if symbol.area < 16 {
return Err(Error::undecodable("dark region too small for a symbol"));
}
let b = symbol.bounds;
let pad = 2usize;
let x0 = b.min_x.saturating_sub(pad);
let y0 = b.min_y.saturating_sub(pad);
let x1 = (b.max_x + pad).min(bin.width() - 1);
let y1 = (b.max_y + pad).min(bin.height() - 1);
let corners = find_corners(&bin, x0, y0, x1, y1)?;
let centroid = centroid_of(&corners);
let dark: [f32; 4] = [
edge_darkness(&bin, corners[0], corners[1], centroid),
edge_darkness(&bin, corners[1], corners[2], centroid),
edge_darkness(&bin, corners[2], corners[3], centroid),
edge_darkness(&bin, corners[3], corners[0], centroid),
];
let mut l_vertex = 0usize;
let mut best = -1.0f32;
for i in 0..4 {
let s = dark[(i + 3) % 4] + dark[i];
if s > best {
best = s;
l_vertex = i;
}
}
let tl0 = corners[(l_vertex + 3) % 4];
let br0 = corners[(l_vertex + 1) % 4];
let bl0 = corners[l_vertex];
let tr0 = Point::new(tl0.x + br0.x - bl0.x, tl0.y + br0.y - bl0.y);
let side = (bl0.distance(tl0) + bl0.distance(br0)) / 2.0;
let n_rough = (count_modules(frame, tl0, tr0, centroid, threshold)
+ count_modules(frame, br0, tr0, centroid, threshold))
.div_ceil(2)
.max(4);
let module_px = (side / n_rough as f32).max(1.0);
let mut init = corners;
init[(l_vertex + 2) % 4] = tr0;
let refined = refine_corners(&bin, &init, centroid, module_px);
let bl = refined[l_vertex];
let tl = refined[(l_vertex + 3) % 4];
let br = refined[(l_vertex + 1) % 4];
let tr = refined[(l_vertex + 2) % 4];
let m1 = count_modules(frame, tl, tr, centroid, threshold);
let m2 = count_modules(frame, br, tr, centroid, threshold);
let n_est = (m1 + m2).div_ceil(2);
Ok(Geometry {
bl,
tl,
br,
tr,
threshold,
n_est,
})
}
fn centroid_of(pts: &[Point; 4]) -> Point {
let (sx, sy) = pts
.iter()
.fold((0.0f32, 0.0f32), |(sx, sy), p| (sx + p.x, sy + p.y));
Point::new(sx / 4.0, sy / 4.0)
}
fn find_corners(
bin: &BinaryImage,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
) -> Result<[Point; 4]> {
let mut sx = 0.0f64;
let mut sy = 0.0f64;
let mut n = 0u64;
for y in y0..=y1 {
for x in x0..=x1 {
if bin.get(x, y) {
sx += x as f64;
sy += y as f64;
n += 1;
}
}
}
if n < 16 {
return Err(Error::undecodable("too few dark pixels for a symbol"));
}
let centroid = Point::new((sx / n as f64) as f32, (sy / n as f64) as f32);
let p0 = farthest_from(bin, x0, y0, x1, y1, centroid);
let p2 = farthest_from(bin, x0, y0, x1, y1, p0);
let ax = p2.x - p0.x;
let ay = p2.y - p0.y;
let mut best_pos = 0.0f32;
let mut best_neg = 0.0f32;
let mut p1 = p0;
let mut p3 = p0;
for y in y0..=y1 {
for x in x0..=x1 {
if !bin.get(x, y) {
continue;
}
let cross = ax * (y as f32 - p0.y) - ay * (x as f32 - p0.x);
if cross > best_pos {
best_pos = cross;
p1 = Point::new(x as f32, y as f32);
} else if cross < best_neg {
best_neg = cross;
p3 = Point::new(x as f32, y as f32);
}
}
}
let mut pts = [p0, p1, p2, p3];
order_clockwise(&mut pts, centroid);
if quad_area(&pts) < 4.0 {
return Err(Error::undecodable("degenerate symbol quad"));
}
Ok(pts)
}
fn refine_corners(
bin: &BinaryImage,
corners: &[Point; 4],
centroid: Point,
module_px: f32,
) -> [Point; 4] {
let lines: [Line; 4] = std::array::from_fn(|i| {
let a = corners[i];
let b = corners[(i + 1) % 4];
edge_line(bin, a, b, centroid, module_px).unwrap_or_else(|| {
Line::from_points(a, b).unwrap_or(Line {
a: 1.0,
b: 0.0,
c: -a.x,
})
})
});
std::array::from_fn(|i| {
let prev = lines[(i + 3) % 4];
let cur = lines[i];
intersect(prev, cur).unwrap_or(corners[i])
})
}
fn edge_line(
bin: &BinaryImage,
a: Point,
b: Point,
centroid: Point,
module_px: f32,
) -> Option<Line> {
let dx = b.x - a.x;
let dy = b.y - a.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 2.0 {
return None;
}
let (ux, uy) = (dx / len, dy / len);
let mid = Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5);
let mut nx = -uy;
let mut ny = ux;
if nx * (mid.x - centroid.x) + ny * (mid.y - centroid.y) < 0.0 {
nx = -nx;
ny = -ny;
}
let steps = (len as usize).clamp(16, 2048);
let out_range = 1.5 * module_px;
let in_range = 0.4 * module_px;
let mut pts: Vec<Point> = Vec::new();
for s in 0..steps {
let t = 0.08 + 0.84 * (s as f32 + 0.5) / steps as f32;
let bx = a.x + ux * (len * t);
let by = a.y + uy * (len * t);
let mut found: Option<Point> = None;
let mut d = out_range;
while d >= -in_range {
let px = bx + nx * d;
let py = by + ny * d;
if px >= 0.0 && py >= 0.0 && bin.get(px.round() as usize, py.round() as usize) {
found = Some(Point::new(px, py));
break;
}
d -= 0.5;
}
if let Some(p) = found {
pts.push(p);
}
}
if pts.len() < 4 {
return None;
}
fit_line_least_squares(&pts)
}
fn intersect(l0: Line, l1: Line) -> Option<Point> {
let det = l0.a * l1.b - l1.a * l0.b;
if det.abs() < 1e-6 {
return None;
}
let x = (-l0.c * l1.b + l1.c * l0.b) / det;
let y = (-l0.a * l1.c + l1.a * l0.c) / det;
Some(Point::new(x, y))
}
fn farthest_from(bin: &BinaryImage, x0: usize, y0: usize, x1: usize, y1: usize, p: Point) -> Point {
let mut best = p;
let mut best_d = -1.0f32;
for y in y0..=y1 {
for x in x0..=x1 {
if !bin.get(x, y) {
continue;
}
let dx = x as f32 - p.x;
let dy = y as f32 - p.y;
let d = dx * dx + dy * dy;
if d > best_d {
best_d = d;
best = Point::new(x as f32, y as f32);
}
}
}
best
}
fn order_clockwise(pts: &mut [Point; 4], centroid: Point) {
pts.sort_by(|a, b| {
let aa = (a.y - centroid.y).atan2(a.x - centroid.x);
let bb = (b.y - centroid.y).atan2(b.x - centroid.x);
aa.partial_cmp(&bb).unwrap_or(std::cmp::Ordering::Equal)
});
}
fn quad_area(p: &[Point; 4]) -> f32 {
let mut s = 0.0f32;
for i in 0..4 {
let a = p[i];
let b = p[(i + 1) % 4];
s += a.x * b.y - b.x * a.y;
}
(s / 2.0).abs()
}
const INSET: f32 = 2.0;
fn edge_darkness(bin: &BinaryImage, a: Point, b: Point, centroid: Point) -> f32 {
let n = 24usize;
let mut dark = 0usize;
for k in 0..n {
let s = 0.15 + 0.7 * (k as f32 + 0.5) / n as f32;
let px = a.x + (b.x - a.x) * s;
let py = a.y + (b.y - a.y) * s;
let (ix, iy) = inset_toward(px, py, centroid, INSET);
if bin.get(ix, iy) {
dark += 1;
}
}
dark as f32 / n as f32
}
fn count_modules(
frame: &GrayFrame<'_>,
a: Point,
b: Point,
centroid: Point,
threshold: u8,
) -> usize {
let len = a.distance(b);
let k = ((len * 2.0) as usize).clamp(32, 4096);
let t = threshold as f64;
let mut prev: Option<bool> = None;
let mut transitions = 0usize;
for i in 0..k {
let s = (i as f32 + 0.5) / k as f32;
let px = a.x + (b.x - a.x) * s;
let py = a.y + (b.y - a.y) * s;
let (fx, fy) = inset_toward_f(px, py, centroid, INSET);
let dark = sample_bilinear(frame, fx, fy) <= t;
if let Some(p) = prev
&& p != dark
{
transitions += 1;
}
prev = Some(dark);
}
transitions + 1
}
fn inset_toward(px: f32, py: f32, centroid: Point, dist: f32) -> (usize, usize) {
let (fx, fy) = inset_toward_f(px, py, centroid, dist);
(fx.max(0.0) as usize, fy.max(0.0) as usize)
}
fn inset_toward_f(px: f32, py: f32, centroid: Point, dist: f32) -> (f64, f64) {
let dx = centroid.x - px;
let dy = centroid.y - py;
let len = (dx * dx + dy * dy).sqrt().max(1e-3);
((px + dx / len * dist) as f64, (py + dy / len * dist) as f64)
}
fn candidate_sizes(n_est: usize) -> Vec<usize> {
let mut sizes: Vec<usize> = all_squares().iter().map(|s| s.symbol_size).collect();
sizes.sort_by_key(|&s| (s as isize - n_est as isize).abs());
sizes.truncate(3);
sizes
}