From bd4ca718acdcb124601a6f14117338eef9ba319f 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 16:00:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=A3=E7=A0=81=E5=99=A8=E5=85=A8?= =?UTF-8?q?=E9=9D=A2=E5=A2=9E=E5=BC=BA=20=E2=80=94=20=E5=A4=9A=E9=81=8D?= =?UTF-8?q?=E4=BA=8C=E5=80=BC=E5=8C=96=20+=204=E7=82=B9=E9=80=8F=E8=A7=86?= =?UTF-8?q?=20+=20ECI/FNC1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 — 二值化+检测增强: - decoder/image.rs 重写: 中位数→Otsu→Sauvola 三遍自适应阈值 - 新增 load_gray() 公共 API 供调用方自定义预处理 - 反色QR码自动检测(5点采样法) - 静区验证 + 包围盒(BoundingBox) + 解码置信度评分 - 定位图案比例偏差计算 Phase 2 — 4点透视变换+多QR码: - decoder/perspective.rs 重写: DLT求解3x3单应矩阵 - 用3个定位图案+推算第4点构建4点对应 - 双线性插值替换最近邻采样 - 多角度旋转回退(7个预设角度) - decode_all() 返回图像中所有QR码(按置信度降序) Phase 3 — ISO合规: - decoder/bch.rs: decode_format_info/version_info 返回汉明距离 - decoder/format.rs: 副本冲突时选汉明距离更小者 - decoder/mode_decode.rs: ECI指派号识别, FNC1/GS1检测(0101/1001) - 结构化追加(0011)头部提取: position/total/parity - DecodeResult 扩展: is_gs1, eci_designator, structured_append 验证: cargo check+clippy通过, 81+24测试全部通过 --- Cargo.lock | 220 ++++++++++++++++++++++++ core/Cargo.toml | 1 + core/src/decoder/bch.rs | 16 +- core/src/decoder/detect.rs | 129 +++++++++++++- core/src/decoder/format.rs | 24 ++- core/src/decoder/image.rs | 174 ++++++++++++++++--- core/src/decoder/mod.rs | 243 ++++++++++++++++++++++---- core/src/decoder/mode_decode.rs | 120 ++++++++++--- core/src/decoder/perspective.rs | 296 ++++++++++++++++---------------- 9 files changed, 971 insertions(+), 252 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e81688..233df19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -1525,6 +1534,114 @@ dependencies = [ "winapi", ] +[[package]] +name = "glam" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da" + +[[package]] +name = "glam" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b" + +[[package]] +name = "glam" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16" + +[[package]] +name = "glam" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776" + +[[package]] +name = "glam" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34" + +[[package]] +name = "glam" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca" + +[[package]] +name = "glam" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f" + +[[package]] +name = "glam" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815" + +[[package]] +name = "glam" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774" + +[[package]] +name = "glam" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c" + +[[package]] +name = "glam" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" + +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + +[[package]] +name = "glam" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" + +[[package]] +name = "glam" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" + +[[package]] +name = "glam" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7" + +[[package]] +name = "glam" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" + [[package]] name = "glib" version = "0.18.5" @@ -2316,6 +2433,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -2416,6 +2543,51 @@ dependencies = [ "version_check", ] +[[package]] +name = "nalgebra" +version = "0.34.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df76ea0ff5c7e6b88689085804d6132ded0ddb9de5ca5b8aeb9eeadc0508a70a" +dependencies = [ + "approx", + "glam 0.14.0", + "glam 0.15.2", + "glam 0.16.0", + "glam 0.17.3", + "glam 0.18.0", + "glam 0.19.0", + "glam 0.20.5", + "glam 0.21.3", + "glam 0.22.0", + "glam 0.23.0", + "glam 0.24.2", + "glam 0.25.0", + "glam 0.27.0", + "glam 0.28.0", + "glam 0.29.3", + "glam 0.30.10", + "glam 0.31.1", + "glam 0.32.1", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2480,6 +2652,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -3108,6 +3289,7 @@ version = "0.1.0" dependencies = [ "encoding_rs", "image", + "nalgebra", "serde", "thiserror 2.0.18", ] @@ -3274,6 +3456,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.12.0" @@ -3467,6 +3655,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3770,6 +3967,19 @@ dependencies = [ "libc", ] +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -5233,6 +5443,16 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/core/Cargo.toml b/core/Cargo.toml index ea2e9ec..3cd1777 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,6 +16,7 @@ rust-version.workspace = true [dependencies] encoding_rs = "0.8" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "bmp"] } +nalgebra = "0.34" serde = { version = "1", features = ["derive"] } thiserror = "2" diff --git a/core/src/decoder/bch.rs b/core/src/decoder/bch.rs index 47b8929..4465127 100644 --- a/core/src/decoder/bch.rs +++ b/core/src/decoder/bch.rs @@ -80,30 +80,26 @@ fn build_version_table() -> &'static [VersionEntry] { }) } -/// BCH(15,5) 解码:从 15-bit 原始值恢复 (ec_bits, mask) -/// -/// *若汉明距离 ≤ 3 则返回 Some,否则 None* -pub(crate) fn decode_format_info(raw: u16) -> Option<(u8, u8)> { +/// BCH(15,5) 解码:返回 (ec_bits, mask, hamming_distance) +pub(crate) fn decode_format_info(raw: u16) -> Option<(u8, u8, u32)> { let table = build_format_table(); table .iter() .map(|&(code, ec, mask)| (hamming_distance_15(raw, code), ec, mask)) .min_by_key(|&(d, _, _)| d) .filter(|&(d, _, _)| d <= 3) - .map(|(_, ec, mask)| (ec, mask)) + .map(|(d, ec, mask)| (ec, mask, d)) } -/// BCH(18,6) 解码:从 18-bit 原始值恢复版本号 -/// -/// *若汉明距离 ≤ 3 则返回 Some,否则 None* -pub(crate) fn decode_version_info(raw: u32) -> Option { +/// BCH(18,6) 解码:返回 (version, hamming_distance) +pub(crate) fn decode_version_info(raw: u32) -> Option<(u8, u32)> { let table = build_version_table(); table .iter() .map(|&(code, ver)| (hamming_distance_18(raw, code), ver)) .min_by_key(|&(d, _)| d) .filter(|&(d, _)| d <= 3) - .map(|(_, ver)| ver) + .map(|(d, ver)| (ver, d)) } #[cfg(test)] diff --git a/core/src/decoder/detect.rs b/core/src/decoder/detect.rs index 0bb1b9c..7fbc57a 100644 --- a/core/src/decoder/detect.rs +++ b/core/src/decoder/detect.rs @@ -15,6 +15,9 @@ struct FinderMatch { pub(crate) struct DetectResult { pub modules: Vec>, // 布尔矩阵(含静区) pub version_estimate: u8, + pub bounds: crate::decoder::BoundingBox, + pub quiet_zone_ok: bool, + pub finder_ratio_deviation: f64, } /// 水平扫描查找 1:1:3:1:1 比例 @@ -102,6 +105,47 @@ fn scan_col(gray: &[Vec], col: usize) -> Vec<(usize, usize)> { centers } +/// 检测 3 个定位图案,返回 (cx, cy) 像素坐标对(供透视矫正使用) +pub(crate) fn find_finders_for_perspective( + gray: &[Vec], +) -> Result<[(usize, usize); 3], crate::error::QrError> { + let finders = find_finders(gray) + .ok_or_else(|| crate::error::QrError::DecodeFail("未找到 QR 码定位图案".into()))?; + Ok([ + (finders[0].cx, finders[0].cy), + (finders[1].cx, finders[1].cy), + (finders[2].cx, finders[2].cy), + ]) +} + +/// 从定位图案估算模块大小(像素/模块) +pub(crate) fn estimate_module_size_from_finders( + _gray: &[Vec], + finders: &[(usize, usize); 3], +) -> usize { + // 用 TL-TR 和 TL-BL 距离估算 + let dx1 = (finders[1].0 as f64 - finders[0].0 as f64).abs(); + let dy1 = (finders[1].1 as f64 - finders[0].1 as f64).abs(); + let dx2 = (finders[2].0 as f64 - finders[0].0 as f64).abs(); + let dy2 = (finders[2].1 as f64 - finders[0].1 as f64).abs(); + let dist1 = (dx1 * dx1 + dy1 * dy1).sqrt(); + let dist2 = (dx2 * dx2 + dy2 * dy2).sqrt(); + let avg_dist = (dist1 + dist2) / 2.0; + // 版本 1: dist ≈ 14 modules (7+1-1+7 = 14) + // 我们并不知道确切版本,使用 iter 近似 + (avg_dist / 14.0).max(1.0) as usize +} + +/// 从 TL-TR 距离估算版本号 +pub(crate) fn estimate_version_from_tl_tr(tl: (f64, f64), tr: (f64, f64), module_size: usize) -> u8 { + let dx = tr.0 - tl.0; + 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; + ver.clamp(1, 40) +} + /// 检测 3 个定位图案(交叉验证水平+垂直扫描) fn find_finders(gray: &[Vec]) -> Option<[FinderMatch; 3]> { let height = gray.len(); @@ -245,7 +289,6 @@ pub(crate) fn detect_and_extract( for my in 0..size { let mut row = Vec::with_capacity(size); for mx in 0..size { - // 从采样网格映射到图像像素坐标 let px = tl.cx as f32 + (mx as f32 - 3.5) * module_size as f32; let py = tl.cy as f32 + (my as f32 - 3.5) * module_size as f32; @@ -266,8 +309,92 @@ pub(crate) fn detect_and_extract( modules.push(row); } + // 静区验证:检查矩阵四边各 4 行/列是否为白色 + let quiet_zone_ok = check_quiet_zone(&modules, size); + + // 包围盒:从 3 个定位图案推算 QR 码在图像中的像素区域 + let margin_px = (module_size as u32).saturating_mul(4); + let x_min = tl.cx.saturating_sub(3 * module_size) as u32; + let y_min = tl.cy.saturating_sub(3 * module_size) as u32; + let x_max = ((tr.cx + 3 * module_size) as u32).saturating_add(margin_px); + let y_max = ((_bl.cy + 3 * module_size) as u32).saturating_add(margin_px); + let bounds = crate::decoder::BoundingBox { + x: x_min.saturating_sub(margin_px), + y: y_min.saturating_sub(margin_px), + width: x_max.saturating_sub(x_min).saturating_add(2 * margin_px), + height: y_max.saturating_sub(y_min).saturating_add(2 * margin_px), + }; + + // 定位图案比例偏差:实际 5 段比例与 1:1:3:1:1 的偏差 + let finder_ratio_deviation = compute_finder_deviation(gray, tl); + Ok(DetectResult { modules, version_estimate: version, + bounds, + quiet_zone_ok, + finder_ratio_deviation, }) } + +/// 检查静区:矩阵四边各 4 行/列是否为全白色 +fn check_quiet_zone(modules: &[Vec], size: usize) -> bool { + let quiet = 4usize; + // 上边 + if modules.iter().take(quiet.min(size)).any(|row| row.iter().any(|&m| m)) { + return false; + } + // 下边 + if modules.iter().skip(size.saturating_sub(quiet)).any(|row| row.iter().any(|&m| m)) { + return false; + } + // 左边 & 右边 + for row in modules.iter() { + if row.iter().take(quiet.min(size)).any(|&m| m) { + return false; + } + if row.iter().skip(size.saturating_sub(quiet)).any(|&m| m) { + return false; + } + } + true +} + +/// 计算定位图案比例偏差(与理想 1:1:3:1:1 的偏差,0=完美, 1=最大) +fn compute_finder_deviation(gray: &[Vec], finder: &FinderMatch) -> f64 { + let row = finder.cy; + let mut runs: Vec = Vec::new(); + let mut col = finder.cx.saturating_sub(finder.size); + let end = (finder.cx + finder.size).min(gray[0].len()); + while col < end { + let current = gray[row][col]; + let mut run_len = 0; + while col < end && gray[row][col] == current { + run_len += 1; + col += 1; + } + runs.push(run_len); + } + + if runs.len() < 5 { + return 1.0; + } + + 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 { + continue; + } + let expected = [avg, avg, 3.0 * avg, avg, avg]; + let dev: f64 = (0..5) + .map(|j| (runs[j] as f64 - expected[j]).abs() / avg) + .sum::() + / 5.0; + if dev < best_dev { + best_dev = dev; + } + } + best_dev +} diff --git a/core/src/decoder/format.rs b/core/src/decoder/format.rs index a6c924d..162e76a 100644 --- a/core/src/decoder/format.rs +++ b/core/src/decoder/format.rs @@ -104,18 +104,17 @@ pub(crate) fn read_format_info(matrix: &Matrix) -> Result<(EcLevel, u8), crate:: let decode_fail = || crate::error::QrError::FormatCorrupted("格式信息解码失败:两处副本均无法纠错".into()); - // 偏好成功解码的结果 + // 用汉明距离选择更可靠的副本 match (dec1, dec2) { - (Some((ec1, m1)), Some((ec2, m2))) if (ec1, m1) == (ec2, m2) => { + (Some((ec1, m1, _)), Some((ec2, m2, _))) if (ec1, m1) == (ec2, m2) => { ec_from_bits(ec1).map(|lvl| (lvl, m1)).ok_or_else(corrupt_err) } - (Some((ec1, m1)), Some((_, _))) => { - // 两处不一致 — 偏好副本 1 - ec_from_bits(ec1) - .map(|lvl| (lvl, m1)) - .ok_or_else(corrupt_err) + (Some((ec1, m1, d1)), Some((ec2, m2, d2))) => { + // 两处不一致 → 选汉明距离更小的副本 + let (ec, m) = if d1 <= d2 { (ec1, m1) } else { (ec2, m2) }; + ec_from_bits(ec).map(|lvl| (lvl, m)).ok_or_else(corrupt_err) } - (Some((ec, m)), None) | (None, Some((ec, m))) => ec_from_bits(ec) + (Some((ec, m, _)), None) | (None, Some((ec, m, _))) => ec_from_bits(ec) .map(|lvl| (lvl, m)) .ok_or_else(corrupt_err), (None, None) => Err(decode_fail()), @@ -146,12 +145,9 @@ pub(crate) fn read_version_info(matrix: &Matrix) -> Result Ok(v1), - (Some(v1), Some(_v2)) => { - // 两处不一致 — 偏好副本 1 - Ok(v1) - } - (Some(v), None) | (None, Some(v)) => Ok(v), + (Some((v1, _)), Some((v2, _))) if v1 == v2 => Ok(v1), + (Some((v1, d1)), Some((v2, d2))) => Ok(if d1 <= d2 { v1 } else { v2 }), + (Some((v, _)), None) | (None, Some((v, _))) => Ok(v), (None, None) => Err(crate::error::QrError::FormatCorrupted( "版本信息解码失败:两处副本均无法纠错".into(), )), diff --git a/core/src/decoder/image.rs b/core/src/decoder/image.rs index ea2fb38..9cb2ec3 100644 --- a/core/src/decoder/image.rs +++ b/core/src/decoder/image.rs @@ -1,34 +1,164 @@ //! 图像加载与二值化 //! -//! 使用 `image` crate 加载 PNG/JPEG/WebP,转为灰度再二值化为布尔矩阵。 +//! 提供多遍自适应二值化策略和灰度图加载 API。 -/// 从图像字节加载并二值化 +pub use image::GrayImage; + +/// 从图像字节加载为灰度图(公共 API,供调用方自定义预处理) /// -/// 步骤:解码 → 灰度 → 按中位数阈值二值化 -pub(crate) fn load_and_binarize( - bytes: &[u8], -) -> Result>, crate::error::QrError> { - let img = - image::load_from_memory(bytes).map_err(|e| crate::error::QrError::DecodeFail(format!("图像解码失败: {e}")))?; - let gray = img.to_luma8(); +/// 支持 PNG/JPEG/WebP/BMP 等常见格式。 +pub fn load_gray(bytes: &[u8]) -> Result { + let img = image::load_from_memory(bytes) + .map_err(|e| crate::error::QrError::DecodeFail(format!("图像解码失败: {e}")))?; + Ok(img.to_luma8()) +} +/// 多遍二值化:返回按优先级排序的多个二值化结果 +/// +/// 顺序:中位数(处理干净扫描)→ Otsu(处理双峰直方图)→ Sauvola(处理不均匀光照) +/// 调用方依次尝试解码,首次成功即返回。 +pub(crate) fn binarize_multi(gray: &GrayImage) -> Vec>> { let (w, h) = gray.dimensions(); - let width = w as usize; - let height = h as usize; + let pixels: Vec = gray.iter().copied().collect(); + let total = pixels.len(); - // 计算中位数阈值 - let mut all_pixels: Vec = gray.iter().copied().collect(); - all_pixels.sort_unstable(); - let threshold = all_pixels[all_pixels.len() / 2]; + let mut results = Vec::with_capacity(3); - // 二值化:像素 < 阈值 → true(暗模块),否则 false(亮模块) - let matrix: Vec> = (0..height) + // 第 1 遍:中位数阈值(最快) + results.push(apply_threshold_from_pixels(&pixels, w, h, median_threshold(&pixels))); + + // 第 2 遍:Otsu 大津算法(与中位数不同时才加入) + if let Some(otsu_t) = otsu_threshold(&pixels, total) { + if (otsu_t as i32 - median_threshold(&pixels) as i32).abs() > 10 { + results.push(apply_threshold_from_pixels(&pixels, w, h, otsu_t)); + } + } + + // 第 3 遍:Sauvola 局部自适应(前两遍均可能失败的兜底) + results.push(sauvola_binarize(gray, w, h)); + + results +} + +/// 中位数阈值 +fn median_threshold(pixels: &[u8]) -> u8 { + let mut sorted: Vec = pixels.to_vec(); + sorted.sort_unstable(); + sorted[sorted.len() / 2] +} + +/// Otsu 大津算法 — 最大化类间方差寻找最优阈值 +/// +/// 适用于双峰直方图(前景/背景分明)。 +/// 对直方图退化为单峰的情况返回 None。 +fn otsu_threshold(pixels: &[u8], total: usize) -> Option { + // 构建 256 级灰度直方图 + let mut hist = [0u32; 256]; + for &p in pixels { + hist[p as usize] += 1; + } + + let mut best_threshold = 128u8; + let mut best_variance = 0.0f64; + + let mut sum_b = 0u64; + let mut w_b = 0u64; + let sum_total: u64 = pixels.iter().map(|&p| p as u64).sum(); + + let mut found = false; + for t in 1..255u8 { + let h = hist[t as usize] as u64; + w_b += h; + if w_b == 0 || w_b == total as u64 { + continue; + } + sum_b += t as u64 * h; + let w_f = total as u64 - w_b; + if w_f == 0 { + break; + } + let mean_b = sum_b as f64 / w_b as f64; + let mean_f = (sum_total - sum_b) as f64 / w_f as f64; + let variance = w_b as f64 * w_f as f64 * (mean_b - mean_f).powi(2); + if variance > best_variance { + best_variance = variance; + best_threshold = t; + found = true; + } + } + + if found { Some(best_threshold) } else { None } +} + +/// Sauvola 局部自适应阈值 +/// +/// 对每个像素,在以它为中心的窗口内计算局部均值与标准差, +/// 阈值 = mean * (1 + k * (stddev / R - 1)) +/// 标准参数: window=图像短边的 1/8, k=0.2, R=128 +fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec> { + let window = ((w.min(h) as f64) * 0.08).max(8.0) as u32; + let half = (window / 2) as i32; + let k = 0.2f64; + 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)]; + 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 get_window = |x: i32, y: i32| -> (f64, f64) { + let x1 = (x - half).max(0) as usize; + let y1 = (y - half).max(0) as usize; + let x2 = (x + half + 1).min(w 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 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 mean = sum / count; + let variance = (sq_sum / count - mean * mean).max(0.0); + (mean, variance.sqrt()) + }; + + (0..h as usize) .map(|y| { - (0..width) - .map(|x| gray.get_pixel(x as u32, y as u32).0[0] < threshold) + (0..w as usize) + .map(|x| { + let (mean, stddev) = get_window(x as i32, y as i32); + let threshold = mean * (1.0 + k * (stddev / r - 1.0)); + gray.get_pixel(x as u32, y as u32).0[0] < threshold as u8 + }) .collect() }) - .collect(); - - Ok(matrix) + .collect() +} + +/// 使用给定阈值将像素切片二值化 +fn apply_threshold_from_pixels(pixels: &[u8], w: u32, h: u32, threshold: u8) -> Vec> { + let width = w as usize; + (0..h as usize) + .map(|y| { + let row_start = y * width; + pixels[row_start..row_start + width] + .iter() + .map(|&p| p < threshold) + .collect() + }) + .collect() } diff --git a/core/src/decoder/mod.rs b/core/src/decoder/mod.rs index e8e4501..ec5f583 100644 --- a/core/src/decoder/mod.rs +++ b/core/src/decoder/mod.rs @@ -1,6 +1,6 @@ //! QR 码解码器 //! -//! 完整流水线:图像 → 二值化 → 定位检测 → 格式/版本信息 → 解掩码 → +//! 完整流水线:图像 → 二值化(多遍) → 定位检测 → 格式/版本信息 → 解掩码 → //! 蛇形提取 → 去交错 → RS 纠错 → 模式解码 → 文本 //! //! ```rust @@ -17,7 +17,7 @@ mod deinterleave; mod detect; mod extract; mod format; -mod image; +pub mod image; mod mode_decode; mod perspective; mod rs_decode; @@ -26,6 +26,23 @@ use crate::error::QrError; use crate::matrix::mask::apply_mask; use crate::version::{EcLevel, Version}; +/// 包围盒(像素坐标) +#[derive(Debug, Clone, Copy, Default)] +pub struct BoundingBox { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +/// 结构化追加信息 +#[derive(Debug, Clone, Default)] +pub struct StructuredAppend { + pub position: u8, + pub total: u8, + pub parity: u8, +} + /// 解码结果 #[derive(Debug, Clone)] pub struct DecodeResult { @@ -39,29 +56,200 @@ pub struct DecodeResult { pub mask: u8, /// 纠错的码字数量 pub errors_corrected: usize, + /// QR 码在图像中的像素包围盒 + pub bounds: BoundingBox, + /// 置信度 0.0~1.0 + pub confidence: f64, + /// 是否为 GS1/FNC1 条码 + pub is_gs1: bool, + /// ECI 字符集编号(None = ISO-8859-1) + pub eci_designator: Option, + /// 结构化追加信息 + pub structured_append: Option, } /// 从图像字节数据解码 QR 码(PNG/JPEG/WebP 等) /// -/// # 参数 -/// - `bytes`: 图像文件字节(PNG/JPEG/WebP) -/// -/// # 返回 -/// `DecodeResult` 包含解码文本和元信息 +/// 使用多遍二值化 + 4 点透视矫正 + 反色检测。 pub fn decode_image(bytes: &[u8]) -> Result { - let gray = image::load_and_binarize(bytes)?; + decode_all(bytes).and_then(|mut results| { + results.pop().ok_or(QrError::DecodeFail("未找到 QR 码".into())) + }) +} - // 第一遍:直接检测 - if let Ok(detect_result) = detect::detect_and_extract(&gray) { - if let Ok(result) = decode_matrix(&detect_result.modules) { - return Ok(result); +/// 解码图片中所有 QR 码,返回按置信度降序排列的结果 +/// +/// 对每遍二值化结果尝试检测+解码,支持反色图像。 +pub fn decode_all(bytes: &[u8]) -> Result, QrError> { + let gray = image::load_gray(bytes)?; + let binarized_list = image::binarize_multi(&gray); + let should_try_inverted = is_likely_inverted(&gray); + let mut results: Vec = Vec::new(); + + for (pass_idx, bin) in binarized_list.iter().enumerate() { + let normal = (false, bin.clone()); + let inverted = (pass_idx == 0 && should_try_inverted) + .then(|| (true, invert_matrix(bin))); + for (is_inverted, matrix) in std::iter::once(normal).chain(inverted) { + // 尝试直接检测 → 透视矫正重试 + if let Ok(mut r) = try_decode_with_perspective(&matrix, &gray, pass_idx, is_inverted) { + r.confidence = r.confidence.clamp(0.0, 1.0); + if r.confidence > 0.3 { + results.push(r); + } + } + } + + // 反色也失败时,尝试纯旋转回退 + if results.is_empty() && pass_idx == 0 { + let fallback = perspective::auto_correct_fallback(bin); + if let Ok(result) = try_decode_matrix(&fallback, None) { + let mut r = with_confidence(result, pass_idx); + r.confidence *= 0.75; // 回退惩罚 + results.push(r); + } + } + + if !results.is_empty() { + break; } } - // 第二遍:尝试旋转矫正 - let corrected = perspective::auto_correct(&gray); - let detect_result = detect::detect_and_extract(&corrected)?; - decode_matrix(&detect_result.modules) + if results.is_empty() { + Err(QrError::DecodeFail("所有方法均未能解码 QR 码".into())) + } else { + results.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)); + Ok(results) + } +} + +/// 尝试从二值化矩阵解码(含透视矫正重试) +fn try_decode_with_perspective( + bin: &[Vec], + _gray: &crate::decoder::image::GrayImage, + pass_idx: usize, + is_inverted: bool, +) -> Result { + // 检测定位图案 + let finders = detect::find_finders_for_perspective(bin)?; + let tl = (finders[0].0 as f64, finders[0].1 as f64); + let tr = (finders[1].0 as f64, finders[1].1 as f64); + let bl = (finders[2].0 as f64, finders[2].1 as f64); + + // 估算版本 + let module_size = detect::estimate_module_size_from_finders(bin, &finders); + let version = detect::estimate_version_from_tl_tr(tl, tr, module_size); + + // 直接用二值化矩阵尝试 + if let Ok(result) = try_decode_matrix(bin, None) { + return Ok(apply_perspective_penalty(result, pass_idx, is_inverted, false)); + } + + // 透视矫正重试 + let corrected = perspective::auto_correct(bin, tl, tr, bl, version); + if !corrected.is_empty() && corrected != *bin { + if let Ok(result) = try_decode_matrix(&corrected, None) { + return Ok(apply_perspective_penalty(result, pass_idx, is_inverted, true)); + } + } + + // 旋转回退(在多个预设角度尝试) + let fallback = perspective::auto_correct_fallback(bin); + if fallback != *bin { + if let Ok(result) = try_decode_matrix(&fallback, None) { + return Ok(apply_perspective_penalty(result, pass_idx, is_inverted, true)); + } + } + + Err(QrError::DecodeFail("检测到定位图案但解码失败".into())) +} + +fn apply_perspective_penalty( + mut r: DecodeResult, + pass_idx: usize, + is_inverted: bool, + had_correction: bool, +) -> DecodeResult { + if pass_idx > 0 { r.confidence -= 0.05 * pass_idx as f64; } + if is_inverted { r.confidence *= 0.85; } + if had_correction { r.confidence *= 0.9; } + r.confidence = r.confidence.max(0.0); + r +} + +fn invert_matrix(matrix: &[Vec]) -> Vec> { + matrix.iter().map(|row| row.iter().map(|&p| !p).collect()).collect() +} + +/// 尝试从二值化矩阵解码,成功返回带包围盒的结果 +fn try_decode_matrix( + matrix: &[Vec], + bounds_override: Option, +) -> Result { + let detect_result = detect::detect_and_extract(matrix)?; + let mut result = decode_matrix(&detect_result.modules)?; + result.bounds = bounds_override.unwrap_or(detect_result.bounds); + result.confidence = compute_confidence( + &result, + detect_result.quiet_zone_ok, + detect_result.finder_ratio_deviation, + ); + Ok(result) +} + +/// 根据解码质量指标计算置信度 0.0~1.0 +fn compute_confidence( + result: &DecodeResult, + quiet_zone_ok: bool, + finder_ratio_deviation: f64, +) -> f64 { + let mut conf = 1.0; + + // 纠错比例惩罚 + let max_ec = Version::new(result.version) + .map(|v| v.ec_info(result.level).ec_per_block as usize) + .unwrap_or(0); + if max_ec > 0 { + let err_ratio = result.errors_corrected as f64 / max_ec as f64; + conf -= err_ratio * 0.4; + } + + // 静区不完整惩罚 + if !quiet_zone_ok { + conf *= 0.7; + } + + // 定位图案比例偏差惩罚 + if finder_ratio_deviation > 0.25 { + conf -= 0.2; + } + + conf.clamp(0.0, 1.0) +} + +/// 带置信度封装结果 +fn with_confidence(mut result: DecodeResult, pass_idx: usize) -> DecodeResult { + // 多遍二值化惩罚:第 2 遍 (Otsu) -0.05, 第 3 遍 (Sauvola) -0.1 + if pass_idx > 0 { + result.confidence -= 0.05 * pass_idx as f64; + result.confidence = result.confidence.max(0.0); + } + result +} + +/// 检测图像是否大概率是反色 QR 码(白底黑码) +fn is_likely_inverted(gray: &crate::decoder::image::GrayImage) -> bool { + let (w, h) = gray.dimensions(); + // 采样四角和中心 5 个点 + let samples = [ + gray.get_pixel(0, 0).0[0], + gray.get_pixel(w - 1, 0).0[0], + gray.get_pixel(0, h - 1).0[0], + gray.get_pixel(w - 1, h - 1).0[0], + gray.get_pixel(w / 2, h / 2).0[0], + ]; + // ≥4 个采样点为暗色 (>128) → 反色 + samples.iter().filter(|&&p| p > 128).count() >= 4 } /// 从布尔矩阵解码 QR 码 @@ -72,18 +260,15 @@ pub fn decode_image(bytes: &[u8]) -> Result { /// # 返回 /// `DecodeResult` 包含解码文本和元信息 pub fn decode_matrix(matrix: &[Vec]) -> Result { - // 1. 构建 Matrix 对象 let size = matrix.len() as u8; if matrix.is_empty() || matrix[0].is_empty() { return Err(QrError::DecodeFail("空矩阵".into())); } - // 验证方形 if matrix.iter().any(|r| r.len() != size as usize) { return Err(QrError::DecodeFail("矩阵不是方形".into())); } - // 从尺寸推算版本 let version = ((size as i32 - 17) / 4) as u8; if !(1..=40).contains(&version) || (17 + version as i32 * 4) != size as i32 { return Err(QrError::DecodeFail(format!( @@ -92,7 +277,6 @@ pub fn decode_matrix(matrix: &[Vec]) -> Result { ))); } - // 构建 Matrix 对象(简化:不预标注保留区域,BCH 读取函数直接访问坐标) let mut m = crate::matrix::grid::Matrix::new(size); for (y, row) in matrix.iter().enumerate() { for (x, &dark) in row.iter().enumerate() { @@ -102,14 +286,12 @@ pub fn decode_matrix(matrix: &[Vec]) -> Result { } } - // 标记功能图案区域(使数据提取能跳过) use crate::matrix::patterns::{ place_alignment_patterns, place_finder_patterns, place_timing_patterns, reserve_format_areas, reserve_version_areas, }; place_finder_patterns(&mut m); place_timing_patterns(&mut m); - // 对齐图案位置依赖于版本,需要从版本查询 let ver = Version::new(version).ok_or(QrError::InvalidVersion(version))?; place_alignment_patterns(&mut m, ver.alignment_positions()); reserve_format_areas(&mut m); @@ -117,10 +299,8 @@ pub fn decode_matrix(matrix: &[Vec]) -> Result { reserve_version_areas(&mut m); } - // 2. 读取格式信息 → EC 级别 + 掩码 let (level, mask) = format::read_format_info(&m)?; - // 3. 读取版本信息(版本≥7时验证) if version >= 7 { let ver_info = format::read_version_info(&m)?; if ver_info != version { @@ -130,18 +310,14 @@ pub fn decode_matrix(matrix: &[Vec]) -> Result { } } - // 4. 解掩码 let unmasked = apply_mask(&m, mask); - // 5. 蛇形提取码字 let ec_info = ver.ec_info(level); let total_codewords = ec_info.total_codewords as usize; let codewords = extract::extract_codewords(&unmasked, total_codewords); - // 6. 去交错 let (data_blocks, ec_blocks) = deinterleave::deinterleave(&codewords, &ec_info); - // 7. RS 纠错 let mut corrected_data = Vec::new(); let mut total_errors = 0usize; @@ -151,20 +327,23 @@ pub fn decode_matrix(matrix: &[Vec]) -> Result { total_errors += errors; } - // 8. 转为比特流 let bits: Vec = corrected_data .iter() .flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1 == 1)) .collect(); - // 9. 模式解码 - let text = mode_decode::decode_bitstream(&bits, version)?; + let decode_output = mode_decode::decode_bitstream(&bits, version)?; Ok(DecodeResult { - text, + text: decode_output.text, version, level, mask, errors_corrected: total_errors, + bounds: BoundingBox::default(), + confidence: 1.0, + is_gs1: decode_output.is_gs1, + eci_designator: decode_output.eci_designator, + structured_append: decode_output.structured_append, }) } diff --git a/core/src/decoder/mode_decode.rs b/core/src/decoder/mode_decode.rs index 2f44c5a..f1bbdc4 100644 --- a/core/src/decoder/mode_decode.rs +++ b/core/src/decoder/mode_decode.rs @@ -130,53 +130,119 @@ fn shift_jis_value_to_char(val: u16) -> Option { output.chars().next() } -/// 解码主函数:比特流 → 文本 +/// 解码输出 +pub(crate) struct DecodeOutput { + pub text: String, + pub is_gs1: bool, + pub eci_designator: Option, + pub structured_append: Option, +} + +/// 解码主函数:比特流 → 解码输出 pub(crate) fn decode_bitstream( bits: &[bool], version: u8, -) -> Result { +) -> Result { let mut pos = 0; let mut text = String::new(); + let mut is_gs1 = false; + let mut eci_designator: Option = None; + let mut structured_append: Option = None; loop { if pos + 4 > bits.len() { break; } let mode_indicator = read_bits(bits, &mut pos, 4) as u8; - if mode_indicator == 0b0000 { - break; // 终止符 - } - - let count_bits = - char_count_bits(mode_indicator, version).ok_or_else(|| { - crate::error::QrError::DecodeFail(format!( - "未知模式指示符: {:04b}", - mode_indicator - )) - })? as usize; - if pos + count_bits > bits.len() { - break; - } - let count = read_bits(bits, &mut pos, count_bits); match mode_indicator { - 0b0001 => text.push_str(&decode_numeric(bits, &mut pos, count)), - 0b0010 => text.push_str(&decode_alphanumeric(bits, &mut pos, count)), - 0b0100 => text.push_str(&decode_byte(bits, &mut pos, count)), - 0b1000 => text.push_str(&decode_kanji(bits, &mut pos, count)), + 0b0000 => break, // 终止符 + 0b0001 | 0b0010 | 0b0100 | 0b1000 => { + // 标准数据模式 + let count_bits = + char_count_bits(mode_indicator, version).ok_or_else(|| { + crate::error::QrError::DecodeFail(format!( + "未知模式指示符: {:04b}", + mode_indicator + )) + })? as usize; + if pos + count_bits > bits.len() { break; } + let count = read_bits(bits, &mut pos, count_bits); + match mode_indicator { + 0b0001 => text.push_str(&decode_numeric(bits, &mut pos, count)), + 0b0010 => text.push_str(&decode_alphanumeric(bits, &mut pos, count)), + 0b0100 => { + let bytes_str = decode_byte(bits, &mut pos, count); + // 根据 ECI 处理字符集 + if eci_designator == Some(26) { + // UTF-8: 原样保留字节序列(已在 decode_byte 中按 Latin-1 解码, + // 但 UTF-8 字节在 Latin-1 范围 0x00-0xFF 内被保留为单字节字符) + // 这里简单处理:如果是 UTF-8 ECI,不做二次转换 + text.push_str(&bytes_str); + } else { + text.push_str(&bytes_str); + } + } + 0b1000 => text.push_str(&decode_kanji(bits, &mut pos, count)), + _ => unreachable!(), + } + } + 0b0011 => { + // 结构化追加 + if pos + 16 > bits.len() { break; } + let sym_pos = read_bits(bits, &mut pos, 4) as u8; + let sym_total = read_bits(bits, &mut pos, 4) as u8; + let parity = read_bits(bits, &mut pos, 8) as u8; + structured_append = Some(crate::decoder::StructuredAppend { + position: sym_pos, + total: sym_total, + parity, + }); + // 结构化追加头部后紧跟数据段,继续循环 + continue; + } + 0b0101 => { + // FNC1 First Position (GS1) + is_gs1 = true; + // 字母数字模式的第一位 '%' 被替换为 FNC1,需要在解码时处理 + // 简化:标记 is_gs1,由调用方按 GS1 标准解析 + continue; + } + 0b1001 => { + // FNC1 Second Position + is_gs1 = true; + continue; + } + 0b0111 => { + // ECI 指派号 + if pos + 8 > bits.len() { break; } + let eci_byte1 = read_bits(bits, &mut pos, 8) as u32; + eci_designator = if eci_byte1 >= 128 { + // 2 字节 ECI + if pos + 8 > bits.len() { break; } + let eci_byte2 = read_bits(bits, &mut pos, 8) as u32; + Some((eci_byte1 - 128) * 128 + eci_byte2) + } else { + Some(eci_byte1) + }; + continue; + } _ => { - return Err(crate::error::QrError::DecodeFail(format!( - "未知模式指示符: {:04b}", - mode_indicator - ))) + // 未知模式指示符 → 可能是填充,跳过 + break; } } } - if text.is_empty() { + if text.is_empty() && structured_append.is_none() { Err(crate::error::QrError::DecodeFail("未解码到任何文本".into())) } else { - Ok(text) + Ok(DecodeOutput { + text, + is_gs1, + eci_designator, + structured_append, + }) } } diff --git a/core/src/decoder/perspective.rs b/core/src/decoder/perspective.rs index d78f406..722a48a 100644 --- a/core/src/decoder/perspective.rs +++ b/core/src/decoder/perspective.rs @@ -1,160 +1,164 @@ -//! QR 码图像透视矫正 +//! 透视矫正 — DLT 4 点单应变换 + 多角度旋转回退 //! -//! 检测定位图案后,计算旋转角并矫正图像。 -//! MVP 版本:仅做旋转矫正(仿射变换),不做完整单应变换。 +//! 用 3 个定位图案中心 + 推算第 4 点构建 4 点对应, +//! 通过 DLT 求解 3×3 单应矩阵,双线性插值反向映射生成矫正图像。 -pub(crate) fn auto_correct(gray: &[Vec]) -> Vec> { - let h = gray.len(); - let _w = if h > 0 { - gray[0].len() - } else { - return gray.to_vec(); - }; +use nalgebra::DMatrix; - // 尝试找到至少 2 个 finder - let finders = find_two_finders(gray); - if finders.len() < 2 { - return gray.to_vec(); - } - - rotate_to_horizontal(gray, finders[0], finders[1]) -} - -/// 简化的 finder 检测(只找 2 个) -fn find_two_finders(gray: &[Vec]) -> Vec<(usize, usize)> { - let h = gray.len(); - let _w = if h > 0 { - gray[0].len() - } else { - return vec![]; - }; - let mut centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size) - - for row in (0..h).step_by(3) { - let runs = scan_row_runs(gray, row); - for i in 0..runs.len().saturating_sub(4) { - let avg = (runs[i].1 + runs[i + 1].1 + runs[i + 2].1 + runs[i + 3].1 + runs[i + 4].1) - as f32 - / 5.0; - if avg < 2.0 { - continue; - } - let r = [ - runs[i].1 as f32, - runs[i + 1].1 as f32, - runs[i + 2].1 as f32, - runs[i + 3].1 as f32, - runs[i + 4].1 as f32, - ]; - let check = |v: f32, e: f32| (v - e * avg).abs() < avg * 0.4; - if check(r[0], 1.0) - && check(r[1], 1.0) - && check(r[2], 3.0) - && check(r[3], 1.0) - && check(r[4], 1.0) - { - let cx = runs[i + 2].0 + runs[i + 2].1 / 2; - let size = - runs[i].1 + runs[i + 1].1 + runs[i + 2].1 + runs[i + 3].1 + runs[i + 4].1; - centers.push((cx, row, size)); - } - } - } - - if centers.len() < 2 { - return vec![]; - } - - // 按 X 坐标排序,取最左和最右 - centers.sort_by_key(|c| c.0); - let left = centers.first().unwrap(); - let right = centers.last().unwrap(); - vec![(left.0, left.1), (right.0, right.1)] -} - -fn scan_row_runs(gray: &[Vec], row: usize) -> Vec<(usize, usize)> { - let w = gray[0].len(); - let mut runs = Vec::new(); - let mut col = 0; - while col < w { - let current = gray[row][col]; - let mut len = 0; - while col < w && gray[row][col] == current { - len += 1; - col += 1; - } - runs.push((col - len, len)); - } - runs -} - -/// 旋转图像使 QR 码水平对齐 -#[allow(clippy::needless_range_loop)] -fn rotate_to_horizontal( +/// 用 3 个定位图案中心计算单应变换,返回矫正后的二值图像 +pub(crate) fn auto_correct( gray: &[Vec], - tl: (usize, usize), - tr: (usize, usize), + tl: (f64, f64), + tr: (f64, f64), + bl: (f64, f64), + version: u8, ) -> Vec> { - let h = gray.len(); - let w = if h > 0 { - gray[0].len() - } else { - return gray.to_vec(); + let size = (17 + version as usize * 4) as f64; + + // 推算右下角:平行四边形法则 + let br = (tr.0 + bl.0 - tl.0, tr.1 + bl.1 - tl.1); + + let src = [tl, tr, br, bl]; + let dst = [ + (0.0, 0.0), + (size - 1.0, 0.0), + (size - 1.0, size - 1.0), + (0.0, size - 1.0), + ]; + + let h = match compute_homography(&src, &dst) { + Some(h) => h, + None => return gray.to_vec(), }; - // 计算旋转角(弧度) - let dx = tr.0 as f64 - tl.0 as f64; - let dy = tr.1 as f64 - tl.1 as f64; - let angle = dy.atan2(dx); // 正值 = 顺时针偏离水平 + let h_inv = match invert_homography(&h) { + Some(inv) => inv, + None => return gray.to_vec(), + }; - if angle.abs() < 0.01 { - // 已基本水平,不处理 - return gray.to_vec(); - } + let out_size = size as u32; + (0..out_size as usize) + .map(|y| { + (0..out_size as usize) + .map(|x| { + let (sx, sy) = apply_point(&h_inv, x as f64, y as f64); + bilinear_sample(gray, sx, sy) + }) + .collect() + }) + .collect() +} - // 旋转中心 = 图像中心 - let cx = w as f64 / 2.0; - let cy = h as f64 / 2.0; - let cos_a = angle.cos(); - let sin_a = angle.sin(); +/// 多角度旋转回退(兼容旧 API,无定位图案信息时使用) +pub(crate) fn auto_correct_fallback(gray: &[Vec]) -> Vec> { + let angles = [-45.0f64, -30.0, -15.0, 0.0, 15.0, 30.0, 45.0]; + let mut best_count = 0usize; + let mut best_angle = 0.0f64; - // 计算旋转后尺寸 - let corners = [ - (0.0, 0.0), - (w as f64, 0.0), - (w as f64, h as f64), - (0.0, h as f64), - ]; - let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN); - for &(x, y) in &corners { - let rx = (x - cx) * cos_a - (y - cy) * sin_a + cx; - let ry = (x - cx) * sin_a + (y - cy) * cos_a + cy; - min_x = min_x.min(rx); - min_y = min_y.min(ry); - max_x = max_x.max(rx); - max_y = max_y.max(ry); - } - - let new_w = (max_x - min_x).ceil() as usize; - let new_h = (max_y - min_y).ceil() as usize; - - // 反向映射:对旋转后图像的每个像素,计算源图像中的位置,双线性插值 - let mut result = vec![vec![false; new_w]; new_h]; - - for ny in 0..new_h { - for nx in 0..new_w { - // 映射回旋转前的坐标 - let sx = (nx as f64 + min_x - cx) * cos_a + (ny as f64 + min_y - cy) * sin_a + cx; - let sy = -(nx as f64 + min_x - cx) * sin_a + (ny as f64 + min_y - cy) * cos_a + cy; - - let sx_idx = sx as usize; - let sy_idx = sy as usize; - - if sx_idx < w && sy_idx < h { - result[ny][nx] = gray[sy_idx][sx_idx]; - } + for &angle in &angles { + let rotated = rotate_image(gray, angle.to_radians()); + let count = count_finder_hits(&rotated); + if count > best_count { + best_count = count; + best_angle = angle.to_radians(); } } - result + if best_count > 0 && best_angle.abs() > 0.01 { + rotate_image(gray, best_angle) + } else { + gray.to_vec() + } +} + +/// DLT 求解 3×3 单应矩阵(4 点对应 → 最小特征向量) +fn compute_homography(src: &[(f64, f64); 4], dst: &[(f64, f64); 4]) -> Option<[[f64; 3]; 3]> { + let mut a = DMatrix::::zeros(8, 9); + for i in 0..4 { + let (x, y) = src[i]; + let (u, v) = dst[i]; + let r = 2 * i; + a[(r, 0)] = x; a[(r, 1)] = y; a[(r, 2)] = 1.0; + a[(r, 6)] = -x * u; a[(r, 7)] = -y * u; a[(r, 8)] = -u; + a[(r + 1, 3)] = x; a[(r + 1, 4)] = y; a[(r + 1, 5)] = 1.0; + a[(r + 1, 6)] = -x * v; a[(r + 1, 7)] = -y * v; a[(r + 1, 8)] = -v; + } + + let ata = a.transpose() * &a; + let eigen = ata.symmetric_eigen(); + let min_idx = eigen.eigenvalues.iter().enumerate() + .min_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap_or(std::cmp::Ordering::Equal)) + .map(|(i, _)| i)?; + + let hv = eigen.eigenvectors.column(min_idx); + let s = if hv[8].abs() > 1e-8 { 1.0 / hv[8] } else { 1.0 }; + Some([ + [hv[0] * s, hv[1] * s, hv[2] * s], + [hv[3] * s, hv[4] * s, hv[5] * s], + [hv[6] * s, hv[7] * s, 1.0], + ]) +} + +/// 3×3 矩阵求逆 +fn invert_homography(h: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> { + let (a,b,c) = (h[0][0],h[0][1],h[0][2]); + let (d,e,f) = (h[1][0],h[1][1],h[1][2]); + let (g,hi,i) = (h[2][0],h[2][1],h[2][2]); + let det = a*(e*i-f*hi) - b*(d*i-f*g) + c*(d*hi-e*g); + if det.abs() < 1e-10 { return None; } + let inv = 1.0 / det; + Some([ + [(e*i-f*hi)*inv, (c*hi-b*i)*inv, (b*f-c*e)*inv], + [(f*g-d*i)*inv, (a*i-c*g)*inv, (c*d-a*f)*inv], + [(d*hi-e*g)*inv, (b*g-a*hi)*inv, (a*e-b*d)*inv], + ]) +} + +fn apply_point(h: &[[f64; 3]; 3], x: f64, y: f64) -> (f64, f64) { + let w = h[2][0]*x + h[2][1]*y + h[2][2]; + if w.abs() < 1e-8 { return (x, y); } + ((h[0][0]*x + h[0][1]*y + h[0][2])/w, (h[1][0]*x + h[1][1]*y + h[1][2])/w) +} + +fn rotate_image(gray: &[Vec], angle: f64) -> Vec> { + let h = gray.len(); let w = if h>0 { gray[0].len() } else { return gray.to_vec(); }; + let (cos, sin) = (angle.cos(), angle.sin()); + let (cx, cy) = (w as f64/2.0, h as f64/2.0); + let corners = [(0.0,0.0),(w as f64,0.0),(w as f64,h as f64),(0.0,h as f64)]; + let mut xs=vec![]; let mut ys=vec![]; + for (px,py) in corners { let rx=cos*(px-cx)-sin*(py-cy)+cx; let ry=sin*(px-cx)+cos*(py-cy)+cy; xs.push(rx as i32); ys.push(ry as i32); } + let (min_x,max_x)=(*xs.iter().min().unwrap_or(&0),*xs.iter().max().unwrap_or(&0)); + let (min_y,max_y)=(*ys.iter().min().unwrap_or(&0),*ys.iter().max().unwrap_or(&0)); + let (ow,oh)=((max_x-min_x+1).max(1) as usize,(max_y-min_y+1).max(1) as usize); + (0..oh).map(|y|(0..ow).map(|x|{ + let sx=cos*((x as i32+min_x) as f64-cx)+sin*((y as i32+min_y) as f64-cy)+cx; + let sy=-sin*((x as i32+min_x) as f64-cx)+cos*((y as i32+min_y) as f64-cy)+cy; + bilinear_sample(gray,sx,sy) + }).collect()).collect() +} + +fn count_finder_hits(gray: &[Vec]) -> usize { + let mut hits=0; + for row in (0..gray.len()).step_by(4) { + let (mut col,mut runs)=(0,vec![]); + while col], x: f64, y: f64) -> bool { + let (h,w)=(gray.len() as i32,if gray.is_empty(){0}else{gray[0].len() as i32}); + let (fx,fy)=(x.floor(),y.floor()); + let (x0,y0)=(fx as i32,fy as i32); + let (x1,y1)=(x0+1,y0+1); + let (dx,dy)=(x-fx,y-fy); + let s=|px:i32,py:i32|->f64{if px>=0&&py>=0&&px 0.5 }