//! QR 码定位图案检测与采样 //! //! 在二值化图像中检测 3 个定位图案(1:1:3:1:1 比例), //! 构建模块采样网格,提取布尔矩阵。 /// 定位图案检测结果(图像坐标,原点左上角) struct FinderMatch { cx: usize, // 中心 X cy: usize, // 中心 Y size: usize, // 探测器边长(像素) } /// QR 码检测结果 #[allow(dead_code)] pub(crate) struct DetectResult { pub modules: Vec>, // 布尔矩阵(含静区) pub version_estimate: u8, pub bounds: crate::decoder::BoundingBox, pub quiet_zone_ok: bool, pub finder_ratio_deviation: f64, } /// 水平扫描查找 1:1:3:1:1 比例 fn scan_row(gray: &[Vec], row: usize) -> Vec<(usize, usize, usize)> { // (列号,运行长度) let mut runs: Vec<(usize, usize)> = Vec::new(); let width = if gray.is_empty() { 0 } else { gray[0].len() }; let mut col = 0; while col < width { let current = gray[row][col]; let mut run_len = 0; while col < width && gray[row][col] == current { run_len += 1; col += 1; } runs.push((col - run_len, run_len)); } // 找 5 连段符合 1:1:3:1:1 比例 — 返回 (cx, cy, total_size_px) let mut centers: Vec<(usize, usize, usize)> = Vec::new(); for i in 0..runs.len().saturating_sub(4) { let r0 = runs[i].1 as f32; let r1 = runs[i + 1].1 as f32; let r2 = runs[i + 2].1 as f32; let r3 = runs[i + 3].1 as f32; let r4 = runs[i + 4].1 as f32; let total = r0 + r1 + r2 + r3 + r4; let base = total / 7.0; if base < 2.0 { continue; } let tolerance = 0.5; // ZXing 标准:moduleSize/2 容差 let check = |v: f32, expected: f32| (v - expected * base).abs() < base * tolerance; if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) { // ZXing 的 end-based 中心公式: end - r4 - r3 - r2/2 (浮点精度更高) let end = runs[i + 4].0 + runs[i + 4].1; let cx = end as f32 - r4 - r3 - r2 / 2.0; centers.push((cx as usize, row, total as usize)); } } centers } /// 垂直扫描查找 1:1:3:1:1 比例 fn scan_col(gray: &[Vec], col: usize) -> Vec<(usize, usize, usize)> { let height = gray.len(); let mut runs: Vec<(usize, usize)> = Vec::new(); let mut row = 0; while row < height { let current = gray[row][col]; let mut run_len = 0; while row < height && gray[row][col] == current { run_len += 1; row += 1; } runs.push((row - run_len, run_len)); } let mut centers: Vec<(usize, usize, usize)> = Vec::new(); for i in 0..runs.len().saturating_sub(4) { let r0 = runs[i].1 as f32; let r1 = runs[i + 1].1 as f32; let r2 = runs[i + 2].1 as f32; let r3 = runs[i + 3].1 as f32; let r4 = runs[i + 4].1 as f32; let total = r0 + r1 + r2 + r3 + r4; let base = total / 7.0; if base < 2.0 { continue; } let tolerance = 0.5; // ZXing 标准:moduleSize/2 容差 let check = |v: f32, expected: f32| (v - expected * base).abs() < base * tolerance; if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) { let end = runs[i + 4].0 + runs[i + 4].1; let cy = end as f32 - r4 - r3 - r2 / 2.0; centers.push((col, cy as usize, total as usize)); } } centers } /// 检测 3 个定位图案,返回 (cx, cy) 像素坐标对(供透视矫正使用) pub(crate) fn find_finders_for_perspective( gray: &[Vec], ) -> Result<[(usize, usize); 3], crate::error::QrError> { let finders = find_finders(gray) .ok_or_else(|| crate::error::QrError::DecodeFail("未找到 QR 码定位图案".into()))?; Ok([ (finders[0].cx, finders[0].cy), (finders[1].cx, finders[1].cy), (finders[2].cx, finders[2].cy), ]) } /// 从定位图案估算模块大小(像素/模块) pub(crate) fn estimate_module_size_from_finders( _gray: &[Vec], finders: &[(usize, usize); 3], ) -> usize { // 用 TL-TR 和 TL-BL 距离估算 let dx1 = (finders[1].0 as f64 - finders[0].0 as f64).abs(); let dy1 = (finders[1].1 as f64 - finders[0].1 as f64).abs(); let dx2 = (finders[2].0 as f64 - finders[0].0 as f64).abs(); let dy2 = (finders[2].1 as f64 - finders[0].1 as f64).abs(); let dist1 = (dx1 * dx1 + dy1 * dy1).sqrt(); let dist2 = (dx2 * dx2 + dy2 * dy2).sqrt(); let avg_dist = (dist1 + dist2) / 2.0; // 版本 1: dist ≈ 14 modules (7+1-1+7 = 14) // 我们并不知道确切版本,使用 iter 近似 (avg_dist / 14.0).max(1.0) as usize } /// 从 TL-TR 距离估算版本号 pub(crate) fn estimate_version_from_tl_tr(tl: (f64, f64), tr: (f64, f64), module_size: usize) -> u8 { let dx = tr.0 - tl.0; let dy = tr.1 - tl.1; let dist_px = (dx * dx + dy * dy).sqrt() as f32; let dist_modules = dist_px / module_size as f32; let ver = ((dist_modules as i32 - 10) / 4) as u8; ver.clamp(1, 40) } /// 检测 3 个定位图案(交叉验证水平+垂直扫描) fn find_finders(gray: &[Vec]) -> Option<[FinderMatch; 3]> { let height = gray.len(); let width = if height > 0 { gray[0].len() } else { 0 }; if width < 21 || height < 21 { return None; } // 水平扫描(含 finder 总尺寸) let mut h_centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size) for row in (0..height).step_by(2) { for (cx, cy, total_size) in scan_row(gray, row) { // 交叉验证:垂直扫描 let v_matches = scan_col(gray, cx); if v_matches .iter() .any(|&(_, vy, _)| (vy as i32 - cy as i32).abs() < 5) { h_centers.push((cx, cy, total_size)); } } } if h_centers.len() < 3 { return None; } // 聚类取前 3 个 let mut clusters: Vec> = Vec::new(); for c in &h_centers { let mut found = false; for cluster in &mut clusters { let avg_x: f64 = cluster.iter().map(|c| c.0 as f64).sum::() / cluster.len() as f64; let avg_y: f64 = cluster.iter().map(|c| c.1 as f64).sum::() / cluster.len() as f64; let dx = c.0 as f64 - avg_x; let dy = c.1 as f64 - avg_y; if (dx * dx + dy * dy).sqrt() < 20.0 { cluster.push(c); found = true; break; } } if !found { clusters.push(vec![c]); } } // 按聚类大小排序,取前 3 clusters.sort_by_key(|c| -(c.len() as i32)); if clusters.len() < 3 { return None; } let mut finders: Vec = Vec::new(); for cluster in clusters.iter().take(3) { let avg_x = cluster.iter().map(|c| c.0).sum::() / cluster.len(); let avg_y = cluster.iter().map(|c| c.1).sum::() / cluster.len(); let avg_size = cluster.iter().map(|c| c.2).sum::() / cluster.len(); finders.push(FinderMatch { cx: avg_x, cy: avg_y, size: avg_size, }); } // 排序:左上、右上、左下 finders.sort_by(|a, b| { let da = a.cx * a.cx + a.cy * a.cy; let db = b.cx * b.cx + b.cy * b.cy; da.cmp(&db) }); if finders[1].cx < finders[2].cx { finders.swap(1, 2); } // remove 会使后续元素前移,必须每次 remove(0) let f0 = finders.remove(0); let f1 = finders.remove(0); let f2 = finders.remove(0); // ZXing 几何验证:module_size 一致性 + 勾股定理 if !validate_finder_geometry(&[&f0, &f1, &f2]) { return None; } Some([f0, f1, f2]) } /// ZXing 风格几何验证:3 个定位图案的 module_size 必须一致且构成近似直角三角形 fn validate_finder_geometry(finders: &[&FinderMatch; 3]) -> bool { let s0 = finders[0].size as f64; let s1 = finders[1].size as f64; let s2 = finders[2].size as f64; // module_size 一致性: 最大偏差 < 10% let avg_size = (s0 + s1 + s2) / 3.0; let max_dev = avg_size * 0.10; if (s0 - avg_size).abs() > max_dev || (s1 - avg_size).abs() > max_dev || (s2 - avg_size).abs() > max_dev { return false; } // 勾股定理验证: |C - sqrt(A² + B²)| / min(C, sqrt(A² + B²)) < 15% let ax = finders[1].cx as f64 - finders[0].cx as f64; let ay = finders[1].cy as f64 - finders[0].cy as f64; let bx = finders[2].cx as f64 - finders[0].cx as f64; let by = finders[2].cy as f64 - finders[0].cy as f64; let a_len = (ax * ax + ay * ay).sqrt(); let b_len = (bx * bx + by * by).sqrt(); let c_len = { let cx = finders[2].cx as f64 - finders[1].cx as f64; let cy = finders[2].cy as f64 - finders[1].cy as f64; (cx * cx + cy * cy).sqrt() }; let hypotenuse = (a_len * a_len + b_len * b_len).sqrt(); let min = c_len.min(hypotenuse); if min < 1.0 { return true; // 太小,跳过检查 } let deviation = (c_len - hypotenuse).abs() / min; deviation < 0.15 } /// 从二值化图像中提取 QR 布尔矩阵 pub(crate) fn detect_and_extract( gray: &[Vec], ) -> Result { let finders = find_finders(gray).ok_or_else(|| crate::error::QrError::DecodeFail("未找到 QR 码定位图案".into()))?; let tl = &finders[0]; // top-left let tr = &finders[1]; // top-right let _bl = &finders[2]; // bottom-left // 估算模块大小 let module_size = (tl.size + tr.size) / 14; // finder = 7 modules wide if module_size == 0 { return Err(crate::error::QrError::DecodeFail( "模块大小估算为零".into(), )); } // 估算版本 let dx = tr.cx as f64 - tl.cx as f64; let dy = tr.cy as f64 - tl.cy as f64; let dist_px = (dx * dx + dy * dy).sqrt() as f32; let dist_modules = dist_px / module_size as f32; // ZXing 公式: totalModules = dist/moduleSize + 7, version = (totalModules - 17) / 4 // 简化为: version = (dist_modules + 7 - 17) / 4 = (dist_modules - 10) / 4 let ver = ((dist_modules as i32 - 10) / 4) as u8; let version = ver.clamp(1, 40); let size = 17 + version as usize * 4; // 从采样网格构建模块矩阵 let mut modules: Vec> = Vec::with_capacity(size); for my in 0..size { let mut row = Vec::with_capacity(size); for mx in 0..size { let px = tl.cx as f32 + (mx as f32 - 3.5) * module_size as f32; let py = tl.cy as f32 + (my as f32 - 3.5) * module_size as f32; let px = px.round() as i32; let py = py.round() as i32; let sample = if px >= 0 && py >= 0 && (py as usize) < gray.len() && (px as usize) < gray[0].len() { gray[py as usize][px as usize] } else { false }; row.push(sample); } modules.push(row); } // 静区验证:检查矩阵四边各 4 行/列是否为白色 let quiet_zone_ok = check_quiet_zone(&modules, size); // 包围盒:从 3 个定位图案推算 QR 码在图像中的像素区域 let margin_px = (module_size as u32).saturating_mul(4); let x_min = tl.cx.saturating_sub(3 * module_size) as u32; let y_min = tl.cy.saturating_sub(3 * module_size) as u32; let x_max = ((tr.cx + 3 * module_size) as u32).saturating_add(margin_px); let y_max = ((_bl.cy + 3 * module_size) as u32).saturating_add(margin_px); let bounds = crate::decoder::BoundingBox { x: x_min.saturating_sub(margin_px), y: y_min.saturating_sub(margin_px), width: x_max.saturating_sub(x_min).saturating_add(2 * margin_px), height: y_max.saturating_sub(y_min).saturating_add(2 * margin_px), }; // 定位图案比例偏差:实际 5 段比例与 1:1:3:1:1 的偏差 let finder_ratio_deviation = compute_finder_deviation(gray, tl); Ok(DetectResult { modules, version_estimate: version, bounds, quiet_zone_ok, finder_ratio_deviation, }) } /// 检查静区:矩阵四边各 4 行/列是否为全白色 fn check_quiet_zone(modules: &[Vec], size: usize) -> bool { let quiet = 4usize; // 上边 if modules.iter().take(quiet.min(size)).any(|row| row.iter().any(|&m| m)) { return false; } // 下边 if modules.iter().skip(size.saturating_sub(quiet)).any(|row| row.iter().any(|&m| m)) { return false; } // 左边 & 右边 for row in modules.iter() { if row.iter().take(quiet.min(size)).any(|&m| m) { return false; } if row.iter().skip(size.saturating_sub(quiet)).any(|&m| m) { return false; } } true } /// 计算定位图案比例偏差(0=完美, 1=最大),使用 base=total/7 对齐 ZXing fn compute_finder_deviation(gray: &[Vec], finder: &FinderMatch) -> f64 { let row = finder.cy; let mut runs: Vec = Vec::new(); let mut col = finder.cx.saturating_sub(finder.size); let end = (finder.cx + finder.size).min(gray[0].len()); while col < end { let current = gray[row][col]; let mut run_len = 0; while col < end && gray[row][col] == current { run_len += 1; col += 1; } runs.push(run_len); } if runs.len() < 5 { return 1.0; } let mut best_dev = 1.0f64; for i in 0..runs.len().saturating_sub(4) { let total = (runs[i] + runs[i + 1] + runs[i + 2] + runs[i + 3] + runs[i + 4]) as f64; let base = total / 7.0; if base < 2.0 { continue; } let expected = [base, base, 3.0 * base, base, base]; let dev: f64 = (0..5) .map(|j| (runs[i + j] as f64 - expected[j]).abs() / base) .sum::() / 5.0; if dev < best_dev { best_dev = dev; } } best_dev }