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规则
This commit is contained in:
2026-06-21 23:01:46 +08:00
parent a03ab95ce5
commit 309c9429ea
5 changed files with 65 additions and 41 deletions
+1 -1
View File
@@ -266,7 +266,7 @@ fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result<String> {
let pwd = opts let pwd = opts
.password .password
.as_deref() .as_deref()
.or_else(|| env_pwd.as_deref()) .or(env_pwd.as_deref())
.unwrap_or(""); .unwrap_or("");
Ok(text_builder::build_wifi_text( Ok(text_builder::build_wifi_text(
s, s,
+59 -35
View File
@@ -51,12 +51,14 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize, usize)> {
if base < 2.0 { if base < 2.0 {
continue; continue;
} }
let tolerance = 0.4; let tolerance = 0.5; // ZXing 标准:moduleSize/2 容差
let check = |v: f32, expected: f32| (v - expected * base).abs() < base * 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; // ZXing 的 end-based 中心公式: end - r4 - r3 - r2/2 (浮点精度更高)
centers.push((cx, row, total as usize)); 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));
} }
} }
@@ -93,13 +95,13 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize, usize)> {
continue; continue;
} }
let tolerance = 0.4; let tolerance = 0.5; // ZXing 标准:moduleSize/2 容差
let check = |v: f32, expected: f32| (v - expected * base).abs() < base * 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 end = runs[i + 4].0 + runs[i + 4].1;
let total_px = total as usize; let cy = end as f32 - r4 - r3 - r2 / 2.0;
centers.push((col, cy, total_px)); centers.push((col, cy as usize, total as usize));
} }
} }
@@ -215,43 +217,65 @@ fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
// 排序:左上、右上、左下 // 排序:左上、右上、左下
finders.sort_by(|a, b| { 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; let db = b.cx * b.cx + b.cy * b.cy;
da.cmp(&db) da.cmp(&db)
}); });
// 区分右上(X 最大)和左下(Y 最大)
if finders[1].cx < finders[2].cx { if finders[1].cx < finders[2].cx {
finders.swap(1, 2); finders.swap(1, 2);
} }
let f0 = finders.remove(0); let f0 = finders.remove(0);
let f1 = finders.remove(0); let f1 = finders.remove(1);
let f2 = finders.remove(0); let f2 = finders.remove(2);
// ZXing 几何验证:module_size 一致性 + 勾股定理
if !validate_finder_geometry(&[&f0, &f1, &f2]) {
return None;
}
Some([f0, f1, f2]) Some([f0, f1, f2])
} }
/// 估算定位图案大小(像素) /// ZXing 风格几何验证:3 个定位图案的 module_size 必须一致且构成近似直角三角形
fn estimate_finder_size(gray: &[Vec<bool>], cx: usize, cy: usize) -> usize { fn validate_finder_geometry(finders: &[&FinderMatch; 3]) -> bool {
// 从中心点水平扫描连续暗像素 let s0 = finders[0].size as f64;
let mut left = cx; let s1 = finders[1].size as f64;
while left > 0 { let s2 = finders[2].size as f64;
if cy < gray.len() && left < gray[0].len() && gray[cy][left] {
left -= 1; // module_size 一致性: 最大偏差 < 10%
} else { let avg_size = (s0 + s1 + s2) / 3.0;
break; 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;
} }
let mut right = cx;
while right + 1 < gray[0].len() { // 勾股定理验证: |C - sqrt(A² + B²)| / min(C, sqrt(A² + B²)) < 15%
if cy < gray.len() && gray[cy][right] { let ax = finders[1].cx as f64 - finders[0].cx as f64;
right += 1; let ay = finders[1].cy as f64 - finders[0].cy as f64;
} else { let bx = finders[2].cx as f64 - finders[0].cx as f64;
break; 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; // 太小,跳过检查
} }
right - left let deviation = (c_len - hypotenuse).abs() / min;
deviation < 0.15
} }
/// 从二值化图像中提取 QR 布尔矩阵 /// 从二值化图像中提取 QR 布尔矩阵
@@ -360,7 +384,7 @@ fn check_quiet_zone(modules: &[Vec<bool>], size: usize) -> bool {
true true
} }
/// 计算定位图案比例偏差(与理想 1:1:3:1:1 的偏差,0=完美, 1=最大) /// 计算定位图案比例偏差(0=完美, 1=最大),使用 base=total/7 对齐 ZXing
fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 { fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
let row = finder.cy; let row = finder.cy;
let mut runs: Vec<usize> = Vec::new(); let mut runs: Vec<usize> = Vec::new();
@@ -382,14 +406,14 @@ fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
let mut best_dev = 1.0f64; let mut best_dev = 1.0f64;
for i in 0..runs.len().saturating_sub(4) { for i in 0..runs.len().saturating_sub(4) {
let avg = let total = (runs[i] + runs[i + 1] + runs[i + 2] + runs[i + 3] + runs[i + 4]) as f64;
(runs[i] + runs[i + 1] + runs[i + 2] + runs[i + 3] + runs[i + 4]) as f64 / 5.0; let base = total / 7.0;
if avg < 2.0 { if base < 2.0 {
continue; continue;
} }
let expected = [avg, avg, 3.0 * avg, avg, avg]; let expected = [base, base, 3.0 * base, base, base];
let dev: f64 = (0..5) 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>() .sum::<f64>()
/ 5.0; / 5.0;
if dev < best_dev { if dev < best_dev {
+4 -3
View File
@@ -143,9 +143,10 @@ fn count_finder_hits(gray: &[Vec<bool>]) -> usize {
let (mut col,mut runs)=(0,vec![]); 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); } 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) { 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; let total=(runs[i]+runs[i+1]+runs[i+2]+runs[i+3]+runs[i+4])as f64;
if avg<2.0{continue;} let base=total/7.0;
let ck=|v:f64,e:f64|(v-e*avg).abs()<avg*0.4; 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;} 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;}
} }
} }
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

+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 { if margin > 20 {
return (StatusCode::BAD_REQUEST, "边距过大(最大 20").into_response(); 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(); 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, level,
version: VersionMode::Auto, version: VersionMode::Auto,
margin, margin,
..Default::default()
}; };
let qr = match QrCode::encode(text, config) { let qr = match QrCode::encode(text, config) {