use crate::geometry::{Location, Point, Quad};
use crate::image::GrayFrame;
use crate::output::{Encoding, LinearPattern};
use crate::symbol::Symbol;
use crate::traits::Decode;
const MAX_ANGLE_DEG: f32 = 6.0;
const MIN_AMPLITUDE: f32 = 30.0;
#[derive(Debug, Clone)]
pub struct ScanOptions {
pub scan_count: usize,
pub angles_deg: Vec<f32>,
pub smooth_radius: usize,
pub min_runs: usize,
pub max_candidates: usize,
}
impl Default for ScanOptions {
fn default() -> Self {
ScanOptions {
scan_count: 16,
angles_deg: vec![0.0, -3.0, 3.0, -MAX_ANGLE_DEG, MAX_ANGLE_DEG],
smooth_radius: 0,
min_runs: 3,
max_candidates: 8,
}
}
}
#[derive(Debug, Clone)]
pub struct LinearCandidate {
pub pattern: LinearPattern,
pub edges: Vec<f32>,
pub lead_px: f32,
pub trail_px: f32,
pub location: Location,
pub confidence: f32,
}
pub fn scan_lines(frame: &GrayFrame<'_>, opts: &ScanOptions) -> Vec<LinearCandidate> {
let mut found = scan_all(frame, opts);
dedupe(&mut found, opts.max_candidates);
found
}
const FINE_PROMINENCE: &[f32] = &[0.06, 0.11];
const MAX_FINE_EXTREMA: usize = 1024;
pub fn scan_edges(frame: &GrayFrame<'_>, opts: &ScanOptions) -> Vec<LinearCandidate> {
let w = frame.width();
let h = frame.height();
if w < 4 || h == 0 {
return Vec::new();
}
let mut found: Vec<LinearCandidate> = Vec::new();
let count = opts.scan_count.max(1);
for i in 0..count {
let frac = if count == 1 {
0.5
} else {
0.1 + 0.8 * (i as f32) / ((count - 1) as f32)
};
let cy = frac * (h.saturating_sub(1)) as f32;
for ° in &opts.angles_deg {
let tan = (deg.to_radians()).tan();
let profile = sample_profile(frame, cy, tan, opts.smooth_radius);
for &prom in FINE_PROMINENCE {
if let Some(cand) =
analyze_profile_fine(&profile, cy, tan, deg, w, opts.min_runs, prom)
{
found.push(cand);
}
}
}
}
found.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(core::cmp::Ordering::Equal)
});
found
}
fn scan_all(frame: &GrayFrame<'_>, opts: &ScanOptions) -> Vec<LinearCandidate> {
let w = frame.width();
let h = frame.height();
if w < 4 || h == 0 {
return Vec::new();
}
let mut found: Vec<LinearCandidate> = Vec::new();
let count = opts.scan_count.max(1);
for i in 0..count {
let frac = if count == 1 {
0.5
} else {
0.1 + 0.8 * (i as f32) / ((count - 1) as f32)
};
let cy = frac * (h.saturating_sub(1)) as f32;
for ° in &opts.angles_deg {
let tan = (deg.to_radians()).tan();
let profile = sample_profile(frame, cy, tan, opts.smooth_radius);
if let Some(cand) = analyze_profile(&profile, cy, tan, deg, w, opts.min_runs) {
found.push(cand);
}
}
}
found
}
pub fn try_decode(candidate: &LinearCandidate, decoder: &dyn Decode) -> Option<Symbol> {
let encoding = Encoding::Linear(candidate.pattern.clone());
let mut symbol = decoder.decode(&encoding).ok()?;
if symbol.location.is_none() {
symbol.location = Some(candidate.location.clone());
}
Some(symbol)
}
fn sample_column(frame: &GrayFrame<'_>, x: usize, y: f32) -> f32 {
let h = frame.height();
let yc = y.clamp(0.0, (h - 1) as f32);
let y0 = yc.floor() as usize;
let y1 = (y0 + 1).min(h - 1);
let fy = yc - (y0 as f32);
let a = frame.get_unchecked(x, y0) as f32;
let b = frame.get_unchecked(x, y1) as f32;
a + (b - a) * fy
}
fn sample_profile(frame: &GrayFrame<'_>, cy: f32, tan: f32, smooth_radius: usize) -> Vec<f32> {
let w = frame.width();
let half_w = (w as f32) / 2.0;
let mut profile = Vec::with_capacity(w);
for x in 0..w {
let y = cy + ((x as f32) - half_w) * tan;
profile.push(sample_column(frame, x, y));
}
smooth(&profile, smooth_radius)
}
fn smooth(profile: &[f32], radius: usize) -> Vec<f32> {
if radius == 0 {
return profile.to_vec();
}
let n = profile.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let lo = i.saturating_sub(radius);
let hi = (i + radius).min(n - 1);
let mut sum = 0.0;
for &v in &profile[lo..=hi] {
sum += v;
}
out.push(sum / ((hi - lo + 1) as f32));
}
out
}
#[derive(Debug)]
struct Runs {
edges: Vec<f32>,
amplitude: f32,
lead_px: f32,
trail_px: f32,
}
fn levels(profile: &[f32]) -> (f32, f32) {
let mut sorted = profile.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let n = sorted.len();
let tenth = (n / 10).max(1);
let low: f32 = sorted[..tenth].iter().sum::<f32>() / (tenth as f32);
let high: f32 = sorted[n - tenth..].iter().sum::<f32>() / (tenth as f32);
(low, high)
}
fn crossing(i: usize, a: f32, b: f32, thr: f32) -> f32 {
let denom = b - a;
if denom.abs() < 1e-6 {
(i as f32) - 0.5
} else {
((i - 1) as f32) + (thr - a) / denom
}
}
fn extract_runs(profile: &[f32]) -> Option<Runs> {
let n = profile.len();
if n < 4 {
return None;
}
let (low, high) = levels(profile);
let amplitude = high - low;
if amplitude < MIN_AMPLITUDE {
return None;
}
let thr = (low + high) / 2.0;
let hyst = amplitude * 0.12;
let hi = thr + hyst;
let lo = thr - hyst;
let start_light = profile[0] >= thr;
if !start_light {
return None;
}
let mut edges: Vec<f32> = Vec::new();
let mut dark = false; let mut cand: Option<f32> = None;
for i in 1..n {
let a = profile[i - 1];
let b = profile[i];
if dark {
if a < thr && b >= thr {
cand = Some(crossing(i, a, b, thr));
} else if b < thr {
cand = None;
}
if b >= hi {
edges.push(cand.take().unwrap_or((i as f32) - 0.5));
dark = false;
}
} else {
if a > thr && b <= thr {
cand = Some(crossing(i, a, b, thr));
} else if b > thr {
cand = None;
}
if b <= lo {
edges.push(cand.take().unwrap_or((i as f32) - 0.5));
dark = true;
}
}
}
if dark || edges.len() < 2 || !edges.len().is_multiple_of(2) {
return None;
}
let refined = refine_edges(profile, &edges);
let lead_px = refined[0];
let trail_px = ((n - 1) as f32) - refined[refined.len() - 1];
Some(Runs {
edges: refined,
amplitude,
lead_px,
trail_px,
})
}
fn refine_edges(profile: &[f32], approx: &[f32]) -> Vec<f32> {
let n = profile.len();
let mut bounds = Vec::with_capacity(approx.len() + 2);
bounds.push(0.0f32);
bounds.extend_from_slice(approx);
bounds.push((n - 1) as f32);
let mut ext = Vec::with_capacity(bounds.len() - 1);
for k in 0..bounds.len() - 1 {
let lo = (bounds[k].ceil() as usize).min(n - 1);
let hi = (bounds[k + 1].floor() as usize).min(n - 1);
let dark_run = k % 2 == 1;
let (a, b) = if lo <= hi {
(lo, hi)
} else {
(lo.min(hi), lo.max(hi))
};
let mut acc = profile[a];
for &v in &profile[a..=b] {
if dark_run {
acc = acc.min(v);
} else {
acc = acc.max(v);
}
}
ext.push(acc);
}
let mut out = Vec::with_capacity(approx.len());
for (j, &e) in approx.iter().enumerate() {
let lthr = (ext[j] + ext[j + 1]) / 2.0;
let left_center = (bounds[j] + bounds[j + 1]) / 2.0;
let right_center = (bounds[j + 1] + bounds[j + 2]) / 2.0;
out.push(local_crossing(profile, left_center, right_center, lthr, e));
}
out
}
fn local_crossing(profile: &[f32], left: f32, right: f32, lthr: f32, approx: f32) -> f32 {
let n = profile.len();
let lo = (left.floor().max(0.0) as usize).max(1);
let hi = (right.ceil() as usize).min(n - 1);
let mut best: Option<f32> = None;
let mut best_d = f32::INFINITY;
for i in lo..=hi {
let a = profile[i - 1];
let b = profile[i];
if (a - lthr) * (b - lthr) <= 0.0 && (a - b).abs() > 1e-6 {
let pos = crossing(i, a, b, lthr);
let d = (pos - approx).abs();
if d < best_d {
best_d = d;
best = Some(pos);
}
}
}
best.unwrap_or(approx)
}
fn smooth3(profile: &[f32]) -> Vec<f32> {
let n = profile.len();
(0..n)
.map(|i| {
let a = profile[i.saturating_sub(1)];
let c = profile[(i + 1).min(n - 1)];
(a + profile[i] + c) / 3.0
})
.collect()
}
fn extract_runs_fine(profile: &[f32], prom: f32) -> Option<Runs> {
let n = profile.len();
if n < 8 {
return None;
}
let (low, high) = levels(profile);
let amplitude = high - low;
if amplitude < MIN_AMPLITUDE {
return None;
}
let thr = (low + high) / 2.0;
if profile[0] < thr || profile[n - 1] < thr {
return None;
}
let sm = smooth3(profile);
let mut ext: Vec<(usize, f32, i8)> = vec![(0, profile[0], 1)];
let mut dir: i8 = 0;
for i in 1..n {
let dv = sm[i] - sm[i - 1];
let nd = if dv > 0.0 {
1
} else if dv < 0.0 {
-1
} else {
dir
};
if dir != 0 && nd != dir {
ext.push((i - 1, sm[i - 1], dir));
}
dir = nd;
}
ext.push((n - 1, profile[n - 1], 1));
if ext.len() > MAX_FINE_EXTREMA {
return None;
}
let mut j = 1;
while j < ext.len() {
if ext[j].2 == ext[j - 1].2 {
let keep_prev = if ext[j].2 > 0 {
ext[j - 1].1 >= ext[j].1
} else {
ext[j - 1].1 <= ext[j].1
};
ext.remove(if keep_prev { j } else { j - 1 });
} else {
j += 1;
}
}
let min_prom = amplitude * prom;
loop {
if ext.len() < 3 {
return None;
}
let mut worst: Option<(usize, f32)> = None;
for k in 1..ext.len() - 1 {
let p = (ext[k].1 - ext[k - 1].1)
.abs()
.min((ext[k].1 - ext[k + 1].1).abs());
if p < min_prom && worst.is_none_or(|(_, wp)| p < wp) {
worst = Some((k, p));
}
}
let Some((k, _)) = worst else { break };
ext.remove(k);
if k < ext.len() && ext[k - 1].2 == ext[k].2 {
let keep_prev = if ext[k].2 > 0 {
ext[k - 1].1 >= ext[k].1
} else {
ext[k - 1].1 <= ext[k].1
};
ext.remove(if keep_prev { k } else { k - 1 });
}
}
let mut edges: Vec<f32> = Vec::with_capacity(ext.len().saturating_sub(1));
for w in ext.windows(2) {
let (p0, v0, _) = w[0];
let (p1, v1, _) = w[1];
let mid = (v0 + v1) / 2.0;
let mut pos = (p0 as f32 + p1 as f32) / 2.0;
for i in (p0 + 1)..=p1 {
let a = profile[i - 1];
let b = profile[i];
if (a - mid) * (b - mid) <= 0.0 && (a - b).abs() > 1e-6 {
pos = (i - 1) as f32 + (mid - a) / (b - a);
break;
}
}
edges.push(pos);
}
if edges.len() < 2 || !edges.len().is_multiple_of(2) {
return None;
}
let lead_px = edges[0];
let trail_px = ((n - 1) as f32) - edges[edges.len() - 1];
Some(Runs {
edges,
amplitude,
lead_px,
trail_px,
})
}
#[derive(Debug)]
struct Quantized {
pattern: LinearPattern,
module_px: f32,
fit: f32,
}
fn quantize(runs: &Runs, min_runs: usize) -> Option<Quantized> {
let edges = &runs.edges;
let inner: Vec<f32> = edges.windows(2).map(|w| w[1] - w[0]).collect();
if inner.len() < min_runs {
return None;
}
let mut module = inner
.iter()
.cloned()
.fold(f32::INFINITY, f32::min)
.max(1e-3);
for _ in 0..6 {
let mut sum_w = 0.0f32;
let mut sum_n = 0.0f32;
for &wpx in &inner {
let n = (wpx / module).round().max(1.0);
sum_w += wpx;
sum_n += n;
}
if sum_n > 0.0 {
module = sum_w / sum_n;
}
}
let mut modules: Vec<bool> = Vec::new();
let mut err_sum = 0.0f32;
for (idx, &wpx) in inner.iter().enumerate() {
let exact = wpx / module;
let n = exact.round().max(1.0);
err_sum += (exact - n).abs();
let is_bar = idx % 2 == 0;
for _ in 0..(n as usize) {
modules.push(is_bar);
}
}
let mean_err = err_sum / (inner.len() as f32);
let fit = (1.0 - 2.0 * mean_err).clamp(0.0, 1.0);
let qz_px = runs.lead_px.min(runs.trail_px);
let quiet_zone = (qz_px / module).round().max(0.0) as usize;
Some(Quantized {
pattern: LinearPattern {
modules,
quiet_zone,
},
module_px: module,
fit,
})
}
fn analyze_profile(
profile: &[f32],
cy: f32,
tan: f32,
deg: f32,
width: usize,
min_runs: usize,
) -> Option<LinearCandidate> {
let runs = extract_runs(profile)?;
let q = quantize(&runs, min_runs)?;
let amp_factor = (runs.amplitude / 128.0).clamp(0.0, 1.0);
let confidence = (q.fit * amp_factor).clamp(0.0, 1.0);
let location = build_location(&runs, cy, tan, deg, width, q.module_px);
Some(LinearCandidate {
pattern: q.pattern,
edges: runs.edges,
lead_px: runs.lead_px,
trail_px: runs.trail_px,
location,
confidence,
})
}
fn analyze_profile_fine(
profile: &[f32],
cy: f32,
tan: f32,
deg: f32,
width: usize,
min_runs: usize,
prom: f32,
) -> Option<LinearCandidate> {
let runs = extract_runs_fine(profile, prom)?;
let q = quantize(&runs, min_runs)?;
let amp_factor = (runs.amplitude / 128.0).clamp(0.0, 1.0);
let confidence = (q.fit * amp_factor).clamp(0.0, 1.0);
let location = build_location(&runs, cy, tan, deg, width, q.module_px);
Some(LinearCandidate {
pattern: q.pattern,
edges: runs.edges,
lead_px: runs.lead_px,
trail_px: runs.trail_px,
location,
confidence,
})
}
fn build_location(
runs: &Runs,
cy: f32,
tan: f32,
deg: f32,
width: usize,
module_px: f32,
) -> Location {
let theta = deg.to_radians();
let (sin, cos) = theta.sin_cos();
let half_w = (width as f32) / 2.0;
let x0 = runs.edges[0];
let x1 = runs.edges[runs.edges.len() - 1];
let point_on = |x: f32| Point::new(x, cy + (x - half_w) * tan);
let left = point_on(x0);
let right = point_on(x1);
let half_h = 2.0f32;
let px = -sin * half_h;
let py = cos * half_h;
let outline = Quad::new([
Point::new(left.x + px, left.y + py),
Point::new(right.x + px, right.y + py),
Point::new(right.x - px, right.y - py),
Point::new(left.x - px, left.y - py),
]);
Location {
outline,
rotation: Some(theta),
module_size: Some(module_px),
}
}
fn dedupe(cands: &mut Vec<LinearCandidate>, max: usize) {
cands.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(core::cmp::Ordering::Equal)
});
let mut kept: Vec<LinearCandidate> = Vec::new();
for c in cands.drain(..) {
if kept.iter().any(|k| k.pattern.modules == c.pattern.modules) {
continue;
}
kept.push(c);
if kept.len() >= max {
break;
}
}
*cands = kept;
}
#[cfg(test)]
mod tests;