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:
2026-06-21 16:00:11 +08:00
parent cd75141037
commit bd4ca718ac
9 changed files with 971 additions and 252 deletions
Generated
+220
View File
@@ -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"
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
}
// 第二遍:尝试旋转矫正
let corrected = perspective::auto_correct(&gray);
let detect_result = detect::detect_and_extract(&corrected)?;
decode_matrix(&detect_result.modules)
// 反色也失败时,尝试旋转回退
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;
}
}
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,
})
}
+82 -16
View File
@@ -130,23 +130,35 @@ 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; // 终止符
}
match mode_indicator {
0b0000 => break, // 终止符
0b0001 | 0b0010 | 0b0100 | 0b1000 => {
// 标准数据模式
let count_bits =
char_count_bits(mode_indicator, version).ok_or_else(|| {
crate::error::QrError::DecodeFail(format!(
@@ -154,29 +166,83 @@ pub(crate) fn decode_bitstream(
mode_indicator
))
})? as usize;
if pos + count_bits > bits.len() {
break;
}
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)),
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,
})
}
}
+155 -151
View File
@@ -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 cx = w as f64 / 2.0;
let cy = h as f64 / 2.0;
let cos_a = angle.cos();
let sin_a = angle.sin();
// 计算旋转后尺寸
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];
}
}
}
result
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()
}
/// 多角度旋转回退(兼容旧 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;
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();
}
}
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
}