use super::{MicroQrDecoder, matrix::QUIET_ZONE};
use crate::codes::qr::sample::{found_pattern_cross, run_center, scan_line_runs, walk_run};
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, flood_region};
use crate::imgproc::homography::Homography;
use crate::imgproc::sample::sample_grid;
use crate::imgproc::threshold::{adaptive_binarize_bradley, otsu_binarize, otsu_threshold};
use crate::symbol::Symbol;
const SIZES: [usize; 4] = [11, 13, 15, 17];
#[derive(Debug, Clone, Copy)]
pub(crate) struct Finder {
pub x: f32,
pub y: f32,
pub module: f32,
pub count: u32,
}
pub fn scan(frame: &GrayFrame<'_>) -> Result<Symbol> {
let threshold = otsu_threshold(frame);
let mut last = Error::undecodable("no Micro QR finder pattern found");
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 finders = find_finders(bin);
if finders.is_empty() {
return Err(Error::undecodable("no Micro QR finder pattern found"));
}
let decoder = MicroQrDecoder::new();
let mut last = Error::undecodable("Micro QR finder did not decode");
for finder in finders.iter().take(4) {
let Some(mut corners) = finder_ring_corners(bin, finder) else {
continue;
};
if shoelace(&corners) < 0.0 {
corners.swap(1, 3);
}
for rot in 0..4 {
let mut dst = [Point::new(0.0, 0.0); 4];
for (i, d) in dst.iter_mut().enumerate() {
let c = corners[(i + rot) % 4];
*d = Point::new(c.0, c.1);
}
let src = [
Point::new(0.0, 0.0),
Point::new(7.0, 0.0),
Point::new(7.0, 7.0),
Point::new(0.0, 7.0),
];
let Ok(h) = Homography::from_correspondences(src, dst) else {
continue;
};
for &dim in &SIZES {
let matrix = sample_grid(frame, &h, dim, threshold, QUIET_ZONE);
match decoder.decode_matrix(&matrix) {
Ok(mut sym) => {
let d = dim as f64;
let quad = [
h.map_f64(0.0, 0.0),
h.map_f64(d, 0.0),
h.map_f64(d, d),
h.map_f64(0.0, d),
];
sym.location = Some(Location {
outline: Quad::new(quad.map(|(x, y)| Point::new(x as f32, y as f32))),
rotation: None,
module_size: Some(finder.module),
});
return Ok(sym);
}
Err(e) => last = e,
}
}
}
}
Err(last)
}
pub(crate) 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
}
pub(crate) fn find_finders(bin: &BinaryImage) -> Vec<Finder> {
let (w, h) = (bin.width(), bin.height());
let mut out: Vec<Finder> = Vec::new();
for y in 0..h {
scan_line_runs(
w,
|x| bin.get(x, y),
|mid, counts| {
let Some(module) = found_pattern_cross(counts) else {
return;
};
let Some((vc, vend)) = walk_run(h as i32, y as i32, |k| bin.get(mid, k as usize))
else {
return;
};
if found_pattern_cross(vc).is_none() {
return;
}
let cy = run_center(vc, vend);
merge(&mut out, mid as f32, cy, module);
},
);
}
out.sort_by_key(|f| std::cmp::Reverse(f.count));
out
}
fn merge(finders: &mut Vec<Finder>, x: f32, y: f32, module: f32) {
for f in finders.iter_mut() {
if (f.x - x).abs() <= f.module && (f.y - y).abs() <= f.module {
let c = f.count as f32;
f.x = (f.x * c + x) / (c + 1.0);
f.y = (f.y * c + y) / (c + 1.0);
f.module = (f.module * c + module) / (c + 1.0);
f.count += 1;
return;
}
}
finders.push(Finder {
x,
y,
module,
count: 1,
});
}
pub(crate) fn finder_ring_corners(bin: &BinaryImage, finder: &Finder) -> Option<[(f32, f32); 4]> {
let w = bin.width();
let mut x = finder.x as usize;
let y = finder.y as usize;
let mut seen_light = false;
let mut seed = None;
let limit = ((finder.module * 4.5) as usize).max(4);
for _ in 0..limit {
if x + 1 >= w {
break;
}
x += 1;
let dark = bin.get(x, y);
if !dark {
seen_light = true;
} else if seen_light {
seed = Some((x, y));
break;
}
}
let pixels = flood_region(bin, seed?, true);
if pixels.is_empty() {
return None;
}
let span = 7.0 * finder.module;
let (min_x, max_x) = pixels.iter().fold((usize::MAX, 0), |(lo, hi), &(px, _)| {
(lo.min(px), hi.max(px))
});
let width = (max_x - min_x) as f32;
if width < span * 0.7 || width > span * 1.5 {
return None;
}
extreme_quad(&pixels)
}