use super::micro::{
MICRO_ROW_HEIGHT, MicroPdf417Decoder, NUM_VARIANTS, VAR_COLS, VAR_ROWS, variant_width,
};
use crate::error::{Error, Result};
use crate::geometry::{Location, Point, Quad};
use crate::image::GrayFrame;
use crate::imgproc::binary::BinaryImage;
use crate::imgproc::components::extreme_quad;
use crate::imgproc::homography::Homography;
use crate::imgproc::sample::sample_bilinear;
use crate::imgproc::threshold::{adaptive_binarize_bradley, otsu_binarize, otsu_threshold};
use crate::output::BitMatrix;
use crate::symbol::Symbol;
const MAX_ASPECT_MISMATCH: f32 = 1.4;
pub fn scan_micro(frame: &GrayFrame<'_>) -> Result<Symbol> {
let threshold = otsu_threshold(frame);
let mut last = Error::undecodable("no MicroPDF417 candidate geometry");
for bin in binarizations(frame) {
match scan_with(frame, &bin, threshold) {
Ok(sym) => return Ok(sym),
Err(e) => last = e,
}
}
Err(last)
}
fn binarizations(frame: &GrayFrame<'_>) -> [BinaryImage; 2] {
let radius = (frame.width().min(frame.height()) / 8).clamp(8, 50);
[
otsu_binarize(frame),
adaptive_binarize_bradley(frame, radius, 0.10),
]
}
fn scan_with(frame: &GrayFrame<'_>, bin: &BinaryImage, threshold: u8) -> Result<Symbol> {
let mut pixels = Vec::new();
for y in 0..bin.height() {
for x in 0..bin.width() {
if bin.get(x, y) {
pixels.push((x, y));
}
}
}
if pixels.len() < 64 {
return Err(Error::undecodable("no MicroPDF417 candidate geometry"));
}
let Some(mut corners) = extreme_quad(&pixels) else {
return Err(Error::undecodable("no MicroPDF417 candidate geometry"));
};
if shoelace(&corners) < 0.0 {
corners.swap(1, 3);
}
let side = |a: (f32, f32), b: (f32, f32)| (a.0 - b.0).hypot(a.1 - b.1);
let decoder = MicroPdf417Decoder::new();
let mut last = Error::undecodable("MicroPDF417 geometry did not decode");
for rot in 0..4usize {
let quad = [
corners[rot % 4],
corners[(rot + 1) % 4],
corners[(rot + 2) % 4],
corners[(rot + 3) % 4],
];
let qw = (side(quad[0], quad[1]) + side(quad[3], quad[2])) / 2.0;
let qh = (side(quad[0], quad[3]) + side(quad[1], quad[2])) / 2.0;
if qw < 4.0 || qh < 4.0 {
continue;
}
let measured = qw / qh;
let mut order: Vec<(f32, usize)> = (0..NUM_VARIANTS)
.filter_map(|v| {
let w = variant_width(VAR_COLS[v] as usize) as f32;
let h = (VAR_ROWS[v] as usize * MICRO_ROW_HEIGHT) as f32;
let ratio = (w / h) / measured;
let mismatch = ratio.max(1.0 / ratio);
(mismatch <= MAX_ASPECT_MISMATCH).then_some((mismatch, v))
})
.collect();
order.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
for (_, v) in order {
let w = variant_width(VAR_COLS[v] as usize);
let h = VAR_ROWS[v] as usize * MICRO_ROW_HEIGHT;
let src = [
Point::new(0.0, 0.0),
Point::new(w as f32, 0.0),
Point::new(w as f32, h as f32),
Point::new(0.0, h as f32),
];
let dst = quad.map(|(x, y)| Point::new(x, y));
let Ok(hom) = Homography::from_correspondences(src, dst) else {
continue;
};
let thr = f64::from(threshold);
let mut matrix = BitMatrix::new(w, h, 1);
for y in 0..h {
for x in 0..w {
let (px, py) = hom.map_f64(x as f64 + 0.5, y as f64 + 0.5);
if sample_bilinear(frame, px, py) <= thr {
matrix.set(x, y, true);
}
}
}
match decoder.decode_matrix(&matrix) {
Ok(mut sym) => {
sym.location = Some(Location {
outline: Quad::new(dst),
rotation: None,
module_size: Some(qw / w as f32),
});
return Ok(sym);
}
Err(e) => last = e,
}
}
}
Err(last)
}
fn shoelace(q: &[(f32, f32); 4]) -> f32 {
let mut sum = 0.0;
for i in 0..4 {
let (x0, y0) = q[i];
let (x1, y1) = q[(i + 1) % 4];
sum += x0 * y1 - x1 * y0;
}
sum
}