Files
QRGen/core/src/decoder/detect.rs
T
Serendipity ce9c8b1b6e fix: 解码器3个bug修复 — 编码→解码往返恢复 + 新增往返测试
1. detect.rs: remove()索引bug — 元素前移后索引未更新,改用每次remove(0)
2. detect.rs: 版本估算公式修正 — (dist-14)/4 → (dist-10)/4,符合ZXing公式
3. extract.rs: 移除显式col 6跳过 — read_module已自动跳过保留区,显式skip导致列配对错位/行序反转

新增 test_roundtrip_png: 矩阵往返 + PNG往返双验证
2026-06-27 14:53:48 +08:00

428 lines
14 KiB
Rust

//! 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<Vec<bool>>, // 布尔矩阵(含静区)
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<bool>], 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<bool>], 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<bool>],
) -> 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<bool>],
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<bool>]) -> 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<&(usize, usize, usize)>> = 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::<f64>() / cluster.len() as f64;
let avg_y: f64 = cluster.iter().map(|c| c.1 as f64).sum::<f64>() / 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<FinderMatch> = Vec::new();
for cluster in clusters.iter().take(3) {
let avg_x = cluster.iter().map(|c| c.0).sum::<usize>() / cluster.len();
let avg_y = cluster.iter().map(|c| c.1).sum::<usize>() / cluster.len();
let avg_size = cluster.iter().map(|c| c.2).sum::<usize>() / 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<bool>],
) -> Result<DetectResult, crate::error::QrError> {
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<bool>> = 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<bool>], 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<bool>], finder: &FinderMatch) -> f64 {
let row = finder.cy;
let mut runs: Vec<usize> = 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::<f64>()
/ 5.0;
if dev < best_dev {
best_dev = dev;
}
}
best_dev
}