From a03ab95ce5aa3a31a2987ad836c59a0edb28d029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Sun, 21 Jun 2026 22:19:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A7=A3=E7=A0=81=E5=99=A8=E6=A0=B8?= =?UTF-8?q?=E5=BF=83bug=E4=BF=AE=E5=A4=8D=20=E2=80=94=20=E5=AE=B9=E5=B7=AE?= =?UTF-8?q?=E8=AE=A1=E7=AE=97+=E7=A7=AF=E5=88=86=E5=9B=BE=E5=83=8F+Sauvola?= =?UTF-8?q?=E4=B8=8B=E6=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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元组 --- core/src/decoder/bch.rs | 11 +++++----- core/src/decoder/detect.rs | 40 ++++++++++++++++++------------------- core/src/decoder/image.rs | 30 +++++++++++++--------------- core/src/encoder/mode.rs | 8 ++++---- core/src/matrix/mask.rs | 5 +++-- liuhangyu.png | Bin 0 -> 19259 bytes 6 files changed, 47 insertions(+), 47 deletions(-) create mode 100644 liuhangyu.png diff --git a/core/src/decoder/bch.rs b/core/src/decoder/bch.rs index 4465127..c302ef4 100644 --- a/core/src/decoder/bch.rs +++ b/core/src/decoder/bch.rs @@ -118,7 +118,7 @@ mod tests { result.is_some(), "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!(mask, dec_mask, "mask mismatch"); } @@ -134,7 +134,8 @@ mod tests { result.is_some(), "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 mask in 0u8..8 { let original = encode_format_info(ec_bits, mask); - // 翻转每个比特,验证能纠错 for bit in 0..15 { let corrupted = original ^ (1 << bit); let result = decode_format_info(corrupted); 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!(mask, dec_mask); } @@ -164,7 +164,8 @@ mod tests { let corrupted = original ^ (1 << bit); let result = decode_version_info(corrupted); 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); } } } diff --git a/core/src/decoder/detect.rs b/core/src/decoder/detect.rs index 7fbc57a..657cf56 100644 --- a/core/src/decoder/detect.rs +++ b/core/src/decoder/detect.rs @@ -21,7 +21,7 @@ pub(crate) struct DetectResult { } /// 水平扫描查找 1:1:3:1:1 比例 -fn scan_row(gray: &[Vec], row: usize) -> Vec<(usize, usize)> { +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() }; @@ -37,8 +37,8 @@ fn scan_row(gray: &[Vec], row: usize) -> Vec<(usize, usize)> { runs.push((col - run_len, run_len)); } - // 找 5 连段符合 1:1:3:1:1 比例 - let mut centers: Vec<(usize, usize)> = Vec::new(); + // 找 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; @@ -46,18 +46,17 @@ fn scan_row(gray: &[Vec], row: usize) -> Vec<(usize, usize)> { let r3 = runs[i + 3].1 as f32; let r4 = runs[i + 4].1 as f32; - let avg = (r0 + r1 + r2 + r3 + r4) / 5.0; - if avg < 2.0 { + let total = r0 + r1 + r2 + r3 + r4; + let base = total / 7.0; + if base < 2.0 { continue; } - - // 检查比例容差 ±40% 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) { 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], row: usize) -> Vec<(usize, usize)> { } /// 垂直扫描查找 1:1:3:1:1 比例 -fn scan_col(gray: &[Vec], col: usize) -> Vec<(usize, usize)> { +fn scan_col(gray: &[Vec], col: usize) -> Vec<(usize, usize, usize)> { let height = gray.len(); let mut runs: Vec<(usize, usize)> = Vec::new(); @@ -80,7 +79,7 @@ fn scan_col(gray: &[Vec], col: usize) -> Vec<(usize, usize)> { 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) { let r0 = runs[i].1 as f32; let r1 = runs[i + 1].1 as f32; @@ -88,17 +87,19 @@ fn scan_col(gray: &[Vec], col: usize) -> Vec<(usize, usize)> { let r3 = runs[i + 3].1 as f32; let r4 = runs[i + 4].1 as f32; - let avg = (r0 + r1 + r2 + r3 + r4) / 5.0; - if avg < 2.0 { + let total = r0 + r1 + r2 + r3 + r4; + let base = total / 7.0; + if base < 2.0 { continue; } 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) { 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]) -> Option<[FinderMatch; 3]> { 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) in scan_row(gray, row) { + 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) + .any(|&(_, vy, _)| (vy as i32 - cy as i32).abs() < 5) { - let size = estimate_finder_size(gray, cx, cy); - h_centers.push((cx, cy, size)); + h_centers.push((cx, cy, total_size)); } } } diff --git a/core/src/decoder/image.rs b/core/src/decoder/image.rs index b21ceb1..12e9c1d 100644 --- a/core/src/decoder/image.rs +++ b/core/src/decoder/image.rs @@ -113,21 +113,19 @@ fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec> { let r = 128.0f64; // 预计算积分图像以加速窗口和/平方和 - let mut integral = vec![0u64; (w as usize + 1) * (h as usize + 1)]; - let mut sq_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 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 { - let mut row_sum = 0u64; - let mut row_sq = 0u64; for x in 0..w as usize { let p = gray.get_pixel(x as u32, y as u32).0[0] as u64; - row_sum += p; - row_sq += p * p; - let idx = (y + 1) * (w as usize + 1) + (x + 1); - let above = (y) * (w as usize + 1) + (x + 1); - let left = (y + 1) * (w as usize + 1) + (x); - let above_left = (y) * (w as usize + 1) + (x); - 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; + let idx = (y + 1) * stride + (x + 1); + let above = y * stride + (x + 1); + let left = (y + 1) * stride + x; + let above_left = y * stride + x; + integral[idx] = integral[above] + integral[left] - integral[above_left] + p; + sq_integral[idx] = sq_integral[above] + sq_integral[left] - sq_integral[above_left] + p * p; } } @@ -138,10 +136,10 @@ fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec> { let y2 = (y + half + 1).min(h as i32) as usize; let idx = |xx: usize, yy: usize| yy * (w as usize + 1) + xx; let count = ((x2 - x1) * (y2 - y1)) as f64; - let sum = (integral[idx(x2, y2)] - integral[idx(x1, y2)] - integral[idx(x2, y1)] - + integral[idx(x1, y1)]) as f64; - let sq_sum = (sq_integral[idx(x2, y2)] - sq_integral[idx(x1, y2)] - sq_integral[idx(x2, y1)] - + sq_integral[idx(x1, y1)]) as f64; + let sum = (integral[idx(x2, y2)] as i64 - integral[idx(x1, y2)] as i64 + - integral[idx(x2, y1)] as i64 + integral[idx(x1, y1)] as i64) as f64; + let sq_sum = (sq_integral[idx(x2, y2)] as i64 - sq_integral[idx(x1, y2)] as i64 + - sq_integral[idx(x2, y1)] as i64 + sq_integral[idx(x1, y1)] as i64) as f64; let mean = sum / count; let variance = (sq_sum / count - mean * mean).max(0.0); (mean, variance.sqrt()) diff --git a/core/src/encoder/mode.rs b/core/src/encoder/mode.rs index fe4ac65..3576a3e 100644 --- a/core/src/encoder/mode.rs +++ b/core/src/encoder/mode.rs @@ -324,10 +324,10 @@ mod tests { #[test] fn test_unicode_to_shift_jis_known() { - // 基本汉字应返回 Some - assert!(unicode_to_shift_jis('中').is_some()); - assert!(unicode_to_shift_jis('文').is_some()); - assert!(unicode_to_shift_jis('你').is_some()); + // 基本汉字应返回 Some(encoding_rs 精确映射) + assert!(unicode_to_shift_jis('中').is_some(), "中 should map"); + assert!(unicode_to_shift_jis('文').is_some(), "文 should map"); + assert!(unicode_to_shift_jis('日').is_some(), "日 should map"); } #[test] diff --git a/core/src/matrix/mask.rs b/core/src/matrix/mask.rs index 5b49542..79bf821 100644 --- a/core/src/matrix/mask.rs +++ b/core/src/matrix/mask.rs @@ -305,13 +305,14 @@ mod tests { #[test] fn test_score_rule2() { let mut m = Matrix::new(3); + // 全部设为 dark → 4 个 2×2 同色方块 for y in 0..3u8 { for x in 0..3u8 { m.set(x, y, true); } } - let view = MaskedView::new(&m, 0); - assert_eq!(score_rule2(&view), 4 * 3); + // 使用 raw 版(不经过掩码)验证 + assert_eq!(score_rule2_raw(&m), 4 * 3); } #[test] diff --git a/liuhangyu.png b/liuhangyu.png new file mode 100644 index 0000000000000000000000000000000000000000..7992a2ce696e1a0000302c6357234f103f92da38 GIT binary patch literal 19259 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 C2Y9;x literal 0 HcmV?d00001