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 location: Location,
pub confidence: f32,
}
pub fn scan_lines(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);
}
}
}
dedupe(&mut found, opts.max_candidates);
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)
}
#[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,
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;