use super::matrix::QUIET_ZONE;
use super::tables::alignment_positions;
use super::{QrDecoder, Version};
use crate::error::{Error, Result};
use crate::geometry::{Location, Point, Quad};
use crate::image::GrayFrame;
use crate::imgproc::binary::BinaryImage;
use crate::imgproc::integral::IntegralImage;
use crate::imgproc::sample::sample_bilinear;
use crate::imgproc::threshold::{
adaptive_binarize_bradley, adaptive_binarize_sauvola, otsu_threshold,
};
use crate::imgproc::tps::ThinPlateSpline;
use crate::output::BitMatrix;
use crate::pipeline::{Candidate, Hints};
use crate::symbol::Symbol;
use crate::traits::{Analyze, Detect};
use crate::transform::Projection;
#[derive(Debug, Default, Clone, Copy)]
pub struct QrScanner;
impl QrScanner {
pub fn new() -> Self {
QrScanner
}
}
impl Detect for QrScanner {
fn detect(&self, frame: &GrayFrame<'_>, _hints: &Hints) -> Vec<Candidate> {
match locate_any(frame) {
Some(loc) => vec![Candidate {
location: loc.as_location(),
symbology: Some(crate::symbology::Symbology::QrCode),
fingerprint: None,
known: None,
}],
None => Vec::new(),
}
}
}
impl Analyze for QrScanner {
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> {
let integral = IntegralImage::from_frame(frame);
let decoder = QrDecoder::new();
let mut last = Error::undecodable("no QR symbol found");
let mut pending: Vec<Located> = Vec::new();
for pass in 0.. {
let Some(bin) = binarize(frame, pass) else {
break;
};
let mut dewarps = 0;
let cands = candidates(frame, &bin);
if std::env::var("ANYD_QR_DEBUG").is_ok() {
eprintln!("pass {pass}: {} candidates", cands.len());
for c in &cands {
eprintln!(
" cand dim={} ms={:.2} corners={:?}",
c.dimension,
c.module_size,
c.corners.map(|p| (p.x as i32, p.y as i32))
);
}
}
for located in cands {
let thr = located.threshold(&integral);
let matrix = located.sample(frame, &thr);
match decoder.decode_matrix(&matrix) {
Ok(sym) => return Ok(sym),
Err(e) => {
last = e;
if dewarps < MAX_DEWARP_PER_PASS {
dewarps += 1;
if let Some(sym) = dewarp_decode(frame, &bin, &located, &integral, &decoder)
{
return Ok(sym);
}
}
if located.dimension > 21 && pending.len() < REFINE_HYPOTHESES {
pending.push(located);
}
}
}
}
}
let mut budget = REFINE_MODULE_BUDGET;
for located in &pending {
if let Some(sym) = refine_fourth_corner(frame, located, &integral, &decoder, &mut budget) {
return Ok(sym);
}
if budget == 0 {
break;
}
}
Err(last)
}
const REFINE_HYPOTHESES: usize = 4;
const REFINE_MODULE_BUDGET: usize = 350_000;
const MAX_DEWARP_PER_PASS: usize = 4;
fn refine_fourth_corner(
frame: &GrayFrame<'_>,
located: &Located,
integral: &IntegralImage,
decoder: &QrDecoder,
budget: &mut usize,
) -> Option<Symbol> {
let [tl, tr, _, bl] = located.corners;
let dim = located.dimension;
let d = dim as f64;
let ms = located.module_size.max(1.0);
let src = [
(3.5, 3.5),
(d - 3.5, 3.5),
(d - 6.5, d - 6.5),
(3.5, d - 3.5),
];
let u = (d - 10.0) as f32 / (d - 7.0) as f32;
let ex = tl.x + u * (tr.x - tl.x) + u * (bl.x - tl.x);
let ey = tl.y + u * (tr.y - tl.y) + u * (bl.y - tl.y);
let radius = (ms * 2.0).round().clamp(2.0, 64.0) as usize;
let thresholds = [
ModuleThreshold::Local {
integral,
radius,
bias: 1.0,
},
ModuleThreshold::Global(located.threshold),
];
let reach = ms * 3.0;
let step = (ms * 0.4).clamp(0.75, 3.0);
let n = ((reach / step) as i32).min(8);
let mut offsets: Vec<(i32, i32)> = (-n..=n)
.flat_map(|gy| (-n..=n).map(move |gx| (gx, gy)))
.collect();
offsets.sort_by_key(|&(gx, gy)| gx * gx + gy * gy);
for (gx, gy) in offsets {
let ax = ex + gx as f32 * step;
let ay = ey + gy as f32 * step;
let dst = [
(tl.x as f64, tl.y as f64),
(tr.x as f64, tr.y as f64),
(ax as f64, ay as f64),
(bl.x as f64, bl.y as f64),
];
let projection = Projection::quad_to_quad(src, dst);
let trial = Located {
projection,
dimension: dim,
threshold: located.threshold,
local: located.local,
corners: located.corners,
module_size: located.module_size,
};
for thr in &thresholds {
*budget = budget.saturating_sub(dim * dim);
let matrix = trial.sample(frame, thr);
if let Ok(sym) = decoder.decode_matrix(&matrix) {
return Some(sym);
}
if *budget == 0 {
return None;
}
}
}
None
}
pub fn sample_grid(frame: &GrayFrame<'_>) -> Result<BitMatrix> {
let integral = IntegralImage::from_frame(frame);
let decoder = QrDecoder::new();
let mut first: Option<BitMatrix> = None;
for pass in 0.. {
let Some(bin) = binarize(frame, pass) else {
break;
};
for located in candidates(frame, &bin) {
let thr = located.threshold(&integral);
let matrix = located.sample(frame, &thr);
if decoder.decode_matrix(&matrix).is_ok() {
return Ok(matrix);
}
if first.is_none() {
first = Some(matrix);
}
}
}
first.ok_or_else(|| Error::undecodable("no QR finder patterns found"))
}
fn locate_any(frame: &GrayFrame<'_>) -> Option<Located> {
for pass in 0.. {
let bin = binarize(frame, pass)?;
if let Some(located) = candidates(frame, &bin).into_iter().next() {
return Some(located);
}
}
None
}
struct Binary {
img: BinaryImage,
threshold: u8,
local: bool,
}
impl Binary {
#[inline]
fn dark(&self, x: usize, y: usize) -> bool {
self.img.get(x, y)
}
#[inline]
fn width(&self) -> usize {
self.img.width()
}
#[inline]
fn height(&self) -> usize {
self.img.height()
}
}
fn binarize(frame: &GrayFrame<'_>, pass: usize) -> Option<Binary> {
let threshold = otsu_threshold(frame);
let small = frame.width().min(frame.height());
let r_small = (small / 12).clamp(6, 40);
let r_large = (small / 6).clamp(10, 80);
let img = match pass {
0 => {
let w = frame.width();
let h = frame.height();
let mut bin = BinaryImage::new(w, h);
for y in 0..h {
for x in 0..w {
if frame.get_unchecked(x, y) <= threshold {
bin.set(x, y, true);
}
}
}
return Some(Binary {
img: bin,
threshold,
local: false,
});
}
1 => adaptive_binarize_bradley(frame, r_small, 0.08),
2 => adaptive_binarize_bradley(frame, r_large, 0.08),
3 => adaptive_binarize_sauvola(frame, r_small, 0.2, 128.0),
_ => return None,
};
Some(Binary {
img,
threshold,
local: true,
})
}
#[derive(Debug, Clone, Copy)]
struct Finder {
x: f32,
y: f32,
module_size: f32,
count: u32,
}
fn found_pattern_cross(counts: [i32; 5]) -> Option<f32> {
let total: i32 = counts.iter().sum();
if total < 7 {
return None;
}
let module = total as f32 / 7.0;
let max_var = module * 0.7;
let ok = (counts[0] as f32 - module).abs() < max_var
&& (counts[1] as f32 - module).abs() < max_var
&& (counts[2] as f32 - 3.0 * module).abs() < 3.0 * max_var
&& (counts[3] as f32 - module).abs() < max_var
&& (counts[4] as f32 - module).abs() < max_var;
ok.then_some(module)
}
fn walk_run(len: i32, start: i32, sample: impl Fn(i32) -> bool) -> Option<([i32; 5], i32)> {
let mut counts = [0i32; 5];
let mut i = start;
while i >= 0 && sample(i) {
counts[2] += 1;
i -= 1;
}
if i < 0 {
return None;
}
while i >= 0 && !sample(i) {
counts[1] += 1;
i -= 1;
}
if i < 0 || counts[1] == 0 {
return None;
}
while i >= 0 && sample(i) {
counts[0] += 1;
i -= 1;
}
if counts[0] == 0 {
return None;
}
let mut j = start + 1;
while j < len && sample(j) {
counts[2] += 1;
j += 1;
}
if j >= len {
return None;
}
while j < len && !sample(j) {
counts[3] += 1;
j += 1;
}
if j >= len || counts[3] == 0 {
return None;
}
while j < len && sample(j) {
counts[4] += 1;
j += 1;
}
if counts[4] == 0 {
return None;
}
Some((counts, j))
}
fn run_center(counts: [i32; 5], end: i32) -> f32 {
end as f32 - counts[4] as f32 - counts[3] as f32 - counts[2] as f32 / 2.0
}
fn cross_check_vertical(bin: &Binary, cx: usize, start: usize) -> Option<f32> {
let (counts, end) = walk_run(bin.height() as i32, start as i32, |k| {
bin.dark(cx, k as usize)
})?;
found_pattern_cross(counts)?;
Some(run_center(counts, end))
}
fn cross_check_horizontal(bin: &Binary, cy: usize, start: usize) -> Option<f32> {
let (counts, end) = walk_run(bin.width() as i32, start as i32, |k| {
bin.dark(k as usize, cy)
})?;
found_pattern_cross(counts)?;
Some(run_center(counts, end))
}
fn add_center(centers: &mut Vec<Finder>, x: f32, y: f32, module_size: f32) {
for f in centers.iter_mut() {
if (f.x - x).abs() <= f.module_size && (f.y - y).abs() <= f.module_size {
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_size = (f.module_size * c + module_size) / (c + 1.0);
f.count += 1;
return;
}
}
centers.push(Finder {
x,
y,
module_size,
count: 1,
});
}
fn scan_line_runs(len: usize, dark: impl Fn(usize) -> bool, mut emit: impl FnMut(usize, [i32; 5])) {
let mut runs: Vec<(bool, usize, i32)> = Vec::new();
let mut cur = dark(0);
let mut start = 0usize;
for p in 1..len {
let d = dark(p);
if d != cur {
runs.push((cur, start, (p - start) as i32));
cur = d;
start = p;
}
}
runs.push((cur, start, (len - start) as i32));
if runs.len() < 5 {
return;
}
for i in 0..=runs.len() - 5 {
if !runs[i].0 {
continue; }
let counts = [
runs[i].2,
runs[i + 1].2,
runs[i + 2].2,
runs[i + 3].2,
runs[i + 4].2,
];
let center = &runs[i + 2];
emit(center.1 + (center.2 as usize) / 2, counts);
}
}
fn find_finders(bin: &Binary) -> Vec<Finder> {
let mut centers: Vec<Finder> = Vec::new();
let (w, h) = (bin.width(), bin.height());
for y in 0..h {
scan_line_runs(
w,
|x| bin.dark(x, y),
|mid, counts| {
if found_pattern_cross(counts).is_none() {
return;
}
let Some(cy) = cross_check_vertical(bin, mid, y) else {
return;
};
let cy_row = cy.round().clamp(0.0, (h - 1) as f32) as usize;
let Some(cx) = cross_check_horizontal(bin, cy_row, mid) else {
return;
};
let module = found_pattern_cross(counts).unwrap();
add_center(&mut centers, cx, cy, module);
},
);
}
for x in 0..w {
scan_line_runs(
h,
|y| bin.dark(x, y),
|mid, counts| {
if found_pattern_cross(counts).is_none() {
return;
}
let Some(cx) = cross_check_horizontal(bin, mid, x) else {
return;
};
let cx_col = cx.round().clamp(0.0, (w - 1) as f32) as usize;
let Some(cy) = cross_check_vertical(bin, cx_col, mid) else {
return;
};
let module = found_pattern_cross(counts).unwrap();
add_center(&mut centers, cx, cy, module);
},
);
}
centers
}
struct Located {
projection: Projection,
dimension: usize,
threshold: u8,
local: bool,
corners: [Point; 4],
module_size: f32,
}
enum ModuleThreshold<'a> {
Global(u8),
Local {
integral: &'a IntegralImage,
radius: usize,
bias: f64,
},
}
impl Located {
fn as_location(&self) -> Location {
let rotation = {
let dx = self.corners[1].x - self.corners[0].x;
let dy = self.corners[1].y - self.corners[0].y;
dy.atan2(dx)
};
Location {
outline: Quad::new(self.corners),
rotation: Some(rotation),
module_size: Some(self.module_size),
}
}
fn threshold<'a>(&self, integral: &'a IntegralImage) -> ModuleThreshold<'a> {
if self.local {
let radius = (self.module_size * 2.0).round().clamp(2.0, 64.0) as usize;
ModuleThreshold::Local {
integral,
radius,
bias: 1.0,
}
} else {
ModuleThreshold::Global(self.threshold)
}
}
fn sample(&self, frame: &GrayFrame<'_>, thr: &ModuleThreshold<'_>) -> BitMatrix {
self.sample_map(frame, thr, |x, y| self.projection.map(x, y))
}
fn sample_warp(
&self,
frame: &GrayFrame<'_>,
thr: &ModuleThreshold<'_>,
warp: &ThinPlateSpline,
) -> BitMatrix {
self.sample_map(frame, thr, |x, y| warp.map(x, y))
}
fn sample_map(
&self,
frame: &GrayFrame<'_>,
thr: &ModuleThreshold<'_>,
map: impl Fn(f64, f64) -> (f64, f64),
) -> BitMatrix {
let dim = self.dimension;
let tap = (self.module_size * 0.25).clamp(0.5, 8.0) as f64;
const OFF: [f64; 4] = [-0.3, -0.1, 0.1, 0.3];
let win = (self.module_size * 2.0).round().clamp(2.0, 48.0) as i32;
let mut matrix = BitMatrix::new(dim, dim, QUIET_ZONE);
for my in 0..dim {
for mx in 0..dim {
let gx = mx as f64 + 0.5;
let gy = my as f64 + 0.5;
let dark = match *thr {
ModuleThreshold::Global(t) => {
let (px, py) = map(gx, gy);
sample_dark_global(frame, px, py, tap, t)
}
ModuleThreshold::Local {
integral,
radius,
bias,
} => {
let (cx, cy) = map(gx, gy);
let midpoint = local_contrast_midpoint(frame, cx, cy, win) * bias;
let backstop = (bias - 1.0).abs() < 1e-9;
let mut votes_dark = 0u32;
for &dv in &OFF {
for &du in &OFF {
let (px, py) = map(gx + du, gy + dv);
if sample_dark_local(
frame, px, py, integral, radius, midpoint, backstop,
) {
votes_dark += 1;
}
}
}
votes_dark * 2 > (OFF.len() * OFF.len()) as u32
}
};
if dark {
matrix.set(mx, my, true);
}
}
}
matrix
}
}
fn local_contrast_midpoint(frame: &GrayFrame<'_>, cx: f64, cy: f64, win: i32) -> f64 {
let w = frame.width() as i32;
let h = frame.height() as i32;
let ix = cx.round() as i32;
let iy = cy.round() as i32;
let mut mn = 255.0f64;
let mut mx = 0.0f64;
let mut any = false;
let mut dy = -win;
while dy <= win {
let mut dx = -win;
while dx <= win {
let x = ix + dx;
let y = iy + dy;
if x >= 0 && y >= 0 && x < w && y < h {
let l = f64::from(frame.get_unchecked(x as usize, y as usize));
mn = mn.min(l);
mx = mx.max(l);
any = true;
}
dx += 1;
}
dy += 1;
}
if any { (mn + mx) * 0.5 } else { 128.0 }
}
fn sample_dark_global(frame: &GrayFrame<'_>, px: f64, py: f64, tap: f64, t: u8) -> bool {
let lum = (sample_bilinear(frame, px, py)
+ sample_bilinear(frame, px - tap, py)
+ sample_bilinear(frame, px + tap, py)
+ sample_bilinear(frame, px, py - tap)
+ sample_bilinear(frame, px, py + tap))
/ 5.0;
lum <= f64::from(t)
}
fn sample_dark_local(
frame: &GrayFrame<'_>,
px: f64,
py: f64,
integral: &IntegralImage,
radius: usize,
midpoint: f64,
backstop: bool,
) -> bool {
let lum = sample_bilinear(frame, px, py);
if !backstop {
return lum < midpoint;
}
let cx = (px.round().max(0.0) as usize).min(integral.width().saturating_sub(1));
let cy = (py.round().max(0.0) as usize).min(integral.height().saturating_sub(1));
let (sum, count) = integral.window_sum_count(cx, cy, radius);
let mean = sum as f64 / count.max(1) as f64;
lum < midpoint && lum < mean - 4.0 && lum < mean * 0.98
}
fn order_finders(a: Finder, b: Finder, c: Finder) -> ([Point; 3], f32) {
let pa = Point::new(a.x, a.y);
let pb = Point::new(b.x, b.y);
let pc = Point::new(c.x, c.y);
let d_ab = pa.distance(pb);
let d_bc = pb.distance(pc);
let d_ac = pa.distance(pc);
let (corner, mut p, mut q) = if d_bc >= d_ab && d_bc >= d_ac {
(pa, pb, pc)
} else if d_ac >= d_ab && d_ac >= d_bc {
(pb, pa, pc)
} else {
(pc, pa, pb)
};
let cross = (q.x - corner.x) * (p.y - corner.y) - (q.y - corner.y) * (p.x - corner.x);
if cross < 0.0 {
std::mem::swap(&mut p, &mut q);
}
let module_size = (a.module_size + b.module_size + c.module_size) / 3.0;
([corner, q, p], module_size)
}
type Quad4 = [(f64, f64); 4];
type AnchorMesh = (Vec<(f64, f64)>, Vec<(f64, f64)>);
fn parallelogram(tl: Point, tr: Point, bl: Point, d: f64) -> (Quad4, Quad4) {
let br = Point::new(tr.x + bl.x - tl.x, tr.y + bl.y - tl.y);
let src = [
(3.5, 3.5),
(d - 3.5, 3.5),
(d - 3.5, d - 3.5),
(3.5, d - 3.5),
];
let dst = [
(tl.x as f64, tl.y as f64),
(tr.x as f64, tr.y as f64),
(br.x as f64, br.y as f64),
(bl.x as f64, bl.y as f64),
];
(src, dst)
}
fn found_alignment_cross(counts: [i32; 5], module: f32) -> Option<f32> {
let inner = counts[1] + counts[2] + counts[3];
let m = inner as f32 / 3.0;
if (m - module).abs() > module * 0.5 {
return None;
}
let max_var = m * 0.5;
let inner_ok = (counts[1] as f32 - m).abs() < max_var
&& (counts[2] as f32 - m).abs() < max_var
&& (counts[3] as f32 - m).abs() < max_var;
let outer_ok = counts[0] > 0 && counts[4] > 0;
(inner_ok && outer_ok).then_some(m)
}
fn find_alignment(
bin: &Binary,
expected: Point,
module_size: f32,
radius_modules: f32,
) -> Option<Point> {
let radius = (module_size * radius_modules).ceil() as i32;
let x0 = (expected.x as i32 - radius).max(0) as usize;
let x1 = ((expected.x as i32 + radius) as usize).min(bin.width() - 1);
let y0 = (expected.y as i32 - radius).max(0) as usize;
let y1 = ((expected.y as i32 + radius) as usize).min(bin.height() - 1);
if x1 <= x0 + 4 {
return None;
}
let mut best: Option<(Point, f32)> = None;
for y in y0..=y1 {
let mut runs: Vec<(bool, usize, i32)> = Vec::new();
let mut cur = bin.dark(x0, y);
let mut start = x0;
for x in (x0 + 1)..=x1 {
let dk = bin.dark(x, y);
if dk != cur {
runs.push((cur, start, (x - start) as i32));
cur = dk;
start = x;
}
}
runs.push((cur, start, (x1 + 1 - start) as i32));
if runs.len() < 5 {
continue;
}
for i in 0..=runs.len() - 5 {
if !runs[i].0 {
continue;
}
let counts = [
runs[i].2,
runs[i + 1].2,
runs[i + 2].2,
runs[i + 3].2,
runs[i + 4].2,
];
if found_alignment_cross(counts, module_size).is_none() {
continue;
}
let center = &runs[i + 2];
let cxi = center.1 + (center.2 as usize) / 2;
let Some((vc, vend)) =
walk_run(bin.height() as i32, y as i32, |k| bin.dark(cxi, k as usize))
else {
continue;
};
if found_alignment_cross(vc, module_size).is_none() {
continue;
}
let cx = cxi as f32;
let cy = run_center(vc, vend);
let dist = (cx - expected.x).abs() + (cy - expected.y).abs();
if best.is_none_or(|(_, d)| dist < d) {
best = Some((Point::new(cx, cy), dist));
}
}
}
best.map(|(p, _)| p)
}
fn alignment_module_dark(i: usize, j: usize) -> bool {
let ring = i == 0 || i == 4 || j == 0 || j == 4;
let center = i == 2 && j == 2;
ring || center
}
fn alignment_template_score(
frame: &GrayFrame<'_>,
cx: f32,
cy: f32,
ax: (f32, f32),
ay: (f32, f32),
) -> f32 {
let (mut light, mut dark) = (0.0f32, 0.0f32);
let (mut nl, mut nd) = (0.0f32, 0.0f32);
for j in 0..5usize {
for i in 0..5usize {
let fi = i as f32 - 2.0;
let fj = j as f32 - 2.0;
let px = cx + fi * ax.0 + fj * ay.0;
let py = cy + fi * ax.1 + fj * ay.1;
let lum = sample_bilinear(frame, px as f64, py as f64) as f32;
if alignment_module_dark(i, j) {
dark += lum;
nd += 1.0;
} else {
light += lum;
nl += 1.0;
}
}
}
light / nl.max(1.0) - dark / nd.max(1.0)
}
fn search_alignment_template(
frame: &GrayFrame<'_>,
cx0: f32,
cy0: f32,
ax: (f32, f32),
ay: (f32, f32),
reach_modules: f32,
) -> Option<Point> {
if ax.0.hypot(ax.1) < 0.5 || ay.0.hypot(ay.1) < 0.5 {
return None;
}
let step = 0.2f32;
let n = (reach_modules / step).round() as i32;
let mut best: Option<(f32, f32, f32)> = None; for oy in -n..=n {
for ox in -n..=n {
let fx = ox as f32 * step;
let fy = oy as f32 * step;
let cx = cx0 + fx * ax.0 + fy * ay.0;
let cy = cy0 + fx * ax.1 + fy * ay.1;
let s = alignment_template_score(frame, cx, cy, ax, ay);
if best.is_none_or(|(bs, _, _)| s > bs) {
best = Some((s, cx, cy));
}
}
}
best.and_then(|(s, x, y)| (s > 6.0).then_some(Point::new(x, y)))
}
fn find_alignment_template(
frame: &GrayFrame<'_>,
projection: &Projection,
gx: f64,
gy: f64,
reach_modules: f32,
) -> Option<Point> {
let (cx0, cy0) = projection.map(gx, gy);
let (rx, ry) = projection.map(gx + 1.0, gy);
let (dx, dy) = projection.map(gx, gy + 1.0);
let ax = ((rx - cx0) as f32, (ry - cy0) as f32);
let ay = ((dx - cx0) as f32, (dy - cy0) as f32);
search_alignment_template(frame, cx0 as f32, cy0 as f32, ax, ay, reach_modules)
}
struct Anchors {
grid: Vec<(f64, f64)>,
img: Vec<(f64, f64)>,
}
fn refine_alignment_centroid(
frame: &GrayFrame<'_>,
cx: f32,
cy: f32,
module_size: f32,
) -> (f32, f32) {
let r = (module_size * 2.0).round().clamp(2.0, 32.0) as i32;
let r2 = (r * r) as f32;
let (mut sw, mut sx, mut sy) = (0.0f32, 0.0f32, 0.0f32);
for dy in -r..=r {
for dx in -r..=r {
if (dx * dx + dy * dy) as f32 > r2 {
continue;
}
let x = cx + dx as f32;
let y = cy + dy as f32;
let lum = sample_bilinear(frame, x as f64, y as f64) as f32;
let w = (255.0 - lum).max(0.0);
sw += w;
sx += w * x;
sy += w * y;
}
}
if sw > 1e-3 {
(sx / sw, sy / sw)
} else {
(cx, cy)
}
}
fn collect_anchors(frame: &GrayFrame<'_>, bin: &Binary, located: &Located) -> Option<Anchors> {
let dim = located.dimension;
let d = dim as f64;
let version = Version::new(((dim - 17) / 4) as u8)?;
let positions = alignment_positions(version);
let n = positions.len();
if n == 0 {
return None;
}
let [tl, tr, _, bl] = located.corners;
let mut grid = vec![(3.5, 3.5), (d - 3.5, 3.5), (3.5, d - 3.5)];
let mut img = vec![
(tl.x as f64, tl.y as f64),
(tr.x as f64, tr.y as f64),
(bl.x as f64, bl.y as f64),
];
let mut remaining: Vec<(f64, f64)> = Vec::new();
for ri in 0..n {
for ci in 0..n {
let finder_corner = (ri == 0 && (ci == 0 || ci == n - 1)) || (ri == n - 1 && ci == 0);
if finder_corner {
continue;
}
remaining.push((
f64::from(positions[ci]) + 0.5,
f64::from(positions[ri]) + 0.5,
));
}
}
let mut alignments = 0usize;
for round in 0.. {
if remaining.is_empty() || round >= 6 {
break;
}
let predictor = (alignments > 0)
.then(|| ThinPlateSpline::fit(&grid, &img))
.flatten();
let radius = if round == 0 { 3.5 } else { 5.5 };
let mut still = Vec::new();
for &(gx, gy) in &remaining {
let (ex, ey) = match &predictor {
Some(t) => t.map(gx, gy),
None => located.projection.map(gx, gy),
};
let hit = find_alignment(
bin,
Point::new(ex as f32, ey as f32),
located.module_size,
radius,
)
.or_else(|| {
let predictor_proj = &located.projection;
match &predictor {
Some(t) => {
let (rx, ry) = t.map(gx + 1.0, gy);
let (dx, dy) = t.map(gx, gy + 1.0);
let ax = ((rx - ex) as f32, (ry - ey) as f32);
let ay = ((dx - ex) as f32, (dy - ey) as f32);
search_alignment_template(frame, ex as f32, ey as f32, ax, ay, radius)
}
None => find_alignment_template(frame, predictor_proj, gx, gy, radius),
}
});
if let Some(p) = hit {
let (rx, ry) = refine_alignment_centroid(frame, p.x, p.y, located.module_size);
grid.push((gx, gy));
img.push((f64::from(rx), f64::from(ry)));
alignments += 1;
} else {
still.push((gx, gy));
}
}
let progressed = still.len() < remaining.len();
remaining = still;
if !progressed {
break;
}
}
(alignments >= 1).then_some(Anchors { grid, img })
}
fn timing_anchors(bin: &Binary, located: &Located) -> AnchorMesh {
let dim = located.dimension;
let mut grid = Vec::new();
let mut img = Vec::new();
collect_timing_line(bin, located, dim, true, &mut grid, &mut img);
collect_timing_line(bin, located, dim, false, &mut grid, &mut img);
(grid, img)
}
fn collect_timing_line(
bin: &Binary,
located: &Located,
dim: usize,
horizontal: bool,
grid: &mut Vec<(f64, f64)>,
img: &mut Vec<(f64, f64)>,
) {
let expected = dim as i32 - 13;
if expected < 3 {
return;
}
let lo = 6.5f64;
let hi = dim as f64 - 6.5;
let mut best: Option<(f64, Vec<f64>)> = None;
let mut best_var = f64::MAX;
let mut perp = 6.5 - 1.5;
while perp <= 6.5 + 1.5 + 1e-9 {
if let Some(tr) = walk_timing(bin, located, horizontal, perp, lo, hi, expected) {
let mean = (hi - lo) / tr.len() as f64;
let var: f64 = tr
.windows(2)
.map(|w| {
let g = w[1] - w[0] - mean;
g * g
})
.sum();
if var < best_var {
best_var = var;
best = Some((perp, tr));
}
}
perp += 0.1;
}
let Some((perp, transitions)) = best else {
return;
};
let map_pt = |scan: f64| -> (f64, f64) {
if horizontal {
located.projection.map(scan, perp)
} else {
located.projection.map(perp, scan)
}
};
for (i, &s_t) in transitions.iter().enumerate() {
let b = (7 + i) as f64;
let (px, py) = map_pt(s_t);
if horizontal {
grid.push((b, 6.5));
} else {
grid.push((6.5, b));
}
img.push((px, py));
}
}
fn walk_timing(
bin: &Binary,
located: &Located,
horizontal: bool,
perp: f64,
lo: f64,
hi: f64,
expected: i32,
) -> Option<Vec<f64>> {
let dark_at = |scan: f64| -> bool {
let (px, py) = if horizontal {
located.projection.map(scan, perp)
} else {
located.projection.map(perp, scan)
};
let xi = px.round();
let yi = py.round();
if xi < 0.0 || yi < 0.0 || xi >= bin.width() as f64 || yi >= bin.height() as f64 {
return false;
}
bin.dark(xi as usize, yi as usize)
};
let step = 0.02f64;
let mut transitions: Vec<f64> = Vec::new();
let mut prev = dark_at(lo);
let mut prev_s = lo;
let mut s = lo + step;
while s <= hi {
let cur = dark_at(s);
if cur != prev {
transitions.push((s + prev_s) * 0.5);
prev = cur;
}
prev_s = s;
s += step;
}
if transitions.len() as i32 != expected {
return None;
}
if transitions
.windows(2)
.any(|w| !(0.5..1.6).contains(&(w[1] - w[0])))
{
return None;
}
Some(transitions)
}
fn dewarp_decode(
frame: &GrayFrame<'_>,
bin: &Binary,
located: &Located,
integral: &IntegralImage,
decoder: &QrDecoder,
) -> Option<Symbol> {
let base = collect_anchors(frame, bin, located);
let (tg, ti) = timing_anchors(bin, located);
if std::env::var("ANYD_QR_DEBUG").is_ok() {
eprintln!(
"dewarp: dim={} ms={:.2} anchors={:?} timing={}",
located.dimension,
located.module_size,
base.as_ref().map(|a| a.grid.len()),
tg.len(),
);
}
let mut meshes: Vec<AnchorMesh> = Vec::new();
match &base {
Some(a) => {
if !tg.is_empty() {
let mut g = a.grid.clone();
g.extend_from_slice(&tg);
let mut im = a.img.clone();
im.extend_from_slice(&ti);
meshes.push((g, im));
}
meshes.push((a.grid.clone(), a.img.clone()));
}
None => {
if !tg.is_empty() {
let [tl, tr, _, bl] = located.corners;
let d = located.dimension as f64;
let mut g = vec![(3.5, 3.5), (d - 3.5, 3.5), (3.5, d - 3.5)];
g.extend_from_slice(&tg);
let mut im = vec![
(tl.x as f64, tl.y as f64),
(tr.x as f64, tr.y as f64),
(bl.x as f64, bl.y as f64),
];
im.extend_from_slice(&ti);
meshes.push((g, im));
}
}
}
let radius = (located.module_size * 2.0).round().clamp(2.0, 64.0) as usize;
let thresholds = [
ModuleThreshold::Local {
integral,
radius,
bias: 1.0,
},
ModuleThreshold::Global(located.threshold),
ModuleThreshold::Local {
integral,
radius,
bias: 0.85,
},
ModuleThreshold::Local {
integral,
radius,
bias: 1.15,
},
];
for (g, im) in &meshes {
let Some(warp) = ThinPlateSpline::fit(g, im) else {
continue;
};
for thr in &thresholds {
let matrix = located.sample_warp(frame, thr, &warp);
if let Ok(sym) = decoder.decode_matrix(&matrix) {
return Some(sym);
}
}
}
None
}
fn directional_module_size(bin: &Binary, from: Point, to: Point) -> Option<f32> {
let dx = to.x - from.x;
let dy = to.y - from.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 1.0 {
return None;
}
let (ux, uy) = (dx / len, dy / len);
let step = 0.25;
let mut dist = 0.0f32;
let mut prev = true; let mut transitions = 0;
while dist <= len {
dist += step;
let px = from.x + ux * dist;
let py = from.y + uy * dist;
let (ix, iy) = (px.round() as i32, py.round() as i32);
if ix < 0 || iy < 0 || ix >= bin.width() as i32 || iy >= bin.height() as i32 {
return None;
}
let cur = bin.dark(ix as usize, iy as usize);
if cur != prev {
transitions += 1;
prev = cur;
if transitions == 3 {
return Some(dist / 3.5);
}
}
}
None
}
fn snap_dimension(estimate: f32) -> Option<usize> {
let version = (((estimate - 17.0) / 4.0).round() as i32).clamp(1, 40);
let dim = 17 + 4 * version as usize;
(dim >= 21).then_some(dim)
}
fn finder_module_dark(i: usize, j: usize) -> bool {
let ring = i == 0 || i == 6 || j == 0 || j == 6;
let center = (2..=4).contains(&i) && (2..=4).contains(&j);
ring || center
}
fn finder_template_score(
frame: &GrayFrame<'_>,
cx: f32,
cy: f32,
ax: (f32, f32),
ay: (f32, f32),
) -> f32 {
let (mut light, mut dark) = (0.0f32, 0.0f32);
let (mut nl, mut nd) = (0.0f32, 0.0f32);
for j in 0..7usize {
for i in 0..7usize {
let fi = i as f32 - 3.0;
let fj = j as f32 - 3.0;
let px = cx + fi * ax.0 + fj * ay.0;
let py = cy + fi * ax.1 + fj * ay.1;
let lum = sample_bilinear(frame, px as f64, py as f64) as f32;
if finder_module_dark(i, j) {
dark += lum;
nd += 1.0;
} else {
light += lum;
nl += 1.0;
}
}
}
light / nl.max(1.0) - dark / nd.max(1.0)
}
fn refine_finder(frame: &GrayFrame<'_>, vertex: Finder, arm: Finder, guess: Finder) -> Finder {
let ms = vertex.module_size.max(1.0);
let unit = |dx: f32, dy: f32| {
let l = (dx * dx + dy * dy).sqrt().max(1e-3);
(dx / l, dy / l)
};
let (uyx, uyy) = unit(arm.x - vertex.x, arm.y - vertex.y);
let (uxx, uxy) = unit(guess.x - vertex.x, guess.y - vertex.y);
let ax = (uxx * ms, uxy * ms);
let ay = (uyx * ms, uyy * ms);
let reach = 2.0 * ms;
let step = 0.5f32;
let steps = (reach / step) as i32;
let mut best = guess;
let mut best_score = f32::MIN;
for oy in -steps..=steps {
for ox in -steps..=steps {
let cx = guess.x + ox as f32 * step;
let cy = guess.y + oy as f32 * step;
let s = finder_template_score(frame, cx, cy, ax, ay);
if s > best_score {
best_score = s;
best = Finder {
x: cx,
y: cy,
module_size: ms,
count: guess.count,
};
}
}
}
best
}
const MAX_FINDERS: usize = 8;
const MAX_TRIPLES: usize = 24;
const SYNTH_MIN_COUNT: u32 = 3;
const SYNTH_PENALTY: f32 = 10.0;
fn candidates(frame: &GrayFrame<'_>, bin: &Binary) -> Vec<Located> {
if bin.width() < 21 || bin.height() < 21 {
return Vec::new();
}
let mut centers = find_finders(bin);
centers.sort_by_key(|f| std::cmp::Reverse(f.count));
centers.truncate(MAX_FINDERS);
let mut triples: Vec<(f32, [Finder; 3])> = Vec::new();
let n = centers.len();
for i in 0..n {
for j in (i + 1)..n {
for k in (j + 1)..n {
if let Some(score) = triple_score(centers[i], centers[j], centers[k]) {
triples.push((score, [centers[i], centers[j], centers[k]]));
}
}
}
}
let strong: Vec<Finder> = centers
.iter()
.filter(|f| f.count >= SYNTH_MIN_COUNT)
.take(5)
.copied()
.collect();
for a in &strong {
for b in &strong {
if std::ptr::eq(a, b) {
continue;
}
for &sign in &[1.0f32, -1.0] {
let (dx, dy) = (b.x - a.x, b.y - a.y);
let c = Finder {
x: a.x - sign * dy,
y: a.y + sign * dx,
module_size: a.module_size,
count: 1,
};
if centers.iter().any(|f| {
(f.x - c.x).abs() <= f.module_size && (f.y - c.y).abs() <= f.module_size
}) {
continue;
}
let c = refine_finder(frame, *a, *b, c);
if let Some(score) = triple_score(*a, *b, c) {
triples.push((score + SYNTH_PENALTY, [*a, *b, c]));
}
}
}
}
triples.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut out = Vec::new();
for (_, tri) in triples.into_iter().take(MAX_TRIPLES) {
if let Some(located) = build_located(frame, bin, tri[0], tri[1], tri[2]) {
out.push(located);
}
}
out
}
fn triple_score(a: Finder, b: Finder, c: Finder) -> Option<f32> {
let ([tl, tr, bl], ms) = order_finders(a, b, c);
if ms <= 0.0 {
return None;
}
let (v1x, v1y) = (tr.x - tl.x, tr.y - tl.y);
let (v2x, v2y) = (bl.x - tl.x, bl.y - tl.y);
let l1 = (v1x * v1x + v1y * v1y).sqrt();
let l2 = (v2x * v2x + v2y * v2y).sqrt();
if l1 < ms * 6.0 || l2 < ms * 6.0 {
return None;
}
let ratio = (l1 / l2).max(l2 / l1);
if ratio > 2.0 {
return None;
}
let cos = (v1x * v2x + v1y * v2y) / (l1 * l2);
if cos.abs() > 0.45 {
return None;
}
let (mn, mx) = [a.module_size, b.module_size, c.module_size]
.iter()
.fold((f32::MAX, 0.0f32), |(mn, mx), &m| (mn.min(m), mx.max(m)));
if mn <= 0.0 || mx / mn > 3.0 {
return None;
}
let count = (a.count + b.count + c.count) as f32;
Some(cos.abs() * 2.0 + ratio.ln() - 0.02 * count)
}
fn refine_finder_centroid(frame: &GrayFrame<'_>, p: Point, module_size: f32) -> Point {
let (x, y) = refine_alignment_centroid(frame, p.x, p.y, module_size * 0.75);
Point::new(x, y)
}
fn build_located(
frame: &GrayFrame<'_>,
bin: &Binary,
a: Finder,
b: Finder,
c: Finder,
) -> Option<Located> {
let ([tl, tr, bl], module_size) = order_finders(a, b, c);
let tl = refine_finder_centroid(frame, tl, module_size);
let tr = refine_finder_centroid(frame, tr, module_size);
let bl = refine_finder_centroid(frame, bl, module_size);
let ms_h = directional_module_size(bin, tl, tr).unwrap_or(module_size);
let ms_v = directional_module_size(bin, tl, bl).unwrap_or(module_size);
let dist_tr = tl.distance(tr);
let dist_bl = tl.distance(bl);
let est_h = dist_tr / ms_h + 7.0;
let est_v = dist_bl / ms_v + 7.0;
let module_size = (ms_h + ms_v) / 2.0;
let dimension = snap_dimension((est_h + est_v) / 2.0)?;
let d = dimension as f64;
let (src, dst) = if dimension > 21 {
let u = (d - 10.0) / (d - 7.0);
let ex = tl.x + u as f32 * (tr.x - tl.x) + u as f32 * (bl.x - tl.x);
let ey = tl.y + u as f32 * (tr.y - tl.y) + u as f32 * (bl.y - tl.y);
let align = find_alignment(bin, Point::new(ex, ey), module_size, 5.0).or_else(|| {
let inv = (d - 7.0) as f32;
let ax = ((tr.x - tl.x) / inv, (tr.y - tl.y) / inv);
let ay = ((bl.x - tl.x) / inv, (bl.y - tl.y) / inv);
search_alignment_template(frame, ex, ey, ax, ay, 3.0)
});
if let Some(align) = align {
let src = [
(3.5, 3.5),
(d - 3.5, 3.5),
(d - 6.5, d - 6.5),
(3.5, d - 3.5),
];
let dst = [
(tl.x as f64, tl.y as f64),
(tr.x as f64, tr.y as f64),
(align.x as f64, align.y as f64),
(bl.x as f64, bl.y as f64),
];
(src, dst)
} else {
parallelogram(tl, tr, bl, d)
}
} else {
parallelogram(tl, tr, bl, d)
};
let projection = Projection::quad_to_quad(src, dst);
let br_corner = Point::new(tr.x + bl.x - tl.x, tr.y + bl.y - tl.y);
Some(Located {
projection,
dimension,
threshold: bin.threshold,
local: bin.local,
corners: [tl, tr, br_corner, bl],
module_size,
})
}