feat: 解码器全面增强 — 多遍二值化 + 4点透视 + ECI/FNC1
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测试全部通过
This commit is contained in:
Generated
+220
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
+6
-10
@@ -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<u8> {
|
||||
/// 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)]
|
||||
|
||||
+128
-1
@@ -15,6 +15,9 @@ struct FinderMatch {
|
||||
pub(crate) struct DetectResult {
|
||||
pub modules: Vec<Vec<bool>>, // 布尔矩阵(含静区)
|
||||
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<bool>], col: usize) -> Vec<(usize, usize)> {
|
||||
centers
|
||||
}
|
||||
|
||||
/// 检测 3 个定位图案,返回 (cx, cy) 像素坐标对(供透视矫正使用)
|
||||
pub(crate) fn find_finders_for_perspective(
|
||||
gray: &[Vec<bool>],
|
||||
) -> 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<bool>],
|
||||
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<bool>]) -> 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<bool>], 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<bool>], finder: &FinderMatch) -> f64 {
|
||||
let row = finder.cy;
|
||||
let mut runs: Vec<usize> = 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::<f64>()
|
||||
/ 5.0;
|
||||
if dev < best_dev {
|
||||
best_dev = dev;
|
||||
}
|
||||
}
|
||||
best_dev
|
||||
}
|
||||
|
||||
+10
-14
@@ -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<u8, crate::error::QrE
|
||||
let dec2 = bch::decode_version_info(raw2);
|
||||
|
||||
match (dec1, dec2) {
|
||||
(Some(v1), Some(v2)) if v1 == v2 => 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(),
|
||||
)),
|
||||
|
||||
+152
-22
@@ -1,34 +1,164 @@
|
||||
//! 图像加载与二值化
|
||||
//!
|
||||
//! 使用 `image` crate 加载 PNG/JPEG/WebP,转为灰度再二值化为布尔矩阵。
|
||||
//! 提供多遍自适应二值化策略和灰度图加载 API。
|
||||
|
||||
/// 从图像字节加载并二值化
|
||||
pub use image::GrayImage;
|
||||
|
||||
/// 从图像字节加载为灰度图(公共 API,供调用方自定义预处理)
|
||||
///
|
||||
/// 步骤:解码 → 灰度 → 按中位数阈值二值化
|
||||
pub(crate) fn load_and_binarize(
|
||||
bytes: &[u8],
|
||||
) -> Result<Vec<Vec<bool>>, 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<GrayImage, crate::error::QrError> {
|
||||
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<Vec<Vec<bool>>> {
|
||||
let (w, h) = gray.dimensions();
|
||||
let width = w as usize;
|
||||
let height = h as usize;
|
||||
let pixels: Vec<u8> = gray.iter().copied().collect();
|
||||
let total = pixels.len();
|
||||
|
||||
// 计算中位数阈值
|
||||
let mut all_pixels: Vec<u8> = 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<Vec<bool>> = (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<u8> = pixels.to_vec();
|
||||
sorted.sort_unstable();
|
||||
sorted[sorted.len() / 2]
|
||||
}
|
||||
|
||||
/// Otsu 大津算法 — 最大化类间方差寻找最优阈值
|
||||
///
|
||||
/// 适用于双峰直方图(前景/背景分明)。
|
||||
/// 对直方图退化为单峰的情况返回 None。
|
||||
fn otsu_threshold(pixels: &[u8], total: usize) -> Option<u8> {
|
||||
// 构建 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<Vec<bool>> {
|
||||
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<Vec<bool>> {
|
||||
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()
|
||||
}
|
||||
|
||||
+211
-32
@@ -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<u32>,
|
||||
/// 结构化追加信息
|
||||
pub structured_append: Option<StructuredAppend>,
|
||||
}
|
||||
|
||||
/// 从图像字节数据解码 QR 码(PNG/JPEG/WebP 等)
|
||||
///
|
||||
/// # 参数
|
||||
/// - `bytes`: 图像文件字节(PNG/JPEG/WebP)
|
||||
///
|
||||
/// # 返回
|
||||
/// `DecodeResult` 包含解码文本和元信息
|
||||
/// 使用多遍二值化 + 4 点透视矫正 + 反色检测。
|
||||
pub fn decode_image(bytes: &[u8]) -> Result<DecodeResult, QrError> {
|
||||
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<Vec<DecodeResult>, 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<DecodeResult> = 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<bool>],
|
||||
_gray: &crate::decoder::image::GrayImage,
|
||||
pass_idx: usize,
|
||||
is_inverted: bool,
|
||||
) -> Result<DecodeResult, QrError> {
|
||||
// 检测定位图案
|
||||
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<bool>]) -> Vec<Vec<bool>> {
|
||||
matrix.iter().map(|row| row.iter().map(|&p| !p).collect()).collect()
|
||||
}
|
||||
|
||||
/// 尝试从二值化矩阵解码,成功返回带包围盒的结果
|
||||
fn try_decode_matrix(
|
||||
matrix: &[Vec<bool>],
|
||||
bounds_override: Option<BoundingBox>,
|
||||
) -> Result<DecodeResult, QrError> {
|
||||
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, QrError> {
|
||||
/// # 返回
|
||||
/// `DecodeResult` 包含解码文本和元信息
|
||||
pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, QrError> {
|
||||
// 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<bool>]) -> Result<DecodeResult, QrError> {
|
||||
)));
|
||||
}
|
||||
|
||||
// 构建 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<bool>]) -> Result<DecodeResult, QrError> {
|
||||
}
|
||||
}
|
||||
|
||||
// 标记功能图案区域(使数据提取能跳过)
|
||||
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<bool>]) -> Result<DecodeResult, QrError> {
|
||||
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<bool>]) -> Result<DecodeResult, QrError> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<bool>]) -> Result<DecodeResult, QrError> {
|
||||
total_errors += errors;
|
||||
}
|
||||
|
||||
// 8. 转为比特流
|
||||
let bits: Vec<bool> = 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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -130,53 +130,119 @@ fn shift_jis_value_to_char(val: u16) -> Option<char> {
|
||||
output.chars().next()
|
||||
}
|
||||
|
||||
/// 解码主函数:比特流 → 文本
|
||||
/// 解码输出
|
||||
pub(crate) struct DecodeOutput {
|
||||
pub text: String,
|
||||
pub is_gs1: bool,
|
||||
pub eci_designator: Option<u32>,
|
||||
pub structured_append: Option<crate::decoder::StructuredAppend>,
|
||||
}
|
||||
|
||||
/// 解码主函数:比特流 → 解码输出
|
||||
pub(crate) fn decode_bitstream(
|
||||
bits: &[bool],
|
||||
version: u8,
|
||||
) -> Result<String, crate::error::QrError> {
|
||||
) -> Result<DecodeOutput, crate::error::QrError> {
|
||||
let mut pos = 0;
|
||||
let mut text = String::new();
|
||||
let mut is_gs1 = false;
|
||||
let mut eci_designator: Option<u32> = None;
|
||||
let mut structured_append: Option<crate::decoder::StructuredAppend> = 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+150
-146
@@ -1,160 +1,164 @@
|
||||
//! QR 码图像透视矫正
|
||||
//! 透视矫正 — DLT 4 点单应变换 + 多角度旋转回退
|
||||
//!
|
||||
//! 检测定位图案后,计算旋转角并矫正图像。
|
||||
//! MVP 版本:仅做旋转矫正(仿射变换),不做完整单应变换。
|
||||
//! 用 3 个定位图案中心 + 推算第 4 点构建 4 点对应,
|
||||
//! 通过 DLT 求解 3×3 单应矩阵,双线性插值反向映射生成矫正图像。
|
||||
|
||||
pub(crate) fn auto_correct(gray: &[Vec<bool>]) -> Vec<Vec<bool>> {
|
||||
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<bool>]) -> 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<bool>], 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<bool>],
|
||||
tl: (usize, usize),
|
||||
tr: (usize, usize),
|
||||
tl: (f64, f64),
|
||||
tr: (f64, f64),
|
||||
bl: (f64, f64),
|
||||
version: u8,
|
||||
) -> Vec<Vec<bool>> {
|
||||
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<bool>]) -> Vec<Vec<bool>> {
|
||||
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::<f64>::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<bool>], angle: f64) -> Vec<Vec<bool>> {
|
||||
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<bool>]) -> usize {
|
||||
let mut hits=0;
|
||||
for row in (0..gray.len()).step_by(4) {
|
||||
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;
|
||||
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;}
|
||||
}
|
||||
}
|
||||
hits
|
||||
}
|
||||
|
||||
fn bilinear_sample(gray: &[Vec<bool>], 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<w&&py<h{gray[py as usize][px as usize]as u8 as f64}else{0.0}};
|
||||
let v00=s(x0,y0);let v10=s(x1,y0);let v01=s(x0,y1);let v11=s(x1,y1);
|
||||
(v00*(1.0-dx)+v10*dx)*(1.0-dy)+(v01*(1.0-dx)+v11*dx)*dy > 0.5
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user