mod finder;
mod grid;
mod tiles;
use crate::geometry::{Location, Point, Quad};
use crate::image::GrayFrame;
use crate::pipeline::{Candidate, Fingerprint, Hints};
use crate::symbology::Symbology;
use crate::traits::Detect;
use finder::FinderHit;
use grid::DownGrid;
use tiles::{Family, Region};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Families {
pub matrix: bool,
pub linear: bool,
}
impl Families {
pub const ALL: Families = Families {
matrix: true,
linear: true,
};
pub const MATRIX: Families = Families {
matrix: true,
linear: false,
};
pub const LINEAR: Families = Families {
matrix: false,
linear: true,
};
fn allows(self, family: Family) -> bool {
match family {
Family::Matrix => self.matrix,
Family::Linear => self.linear,
}
}
}
impl Default for Families {
fn default() -> Self {
Families::ALL
}
}
#[derive(Debug, Clone, Copy)]
pub struct LocateOptions {
pub downscale: usize,
pub families: Families,
pub max_candidates: usize,
pub tile: usize,
pub edge_density: f32,
pub min_region_tiles: usize,
pub anisotropy: f32,
pub max_region_frac: f32,
pub seed: u64,
}
impl Default for LocateOptions {
fn default() -> Self {
LocateOptions {
downscale: 2,
families: Families::ALL,
max_candidates: 16,
tile: 8,
edge_density: 0.12,
min_region_tiles: 3,
anisotropy: 0.55,
max_region_frac: 0.6,
seed: 0x_D0D0_CAFE,
}
}
}
const SUPPRESS_OVERLAP: f32 = 0.5;
const MATRIX_DARK_FRAC: std::ops::Range<f32> = 0.22..0.85;
const LINEAR_MIN_ASPECT: f32 = 0.75;
const MAX_BAR_FLIPS_PER_PX: f32 = 0.20;
const FINDER_SYNTH_MIN_COUNT: u32 = 2;
pub fn locate(frame: &GrayFrame<'_>, opts: &LocateOptions) -> Vec<Candidate> {
if frame.width() < 8 || frame.height() < 8 {
return Vec::new();
}
let grid = DownGrid::build(frame, opts.downscale);
let finders = finder::find(&grid);
let regions = tiles::regions(
&grid,
opts.tile,
opts.edge_density,
opts.min_region_tiles,
opts.anisotropy,
);
let max_area = (opts.max_region_frac * (grid.width * grid.height) as f32) as usize;
struct Scored<'a> {
region: Region,
family: Family,
hit: Option<&'a FinderHit>,
}
let debug = std::env::var("ANYD_LOC_DEBUG").is_ok();
if debug {
for f in &finders {
eprintln!(
"finder ({},{}) module={:.2} count={}",
f.x as usize * grid.scale,
f.y as usize * grid.scale,
f.module,
f.count
);
}
}
let mut scored: Vec<Scored<'_>> = Vec::new();
for region in regions {
if debug {
eprintln!(
"region ({},{})-({},{}) {:?} rh={} area={} lin_ok={} mat_ok={}",
region.x0 * grid.scale,
region.y0 * grid.scale,
region.x1 * grid.scale,
region.y1 * grid.scale,
region.family,
region.reads_horizontal,
region.area(),
linear_plausible(&grid, ®ion),
matrix_plausible(&grid, ®ion),
);
}
let hit = enclosing_finder(&finders, ®ion);
let finder_backed = hit.is_some_and(|h| h.count >= FINDER_SYNTH_MIN_COUNT);
if region.area() > max_area && !finder_backed {
continue;
}
let family = if hit.is_some() {
Family::Matrix
} else {
region.family
};
if !opts.families.allows(family) {
continue;
}
match family {
Family::Linear if !linear_plausible(&grid, ®ion) => continue,
Family::Matrix if hit.is_none() && !matrix_plausible(&grid, ®ion) => continue,
_ => {}
}
scored.push(Scored {
region,
family,
hit,
});
}
scored.sort_by_key(|s| {
(
std::cmp::Reverse(u8::from(s.hit.is_some())),
std::cmp::Reverse(s.region.area()),
)
});
let scale = grid.scale as f32;
let mut accepted: Vec<[usize; 4]> = Vec::new();
let mut out: Vec<Candidate> = Vec::new();
for s in &scored {
if out.len() >= opts.max_candidates {
break;
}
let core = [s.region.x0, s.region.y0, s.region.x1, s.region.y1];
if accepted
.iter()
.any(|a| overlap_min_frac(*a, core) > SUPPRESS_OVERLAP)
{
continue;
}
accepted.push(core);
let symbology = match (s.family, s.hit.is_some()) {
(Family::Matrix, true) => Symbology::QrCode,
(Family::Matrix, false) => Symbology::DataMatrix,
(Family::Linear, _) => Symbology::Code128,
};
let module_size = s.hit.map(|h| h.module * scale);
let [mut x0, mut y0, mut x1, mut y1] = core;
if s.family == Family::Linear {
let t = opts.tile.max(1);
x0 = x0.saturating_sub(t);
y0 = y0.saturating_sub(t);
x1 = (x1 + t).min(grid.width);
y1 = (y1 + t).min(grid.height);
}
let location = Location {
outline: box_quad([x0, y0, x1, y1], scale),
rotation: None,
module_size,
};
out.push(Candidate {
location,
symbology: Some(symbology),
fingerprint: Some(fingerprint(&grid, core, s.family)),
known: None,
});
}
if opts.families.matrix {
for f in &finders {
if out.len() >= opts.max_candidates {
break;
}
if f.count < FINDER_SYNTH_MIN_COUNT {
continue;
}
let (fx, fy) = (f.x as usize, f.y as usize);
if accepted
.iter()
.any(|&[x0, y0, x1, y1]| fx >= x0 && fx < x1 && fy >= y0 && fy < y1)
{
continue;
}
let half = f.module * 12.5;
let x0 = (f.x - half).max(0.0) as usize;
let y0 = (f.y - half).max(0.0) as usize;
let x1 = ((f.x + half).max(0.0) as usize).min(grid.width);
let y1 = ((f.y + half).max(0.0) as usize).min(grid.height);
if x1 <= x0 || y1 <= y0 {
continue;
}
let core = [x0, y0, x1, y1];
accepted.push(core);
out.push(Candidate {
location: Location {
outline: box_quad(core, scale),
rotation: None,
module_size: Some(f.module * scale),
},
symbology: Some(Symbology::QrCode),
fingerprint: Some(fingerprint(&grid, core, Family::Matrix)),
known: None,
});
}
}
out
}
fn overlap_min_frac(a: [usize; 4], b: [usize; 4]) -> f32 {
let ix = a[2].min(b[2]).saturating_sub(a[0].max(b[0]));
let iy = a[3].min(b[3]).saturating_sub(a[1].max(b[1]));
let inter = (ix * iy) as f32;
let area_a = ((a[2] - a[0]) * (a[3] - a[1])) as f32;
let area_b = ((b[2] - b[0]) * (b[3] - b[1])) as f32;
let min = area_a.min(area_b);
if min > 0.0 { inter / min } else { 0.0 }
}
fn matrix_plausible(grid: &DownGrid, region: &Region) -> bool {
let area = region.area();
if area == 0 {
return false;
}
let mut dark = 0usize;
for y in region.y0..region.y1 {
for x in region.x0..region.x1 {
dark += usize::from(grid.dark(x, y));
}
}
MATRIX_DARK_FRAC.contains(&(dark as f32 / area as f32))
}
fn linear_plausible(grid: &DownGrid, region: &Region) -> bool {
let w = region.x1 - region.x0;
let h = region.y1 - region.y0;
if w == 0 || h == 0 {
return false;
}
let (read, bars) = if region.reads_horizontal {
(w, h)
} else {
(h, w)
};
if (read as f32) < LINEAR_MIN_ASPECT * bars as f32 {
return false;
}
let mut flips = 0u32;
for u in 0..read {
let mut prev: Option<bool> = None;
for v in 0..bars {
let (x, y) = if region.reads_horizontal {
(region.x0 + u, region.y0 + v)
} else {
(region.x0 + v, region.y0 + u)
};
let d = grid.dark(x, y);
if prev == Some(!d) {
flips += 1;
}
prev = Some(d);
}
}
let per_px = flips as f32 / (read * bars) as f32;
per_px <= MAX_BAR_FLIPS_PER_PX
}
fn enclosing_finder<'a>(finders: &'a [FinderHit], region: &Region) -> Option<&'a FinderHit> {
finders
.iter()
.filter(|f| {
let x = f.x as usize;
let y = f.y as usize;
x >= region.x0 && x < region.x1 && y >= region.y0 && y < region.y1
})
.max_by_key(|f| f.count)
}
fn box_quad(b: [usize; 4], scale: f32) -> Quad {
let x0 = b[0] as f32 * scale;
let y0 = b[1] as f32 * scale;
let x1 = b[2] as f32 * scale;
let y1 = b[3] as f32 * scale;
Quad::new([
Point::new(x0, y0),
Point::new(x1, y0),
Point::new(x1, y1),
Point::new(x0, y1),
])
}
fn fingerprint(grid: &DownGrid, b: [usize; 4], family: Family) -> Fingerprint {
let mut bits: u64 = 0;
let w = (b[2] - b[0]).max(1);
let h = (b[3] - b[1]).max(1);
let mut sum = 0u64;
let mut n = 0u64;
for gy in 0..4 {
for gx in 0..4 {
let x = b[0] + gx * w / 4 + w / 8;
let y = b[1] + gy * h / 4 + h / 8;
sum += u64::from(grid.luma(x, y));
n += 1;
}
}
let mean = (sum / n.max(1)) as u8;
let mut i = 0;
for gy in 0..4 {
for gx in 0..4 {
let x = b[0] + gx * w / 4 + w / 8;
let y = b[1] + gy * h / 4 + h / 8;
if grid.luma(x, y) <= mean {
bits |= 1 << i;
}
i += 1;
}
}
let tag: u64 = match family {
Family::Matrix => 0x1,
Family::Linear => 0x2,
};
let mut v = bits | (tag << 62);
v ^= v >> 33;
v = v.wrapping_mul(0xFF51_AFD7_ED55_8CCD);
v ^= v >> 33;
Fingerprint(v)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FrameDetector {
pub options: LocateOptions,
}
impl FrameDetector {
pub fn new() -> Self {
FrameDetector::default()
}
pub fn with_options(options: LocateOptions) -> Self {
FrameDetector { options }
}
}
impl Detect for FrameDetector {
fn detect(&self, frame: &GrayFrame<'_>, hints: &Hints) -> Vec<Candidate> {
let mut candidates = locate(frame, &self.options);
for c in &mut candidates {
if let Some(fp) = c.fingerprint
&& let Some(known) = hints.find(fp)
{
c.known = Some(known.symbol.clone());
}
}
candidates
}
}