Compare commits

...

3 Commits

Author SHA1 Message Date
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
Serendipity 309c9429ea refactor: ZXing对齐 — 容差50pct + 几何验证 + end-based中心 + 废弃代码清理
- detect.rs: 容差从40%提升到50% (对齐ZXing的moduleSize/2)
- detect.rs: 中心点改用ZXing end-based公式 (浮点精度, end - r4 - r3 - r2/2)
- detect.rs: 新增validate_finder_geometry()几何验证
  module_size一致性<10% + 勾股定理偏差<15%
- detect.rs: compute_finder_deviation改用base=total/7
- detect.rs: 删除废弃的estimate_finder_size函数
- perspective.rs: count_finder_hits容差对齐 (base=total/7 + 50pct)
- web/main.rs + cli/main.rs: 修复Rust 1.96新版clippy规则
2026-06-21 23:01:46 +08:00
Serendipity a03ab95ce5 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元组
2026-06-21 22:19:00 +08:00
10 changed files with 136 additions and 90 deletions
+1 -1
View File
@@ -266,7 +266,7 @@ fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result<String> {
let pwd = opts
.password
.as_deref()
.or_else(|| env_pwd.as_deref())
.or(env_pwd.as_deref())
.unwrap_or("");
Ok(text_builder::build_wifi_text(
s,
+6 -5
View File
@@ -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);
}
}
}
+79 -52
View File
@@ -21,7 +21,7 @@ pub(crate) struct DetectResult {
}
/// 水平扫描查找 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 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));
}
// 找 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,19 @@ fn scan_row(gray: &[Vec<bool>], 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 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 cx = runs[i + 2].0 + runs[i + 2].1 / 2;
centers.push((cx, row));
// 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));
}
}
@@ -65,7 +66,7 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
}
/// 垂直扫描查找 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 mut runs: Vec<(usize, usize)> = Vec::new();
@@ -80,7 +81,7 @@ fn scan_col(gray: &[Vec<bool>], 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 +89,19 @@ fn scan_col(gray: &[Vec<bool>], 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 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 cy = runs[i + 2].0 + runs[i + 2].1 / 2;
centers.push((col, cy));
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));
}
}
@@ -142,7 +145,7 @@ pub(crate) fn estimate_version_from_tl_tr(tl: (f64, f64), tr: (f64, f64), module
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;
let ver = ((dist_modules as i32 - 10) / 4) as u8;
ver.clamp(1, 40)
}
@@ -154,18 +157,17 @@ fn find_finders(gray: &[Vec<bool>]) -> 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));
}
}
}
@@ -215,43 +217,66 @@ fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
// 排序:左上、右上、左下
finders.sort_by(|a, b| {
let da = a.cx * a.cx + a.cy * a.cy; // 到原点的距离
let da = a.cx * a.cx + a.cy * a.cy;
let db = b.cx * b.cx + b.cy * b.cy;
da.cmp(&db)
});
// 区分右上(X 最大)和左下(Y 最大)
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])
}
/// 估算定位图案大小(像素)
fn estimate_finder_size(gray: &[Vec<bool>], cx: usize, cy: usize) -> usize {
// 从中心点水平扫描连续暗像素
let mut left = cx;
while left > 0 {
if cy < gray.len() && left < gray[0].len() && gray[cy][left] {
left -= 1;
} else {
break;
/// 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 mut right = cx;
while right + 1 < gray[0].len() {
if cy < gray.len() && gray[cy][right] {
right += 1;
} else {
break;
}
}
right - left
let deviation = (c_len - hypotenuse).abs() / min;
deviation < 0.15
}
/// 从二值化图像中提取 QR 布尔矩阵
@@ -279,7 +304,9 @@ pub(crate) fn detect_and_extract(
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;
let ver = ((dist_modules as i32 - 14) / 4) as u8;
// 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;
@@ -360,7 +387,7 @@ fn check_quiet_zone(modules: &[Vec<bool>], size: usize) -> bool {
true
}
/// 计算定位图案比例偏差(与理想 1:1:3:1:1 的偏差,0=完美, 1=最大)
/// 计算定位图案比例偏差(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();
@@ -382,14 +409,14 @@ fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
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 {
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 = [avg, avg, 3.0 * avg, avg, avg];
let expected = [base, base, 3.0 * base, base, base];
let dev: f64 = (0..5)
.map(|j| (runs[j] as f64 - expected[j]).abs() / avg)
.map(|j| (runs[i + j] as f64 - expected[j]).abs() / base)
.sum::<f64>()
/ 5.0;
if dev < best_dev {
+2 -5
View File
@@ -32,11 +32,8 @@ fn extract_bits(matrix: &Matrix, total_codewords: usize) -> Vec<bool> {
}
col -= 2;
going_up = !going_up;
// 跳过垂直时序图案列(col 6
if col == 6 {
col -= 1;
}
// 垂直时序图案列(col 6)由 read_module 自动跳过保留区,
// 无需显式 skip,否则会导致列配对错位、行序反转
}
bits.truncate(target_bits);
+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 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<Vec<bool>> {
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())
+4 -3
View File
@@ -143,9 +143,10 @@ fn count_finder_hits(gray: &[Vec<bool>]) -> usize {
let (mut col,mut runs)=(0,vec![]);
while col<gray[row].len() { let c=gray[row][col]; let mut l=0; while col<gray[row].len()&&gray[row][col]==c{l+=1;col+=1;} runs.push(l); }
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 ck=|v:f64,e:f64|(v-e*avg).abs()<avg*0.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 ck=|v:f64,e:f64|(v-e*base).abs()<base*0.5;
if ck(runs[i]as f64,1.0)&&ck(runs[i+1]as f64,1.0)&&ck(runs[i+2]as f64,3.0)&&ck(runs[i+3]as f64,1.0)&&ck(runs[i+4]as f64,1.0){hits+=1;}
}
}
+4 -4
View File
@@ -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());
// 基本汉字应返回 Someencoding_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]
+3 -2
View File
@@ -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]
+22
View File
@@ -419,4 +419,26 @@ mod tests {
assert!(svg.contains("#FF0000"));
assert!(svg.contains("#0000FF"));
}
#[test]
fn test_roundtrip_png() {
let config = QrConfig {
level: EcLevel::M,
version: VersionMode::Auto,
margin: 4,
};
let qr = QrCode::encode("Hello World", config).unwrap();
// 直接从矩阵解码
let matrix: Vec<Vec<bool>> = qr.modules().to_vec();
let result = crate::decoder::decode_matrix(&matrix)
.expect("矩阵解码往返失败");
assert_eq!(result.text, "Hello World");
// PNG 往返
let png_bytes = qr.to_png_bytes(8, None).unwrap();
let result = crate::decoder::decode_image(&png_bytes)
.expect("PNG 解码往返失败");
assert_eq!(result.text, "Hello World");
}
}
+1 -2
View File
@@ -69,7 +69,7 @@ fn generate_qr_inner(text: &str, level_str: &str, margin: u8, size: u8, fmt: &st
if margin > 20 {
return (StatusCode::BAD_REQUEST, "边距过大(最大 20").into_response();
}
if size < 1 || size > 20 {
if !(1..=20).contains(&size) {
return (StatusCode::BAD_REQUEST, "模块大小需在 1-20 之间").into_response();
}
@@ -77,7 +77,6 @@ fn generate_qr_inner(text: &str, level_str: &str, margin: u8, size: u8, fmt: &st
level,
version: VersionMode::Auto,
margin,
..Default::default()
};
let qr = match QrCode::encode(text, config) {