From 309c9429ea5c214086f93541ef56bbfddaad30f2 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 23:01:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20ZXing=E5=AF=B9=E9=BD=90=20=E2=80=94?= =?UTF-8?q?=20=E5=AE=B9=E5=B7=AE50pct=20+=20=E5=87=A0=E4=BD=95=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=20+=20end-based=E4=B8=AD=E5=BF=83=20+=20=E5=BA=9F?= =?UTF-8?q?=E5=BC=83=E4=BB=A3=E7=A0=81=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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规则 --- cli/src/main.rs | 2 +- core/src/decoder/detect.rs | 94 ++++++++++++++++++++------------ core/src/decoder/perspective.rs | 7 ++- liuhangyu.png | Bin 19259 -> 0 bytes web/src/main.rs | 3 +- 5 files changed, 65 insertions(+), 41 deletions(-) delete mode 100644 liuhangyu.png diff --git a/cli/src/main.rs b/cli/src/main.rs index c0500e5..71d20ba 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -266,7 +266,7 @@ fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result { 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, diff --git a/core/src/decoder/detect.rs b/core/src/decoder/detect.rs index 657cf56..0fc1c62 100644 --- a/core/src/decoder/detect.rs +++ b/core/src/decoder/detect.rs @@ -51,12 +51,14 @@ fn scan_row(gray: &[Vec], row: usize) -> Vec<(usize, usize, usize)> { if base < 2.0 { continue; } - let tolerance = 0.4; + 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, total as usize)); + // 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)); } } @@ -93,13 +95,13 @@ fn scan_col(gray: &[Vec], col: usize) -> Vec<(usize, usize, usize)> { continue; } - let tolerance = 0.4; + 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; - let total_px = total as usize; - centers.push((col, cy, total_px)); + 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)); } } @@ -215,43 +217,65 @@ fn find_finders(gray: &[Vec]) -> 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); } let f0 = finders.remove(0); - let f1 = finders.remove(0); - let f2 = finders.remove(0); + let f1 = finders.remove(1); + let f2 = finders.remove(2); + + // ZXing 几何验证:module_size 一致性 + 勾股定理 + if !validate_finder_geometry(&[&f0, &f1, &f2]) { + return None; + } Some([f0, f1, f2]) } -/// 估算定位图案大小(像素) -fn estimate_finder_size(gray: &[Vec], 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; } - let mut right = cx; - while right + 1 < gray[0].len() { - if cy < gray.len() && gray[cy][right] { - right += 1; - } else { - break; - } + + // 勾股定理验证: |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; // 太小,跳过检查 } - right - left + let deviation = (c_len - hypotenuse).abs() / min; + + deviation < 0.15 } /// 从二值化图像中提取 QR 布尔矩阵 @@ -360,7 +384,7 @@ fn check_quiet_zone(modules: &[Vec], size: usize) -> bool { true } -/// 计算定位图案比例偏差(与理想 1:1:3:1:1 的偏差,0=完美, 1=最大) +/// 计算定位图案比例偏差(0=完美, 1=最大),使用 base=total/7 对齐 ZXing fn compute_finder_deviation(gray: &[Vec], finder: &FinderMatch) -> f64 { let row = finder.cy; let mut runs: Vec = Vec::new(); @@ -382,14 +406,14 @@ fn compute_finder_deviation(gray: &[Vec], 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::() / 5.0; if dev < best_dev { diff --git a/core/src/decoder/perspective.rs b/core/src/decoder/perspective.rs index 722a48a..848231f 100644 --- a/core/src/decoder/perspective.rs +++ b/core/src/decoder/perspective.rs @@ -143,9 +143,10 @@ fn count_finder_hits(gray: &[Vec]) -> usize { let (mut col,mut runs)=(0,vec![]); while colm;zWaI4*?X_G_B#8{wpBehOuh7rmv(e?OzmE=>>C{&7d*{Bmt2Hbe*5CoH60x{ zUC_O3=`Htt=KbGa_*Cb&9)JC0b?*m{yz%z0e|XcK`+7$X_^s<+Gcxw$6-OR^Zg|S9 zZPQ<0ICp#J?N`*^)_!ea^+Y#+UajwY@%ZPSA0FD;Ilja`0xz~^cfUOE!2RnwPqudT zj$e6M*?c+Lx^`sj=Gog?)3Atlk3GEe;Lz6I+pmaQI`L)u$-Vn|&sY`LJLS%SYPj>{ z;F}Ap8QO|>kF~#k`P}Wi&OZe2X-(@Mt6gWGFh1vbZo-+F+yA;}@4nw0_rJ#9yXwXG zny*&ZU6Ox@uUemfsP^uC9}R9f&&#i=-s(Qm)!#e5!2aQVZr(63eK~I0R>kn`JQ$I% z$h1eX>h~3#`S96ciyC6gPpB;zTZ5lqw^rYF`hvoEc63km%|6~6YX;H74l5H; zhI|<@WAPy-$iML=ohMP3L;aD7Sg=BMaN2OA3S55ryRIv%2gZUwoKwq<1rR7FjD-z{J=y74K-i@CVVHA#LN@ekQSfhk}G8rKGQ}C{6uapC^qDc1w8-Cvu8ykrgn zDuUUQ3Wr2#Eii2=NQwTUxvC(frJ`No0JB;{(D?u1`=b2Vb7I2XgJjz`IU=%{H}9I=d?C=OFFJ~I5M ztO%uS<~JBp`AYb{WCWlC_9RB!8!Vf|fhr>q4nU0U$xUl@XX&y{o`40~@-fH6gqu~P z#10}uF`OVrb-*PVv4qmEdXgGM-D;yir1m14SAp_m^kf^U+*}0Idg>iD`VoFo;7Rl{ zpkPk6)w@&AGFOHcz^M|HCh6cys4KPE|%}CtST`_ivzC~rb zcl`Z&|I=mp!Qjq7&PYvTCx7z0At?1*-a-m9u$K>vQbNH#0-W{ldVF0 zimOXib8FTo^pl^mCBLKh)X>i(vEUY&CNj5+l$m4$jJk~`uy$`c#kM+?q|>YDG8aON z5G1?5(S^brqf5K(Uixdm`+B!d%KdF&@a z3`@U5?(vQwc4c@9D|t+jWRn*l)StC!VT$Ix3Rw}sR;$NEj;7CQM}2{R+Qe&+>#f2S z4#;Y+XhxYt%gF0ELM=hOR!?kKP1Xz6cgi7yz6DRL0x}fyb1Y@N4UJRzGhz2Fp!t&& z_9JgDh=;z(fl!rO5<9r_o&Es;c7)MgH#m;o>|{~`72yz_1Pw@Fr&Ri2c>T!OB0JH} zWr##m?h$JdQ@Pi==g>_jBWuZ0hws> zD$w`4Yvpi_!sEHey&87Vkyp-;{1FZK;{K54tlpbF^On+69V$S=vG$vZ96RqVQ9fyQ zEuvu#EUB5F=XmQiJ1>){UN2y9W<$;z74Dl4{M9IRjE6TCdhd zFw-}bUv6cg>a9hva>>WEOozJNoMC}mQTP<-s|SXwZ>Q#UiMLaNSco*t?Fip+#Lzgt zbf4P2gL7o3wZugBR5Z-3$^_A*SUSc>jw|WV15;MmauZ`AH5i9-wU>u9i{QS=0 zszGsG`S{aqY{-tUlx-{|J17bPLrPLVA9yw7Vw425cd+@;fOyms$6>lzVyg%yOU&$kI3<7FUJ2#e8k_;mp&Y8h z0rt)@BIptD*&m(U=D@sDH>eiZ5}aE=b*R&N^b2-8dqRdzYEw|qM6lAH#{S;%={7}> zS+i{&o+GF+Q&@5?fhZ)FV5n}2HAOkJAQ|tF@DWOsEF^O+GrL*sg?VP_(LK{|G#!Xc z^*Tk-y4o26Lhd#n(%Tv>#yqU>#+ka%hvZVwNMxoP2+=%LTg%et(xa_wMV{8$Q}L5H z(!wAnLK1uL4+ATWe;^JXDES^7NKDQRg9%ZQ2{b-f@8M{TZB4uQM9^ynhdRx3_b~%e z7u+a>3DuhILMbOKwduD)H~NRrv6E~t#I{ZJd=|LN2yo}1w5MSs&14u&>l&a;@SEa% z*XL!aBL{VuBJ8xRRUet4P3%O4T^5gZ7p6vJf?N(gGIDMU?hPqgSF7;{a1er=+z zsI@i`Wjp%B8KN5?el(Hcue4)bSpx)HZQfs&X1`SaV`neib3C_V*83H5_ zZ?<{>fU0=k-n!NbkdUIT)o4_5*E<_CSELzP%nBsQi505hl-(bf@opxIbi5%7?UAgm z(!Ic{QTb2~F}W?{3+Okq$CkSuRHx?S8~uWvqGb_MAC3<`_Jb$W(bc1+H5IxqM_L4s zaKsTBJP!jnsWYZc3sKvLMhH@-hblMC5Wfjm;8$yl&NY@N39DhQPV+e~Einy^=fst1 z=4g|gR9~b6Hh?L=fn{8GTi6V$v!|#OGuYuNG)8%HlE0FgaZ;$)BrRk0W~n-P*zCY0 zjTlIjh88qj?KWg;5Qo_HaoWLb9q1`1x|wCiFKimn>rVp7Wrt_89cd?v7`2S>YG=H4 zwJykPC?6wZv2&knMPBxCgj=yJj=iyhUuf>%^oO%^i!r$B#6a=l$JB_OupT zZ^H`Bx$2e$X*XG~ya4$zS&e`cB0d>64Mb-V>s7%IaWms5-c;q@q9=~MlF!m zfbT(K?OYHlan-SxMfpLwqXH{MnOd^JB=#s4J2&-zIfl#@zb4w_+(huy0qMiyXla(Y zmI$P^TBQ_LTR0)8mYb#T^Zy8kp6}(;OwKq+Wez1F)7^ujSrJdA`AZVXNO!lOcqzCD z_3epIpmz{2e8wlA)lITSl$={9EC*2)IEUg;8&n(0#WZCaGU7r}Ke;=Krb$s1jAtvd z_^1k=tZb-6NXy*RI_OGsfO&HfyB)GNhBJIqT(Z?Q8IzYZmo+EBT>q1p@W$BT^1~W<{_09vmM@T>Xm#dsXvzFTFPB@8_sb+7SoT-EI;PkM>mSiE0*I-OI%80 z1a#%ORfYh7oez!MS6Fh207NUbf%CtT3>mTrF3 zY?^@~$KVjEjM%IeC&ZKT8$n|r7hOF`&sJ(p4l$wrM6GhpvQp&Q5e@#Myff$xSudVt zu7-+ovoXXI=Ls3a;>RKOdk7k0AL|sOx{4t!a!k(G9fe2yPuYuHG=#!00f}Z$#*Keg zxNV;6$$i&Wm45JY)#%zk$D@SK01TwyA0=v-5)Zezkn^?(4N|Dth%Gh&&77mMRv#Q3 znkv_Ui>FC=Aez62MZz>jX7aS3Act5R;1hl++q{PQ;d}IpVbsC08#>{KvnGeZKQ4^h z(KSxOF>UkrtXx!f(xL1le4p|_`~URKpSj`hCsyv9x%9bT;+JA|bYI`IY~R=Jc<7(s C2Y9;x diff --git a/web/src/main.rs b/web/src/main.rs index 226fad4..40bc8de 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -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) {