use super::QrDecoder;
use super::matrix::QUIET_ZONE;
use crate::error::{Error, Result};
use crate::geometry::{Location, Point, Quad};
use crate::image::GrayFrame;
use crate::output::BitMatrix;
use crate::pipeline::{Candidate, Hints};
use crate::symbol::Symbol;
use crate::traits::{Analyze, Decode, 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(frame) {
Ok(loc) => vec![Candidate {
location: loc.as_location(),
symbology: Some(crate::symbology::Symbology::QrCode),
fingerprint: None,
known: None,
}],
Err(_) => 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 matrix = sample_grid(frame)?;
QrDecoder::new().decode(&crate::output::Encoding::Matrix(matrix))
}
pub fn sample_grid(frame: &GrayFrame<'_>) -> Result<BitMatrix> {
let located = locate(frame)?;
Ok(located.sample(frame))
}
struct Binary {
bits: Vec<bool>,
width: usize,
height: usize,
threshold: u8,
}
impl Binary {
fn from_frame(frame: &GrayFrame<'_>) -> Binary {
let w = frame.width();
let h = frame.height();
let threshold = otsu_threshold(frame);
let mut bits = vec![false; w * h];
for y in 0..h {
for x in 0..w {
bits[y * w + x] = frame.get_unchecked(x, y) <= threshold;
}
}
Binary {
bits,
width: w,
height: h,
threshold,
}
}
#[inline]
fn dark(&self, x: usize, y: usize) -> bool {
self.bits[y * self.width + x]
}
}
fn otsu_threshold(frame: &GrayFrame<'_>) -> u8 {
let mut hist = [0u64; 256];
for y in 0..frame.height() {
for x in 0..frame.width() {
hist[frame.get_unchecked(x, y) as usize] += 1;
}
}
let total: u64 = (frame.width() * frame.height()) as u64;
let sum: f64 = (0..256).map(|i| i as f64 * hist[i] as f64).sum();
let mut sum_b = 0.0;
let mut w_b = 0u64;
let mut max_var = -1.0;
let mut threshold = 127u8;
for (t, &count) in hist.iter().enumerate() {
w_b += count;
if w_b == 0 {
continue;
}
let w_f = total - w_b;
if w_f == 0 {
break;
}
sum_b += t as f64 * count as f64;
let m_b = sum_b / w_b as f64;
let m_f = (sum - sum_b) / w_f as f64;
let var = w_b as f64 * w_f as f64 * (m_b - m_f) * (m_b - m_f);
if var > max_var {
max_var = var;
threshold = t as u8;
}
}
threshold
}
#[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 / 2.0;
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 find_finders(bin: &Binary) -> Vec<Finder> {
let mut centers: Vec<Finder> = Vec::new();
let w = bin.width;
for y in 0..bin.height {
let mut runs: Vec<(bool, usize, i32)> = Vec::new();
let mut cur = bin.dark(0, y);
let mut start = 0usize;
for x in 1..w {
let d = bin.dark(x, y);
if d != cur {
runs.push((cur, start, (x - start) as i32));
cur = d;
start = x;
}
}
runs.push((cur, start, (w - 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,
];
let Some(module) = found_pattern_cross(counts) else {
continue;
};
let center = &runs[i + 2];
let cx = center.1 + (center.2 as usize) / 2;
let Some(cy) = cross_check_vertical(bin, cx, y) else {
continue;
};
let cy_row = cy.round().clamp(0.0, (bin.height - 1) as f32) as usize;
let Some(cx_ref) = cross_check_horizontal(bin, cy_row, cx) else {
continue;
};
add_center(&mut centers, cx_ref, cy, module);
}
}
centers
}
struct Located {
projection: Projection,
dimension: usize,
threshold: u8,
corners: [Point; 4],
module_size: f32,
}
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 sample(&self, frame: &GrayFrame<'_>) -> BitMatrix {
let dim = self.dimension;
let mut matrix = BitMatrix::new(dim, dim, QUIET_ZONE);
for my in 0..dim {
for mx in 0..dim {
let (px, py) = self.projection.map(mx as f64 + 0.5, my as f64 + 0.5);
if sample_dark(frame, px, py, self.threshold) {
matrix.set(mx, my, true);
}
}
}
matrix
}
}
fn sample_dark(frame: &GrayFrame<'_>, px: f64, py: f64, threshold: u8) -> bool {
let cx = px.round() as i32;
let cy = py.round() as i32;
let mut sum = 0u32;
let mut n = 0u32;
for dy in -1..=1 {
for dx in -1..=1 {
let x = cx + dx;
let y = cy + dy;
if x >= 0
&& y >= 0
&& let Some(v) = frame.get(x as usize, y as usize)
{
sum += v as u32;
n += 1;
}
}
}
if n == 0 {
return false;
}
(sum / n) <= threshold as u32
}
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];
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) -> Option<Point> {
let radius = (module_size * 5.0).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 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 locate(frame: &GrayFrame<'_>) -> Result<Located> {
if frame.width() < 21 || frame.height() < 21 {
return Err(Error::undecodable("frame too small for a QR symbol"));
}
let bin = Binary::from_frame(frame);
let mut centers = find_finders(&bin);
if centers.len() < 3 {
return Err(Error::undecodable(
"fewer than three QR finder patterns found",
));
}
centers.sort_by_key(|f| std::cmp::Reverse(f.count));
let (tl, tr, bl, module_size) = pick_three(¢ers)?;
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).ok_or_else(|| Error::undecodable("bad QR size"))?;
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);
if let Some(align) = find_alignment(&bin, Point::new(ex, ey), module_size) {
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);
Ok(Located {
projection,
dimension,
threshold: bin.threshold,
corners: [tl, tr, br_corner, bl],
module_size,
})
}
fn pick_three(centers: &[Finder]) -> Result<(Point, Point, Point, f32)> {
let strong: Vec<Finder> = centers.iter().filter(|f| f.count >= 2).copied().collect();
let pool: &[Finder] = if strong.len() >= 3 { &strong } else { centers };
if pool.len() < 3 {
return Err(Error::undecodable("insufficient finder patterns"));
}
let (pts, module_size) = order_finders(pool[0], pool[1], pool[2]);
Ok((pts[0], pts[1], pts[2], module_size))
}