feat: 解码器全面增强 — 多遍二值化 + 4点透视 + ECI/FNC1

Phase 1 — 二值化+检测增强:
- decoder/image.rs 重写: 中位数→Otsu→Sauvola 三遍自适应阈值
- 新增 load_gray() 公共 API 供调用方自定义预处理
- 反色QR码自动检测(5点采样法)
- 静区验证 + 包围盒(BoundingBox) + 解码置信度评分
- 定位图案比例偏差计算

Phase 2 — 4点透视变换+多QR码:
- decoder/perspective.rs 重写: DLT求解3x3单应矩阵
- 用3个定位图案+推算第4点构建4点对应
- 双线性插值替换最近邻采样
- 多角度旋转回退(7个预设角度)
- decode_all() 返回图像中所有QR码(按置信度降序)

Phase 3 — ISO合规:
- decoder/bch.rs: decode_format_info/version_info 返回汉明距离
- decoder/format.rs: 副本冲突时选汉明距离更小者
- decoder/mode_decode.rs: ECI指派号识别, FNC1/GS1检测(0101/1001)
- 结构化追加(0011)头部提取: position/total/parity
- DecodeResult 扩展: is_gs1, eci_designator, structured_append

验证: cargo check+clippy通过, 81+24测试全部通过
This commit is contained in:
2026-06-21 16:00:11 +08:00
parent cd75141037
commit bd4ca718ac
9 changed files with 971 additions and 252 deletions
+128 -1
View File
@@ -15,6 +15,9 @@ struct FinderMatch {
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 比例
@@ -102,6 +105,47 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, 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 - 14) / 4) as u8;
ver.clamp(1, 40)
}
/// 检测 3 个定位图案(交叉验证水平+垂直扫描)
fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
let height = gray.len();
@@ -245,7 +289,6 @@ pub(crate) fn detect_and_extract(
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;
@@ -266,8 +309,92 @@ pub(crate) fn detect_and_extract(
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
}
/// 计算定位图案比例偏差(与理想 1:1:3:1:1 的偏差,0=完美, 1=最大)
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 avg =
(runs[i] + runs[i + 1] + runs[i + 2] + runs[i + 3] + runs[i + 4]) as f64 / 5.0;
if avg < 2.0 {
continue;
}
let expected = [avg, avg, 3.0 * avg, avg, avg];
let dev: f64 = (0..5)
.map(|j| (runs[j] as f64 - expected[j]).abs() / avg)
.sum::<f64>()
/ 5.0;
if dev < best_dev {
best_dev = dev;
}
}
best_dev
}