fix: 解码器核心bug修复 — 容差计算+积分图像+Sauvola下溢

- detect.rs: finder比例容差改用 base=total/7 替代 avg=total/5
  修复 1:1:3:1:1 模式匹配对任意module_size均正确工作
- detect.rs: finder尺寸估算改为5段总长度(完整28px)替代外圈暗环(4px)
  修复版本号误判(原本将version 1误判为version 10)
- detect.rs: scan_row/scan_col 返回类型改为 (cx,cy,total_size) 三元组
- image.rs: 积分图像公式修正为 I(y+1,x+1)=I(y,x+1)+I(y+1,x)-I(y,x)+pixel
  替换错误的 cumulative row_sum 公式
- image.rs: 积分和运算 u64→i64 避免 debug 模式溢出panic
- perspective.rs: count_finder_hits 应用相同容差修复
- mask.rs: test_score_rule2 改用 score_rule2_raw
- bch.rs: 测试适配 decode_format_info→3元组, decode_version_info→2元组
This commit is contained in:
2026-06-21 22:19:00 +08:00
parent 5651dbf732
commit a03ab95ce5
6 changed files with 47 additions and 47 deletions
+6 -5
View File
@@ -118,7 +118,7 @@ mod tests {
result.is_some(), result.is_some(),
"decode failed: ec_bits={ec_bits}, mask={mask}, encoded={encoded:#05X}" "decode failed: ec_bits={ec_bits}, mask={mask}, encoded={encoded:#05X}"
); );
let (dec_ec, dec_mask) = result.unwrap(); let (dec_ec, dec_mask, _d) = result.unwrap();
assert_eq!(ec_bits, dec_ec, "ec_bits mismatch"); assert_eq!(ec_bits, dec_ec, "ec_bits mismatch");
assert_eq!(mask, dec_mask, "mask mismatch"); assert_eq!(mask, dec_mask, "mask mismatch");
} }
@@ -134,7 +134,8 @@ mod tests {
result.is_some(), result.is_some(),
"decode failed: ver={ver}, encoded={encoded:#010X}" "decode failed: ver={ver}, encoded={encoded:#010X}"
); );
assert_eq!(ver, result.unwrap(), "version mismatch"); let (dec_ver, _d) = result.unwrap();
assert_eq!(ver, dec_ver, "version mismatch");
} }
} }
@@ -143,12 +144,11 @@ mod tests {
for ec_bits in 0u8..4 { for ec_bits in 0u8..4 {
for mask in 0u8..8 { for mask in 0u8..8 {
let original = encode_format_info(ec_bits, mask); let original = encode_format_info(ec_bits, mask);
// 翻转每个比特,验证能纠错
for bit in 0..15 { for bit in 0..15 {
let corrupted = original ^ (1 << bit); let corrupted = original ^ (1 << bit);
let result = decode_format_info(corrupted); let result = decode_format_info(corrupted);
assert!(result.is_some(), "1-bit error not corrected at bit {bit}"); assert!(result.is_some(), "1-bit error not corrected at bit {bit}");
let (dec_ec, dec_mask) = result.unwrap(); let (dec_ec, dec_mask, _d) = result.unwrap();
assert_eq!(ec_bits, dec_ec); assert_eq!(ec_bits, dec_ec);
assert_eq!(mask, dec_mask); assert_eq!(mask, dec_mask);
} }
@@ -164,7 +164,8 @@ mod tests {
let corrupted = original ^ (1 << bit); let corrupted = original ^ (1 << bit);
let result = decode_version_info(corrupted); let result = decode_version_info(corrupted);
assert!(result.is_some(), "1-bit error not corrected at bit {bit}"); assert!(result.is_some(), "1-bit error not corrected at bit {bit}");
assert_eq!(ver, result.unwrap()); let (dec_ver, _d) = result.unwrap();
assert_eq!(ver, dec_ver);
} }
} }
} }
+20 -20
View File
@@ -21,7 +21,7 @@ pub(crate) struct DetectResult {
} }
/// 水平扫描查找 1:1:3:1:1 比例 /// 水平扫描查找 1:1:3:1:1 比例
fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> { fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize, usize)> {
// (列号,运行长度) // (列号,运行长度)
let mut runs: Vec<(usize, usize)> = Vec::new(); let mut runs: Vec<(usize, usize)> = Vec::new();
let width = if gray.is_empty() { 0 } else { gray[0].len() }; let width = if gray.is_empty() { 0 } else { gray[0].len() };
@@ -37,8 +37,8 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
runs.push((col - run_len, run_len)); runs.push((col - run_len, run_len));
} }
// 找 5 连段符合 1:1:3:1:1 比例 // 找 5 连段符合 1:1:3:1:1 比例 — 返回 (cx, cy, total_size_px)
let mut centers: Vec<(usize, usize)> = Vec::new(); let mut centers: Vec<(usize, usize, usize)> = Vec::new();
for i in 0..runs.len().saturating_sub(4) { for i in 0..runs.len().saturating_sub(4) {
let r0 = runs[i].1 as f32; let r0 = runs[i].1 as f32;
let r1 = runs[i + 1].1 as f32; let r1 = runs[i + 1].1 as f32;
@@ -46,18 +46,17 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
let r3 = runs[i + 3].1 as f32; let r3 = runs[i + 3].1 as f32;
let r4 = runs[i + 4].1 as f32; let r4 = runs[i + 4].1 as f32;
let avg = (r0 + r1 + r2 + r3 + r4) / 5.0; let total = r0 + r1 + r2 + r3 + r4;
if avg < 2.0 { let base = total / 7.0;
if base < 2.0 {
continue; continue;
} }
// 检查比例容差 ±40%
let tolerance = 0.4; let tolerance = 0.4;
let check = |v: f32, expected: f32| (v - expected * avg).abs() < avg * tolerance; 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) { if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) {
let cx = runs[i + 2].0 + runs[i + 2].1 / 2; let cx = runs[i + 2].0 + runs[i + 2].1 / 2;
centers.push((cx, row)); centers.push((cx, row, total as usize));
} }
} }
@@ -65,7 +64,7 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
} }
/// 垂直扫描查找 1:1:3:1:1 比例 /// 垂直扫描查找 1:1:3:1:1 比例
fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> { fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize, usize)> {
let height = gray.len(); let height = gray.len();
let mut runs: Vec<(usize, usize)> = Vec::new(); let mut runs: Vec<(usize, usize)> = Vec::new();
@@ -80,7 +79,7 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
runs.push((row - run_len, run_len)); runs.push((row - run_len, run_len));
} }
let mut centers: Vec<(usize, usize)> = Vec::new(); let mut centers: Vec<(usize, usize, usize)> = Vec::new();
for i in 0..runs.len().saturating_sub(4) { for i in 0..runs.len().saturating_sub(4) {
let r0 = runs[i].1 as f32; let r0 = runs[i].1 as f32;
let r1 = runs[i + 1].1 as f32; let r1 = runs[i + 1].1 as f32;
@@ -88,17 +87,19 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
let r3 = runs[i + 3].1 as f32; let r3 = runs[i + 3].1 as f32;
let r4 = runs[i + 4].1 as f32; let r4 = runs[i + 4].1 as f32;
let avg = (r0 + r1 + r2 + r3 + r4) / 5.0; let total = r0 + r1 + r2 + r3 + r4;
if avg < 2.0 { let base = total / 7.0;
if base < 2.0 {
continue; continue;
} }
let tolerance = 0.4; let tolerance = 0.4;
let check = |v: f32, expected: f32| (v - expected * avg).abs() < avg * tolerance; 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) { if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) {
let cy = runs[i + 2].0 + runs[i + 2].1 / 2; let cy = runs[i + 2].0 + runs[i + 2].1 / 2;
centers.push((col, cy)); let total_px = total as usize;
centers.push((col, cy, total_px));
} }
} }
@@ -154,18 +155,17 @@ fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
return None; return None;
} }
// 水平扫描 // 水平扫描(含 finder 总尺寸)
let mut h_centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size) let mut h_centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size)
for row in (0..height).step_by(2) { for row in (0..height).step_by(2) {
for (cx, cy) in scan_row(gray, row) { for (cx, cy, total_size) in scan_row(gray, row) {
// 交叉验证:垂直扫描 // 交叉验证:垂直扫描
let v_matches = scan_col(gray, cx); let v_matches = scan_col(gray, cx);
if v_matches if v_matches
.iter() .iter()
.any(|&(_, vy)| (vy as i32 - cy as i32).abs() < 5) .any(|&(_, vy, _)| (vy as i32 - cy as i32).abs() < 5)
{ {
let size = estimate_finder_size(gray, cx, cy); h_centers.push((cx, cy, total_size));
h_centers.push((cx, cy, size));
} }
} }
} }
+14 -16
View File
@@ -113,21 +113,19 @@ fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec<Vec<bool>> {
let r = 128.0f64; let r = 128.0f64;
// 预计算积分图像以加速窗口和/平方和 // 预计算积分图像以加速窗口和/平方和
let mut integral = vec![0u64; (w as usize + 1) * (h as usize + 1)]; // 标准公式: I(y+1,x+1) = I(y,x+1) + I(y+1,x) - I(y,x) + pixel(y,x)
let mut sq_integral = vec![0u64; (w as usize + 1) * (h as usize + 1)]; let stride = w as usize + 1;
let mut integral = vec![0u64; stride * (h as usize + 1)];
let mut sq_integral = vec![0u64; stride * (h as usize + 1)];
for y in 0..h as usize { for y in 0..h as usize {
let mut row_sum = 0u64;
let mut row_sq = 0u64;
for x in 0..w as usize { for x in 0..w as usize {
let p = gray.get_pixel(x as u32, y as u32).0[0] as u64; let p = gray.get_pixel(x as u32, y as u32).0[0] as u64;
row_sum += p; let idx = (y + 1) * stride + (x + 1);
row_sq += p * p; let above = y * stride + (x + 1);
let idx = (y + 1) * (w as usize + 1) + (x + 1); let left = (y + 1) * stride + x;
let above = (y) * (w as usize + 1) + (x + 1); let above_left = y * stride + x;
let left = (y + 1) * (w as usize + 1) + (x); integral[idx] = integral[above] + integral[left] - integral[above_left] + p;
let above_left = (y) * (w as usize + 1) + (x); sq_integral[idx] = sq_integral[above] + sq_integral[left] - sq_integral[above_left] + p * p;
integral[idx] = integral[left] + integral[above] - integral[above_left] + row_sum;
sq_integral[idx] = sq_integral[left] + sq_integral[above] - sq_integral[above_left] + row_sq;
} }
} }
@@ -138,10 +136,10 @@ fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec<Vec<bool>> {
let y2 = (y + half + 1).min(h as i32) as usize; let y2 = (y + half + 1).min(h as i32) as usize;
let idx = |xx: usize, yy: usize| yy * (w as usize + 1) + xx; let idx = |xx: usize, yy: usize| yy * (w as usize + 1) + xx;
let count = ((x2 - x1) * (y2 - y1)) as f64; let count = ((x2 - x1) * (y2 - y1)) as f64;
let sum = (integral[idx(x2, y2)] - integral[idx(x1, y2)] - integral[idx(x2, y1)] let sum = (integral[idx(x2, y2)] as i64 - integral[idx(x1, y2)] as i64
+ integral[idx(x1, y1)]) as f64; - integral[idx(x2, y1)] as i64 + integral[idx(x1, y1)] as i64) as f64;
let sq_sum = (sq_integral[idx(x2, y2)] - sq_integral[idx(x1, y2)] - sq_integral[idx(x2, y1)] let sq_sum = (sq_integral[idx(x2, y2)] as i64 - sq_integral[idx(x1, y2)] as i64
+ sq_integral[idx(x1, y1)]) as f64; - sq_integral[idx(x2, y1)] as i64 + sq_integral[idx(x1, y1)] as i64) as f64;
let mean = sum / count; let mean = sum / count;
let variance = (sq_sum / count - mean * mean).max(0.0); let variance = (sq_sum / count - mean * mean).max(0.0);
(mean, variance.sqrt()) (mean, variance.sqrt())
+4 -4
View File
@@ -324,10 +324,10 @@ mod tests {
#[test] #[test]
fn test_unicode_to_shift_jis_known() { fn test_unicode_to_shift_jis_known() {
// 基本汉字应返回 Some // 基本汉字应返回 Someencoding_rs 精确映射)
assert!(unicode_to_shift_jis('中').is_some()); assert!(unicode_to_shift_jis('中').is_some(), "中 should map");
assert!(unicode_to_shift_jis('文').is_some()); assert!(unicode_to_shift_jis('文').is_some(), "文 should map");
assert!(unicode_to_shift_jis('').is_some()); assert!(unicode_to_shift_jis('').is_some(), "日 should map");
} }
#[test] #[test]
+3 -2
View File
@@ -305,13 +305,14 @@ mod tests {
#[test] #[test]
fn test_score_rule2() { fn test_score_rule2() {
let mut m = Matrix::new(3); let mut m = Matrix::new(3);
// 全部设为 dark → 4 个 2×2 同色方块
for y in 0..3u8 { for y in 0..3u8 {
for x in 0..3u8 { for x in 0..3u8 {
m.set(x, y, true); m.set(x, y, true);
} }
} }
let view = MaskedView::new(&m, 0); // 使用 raw 版(不经过掩码)验证
assert_eq!(score_rule2(&view), 4 * 3); assert_eq!(score_rule2_raw(&m), 4 * 3);
} }
#[test] #[test]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB