feat: QR 码解码器 — 从零手写的完整解码流水线

新增 core/src/decoder/ 模块(9 文件,~1500 行):
- bch.rs: BCH(15,5)+BCH(18,6) 查表解码(32+64 有效码字,t≤3)
- format.rs: 从矩阵读取格式信息(EC+掩码)+版本信息(2 副本容错)
- extract.rs: 逆向蛇形排列提取数据码字
- deinterleave.rs: 逆向 RS 数据交错
- rs_decode.rs: RS 纠错流水线(伴随式→BM→Chien→Forney)
- mode_decode.rs: 逆向 4 种编码模式(数字/字母/字节/汉字 Shift JIS)
- detect.rs: 定位图案检测(1:1:3:1:1 比例+交叉验证+聚类)
- image.rs: 图像加载+灰度二值化(PNG/JPEG/WebP)
- mod.rs: 顶层 API(decode_image + decode_matrix)

修改已有文件:
- core: galois.rs 表 pub(crate), 新增 poly_eval(); reed_solomon 公开内部函数
- cli: 新增 --decode <file> 解码模式
- web: 新增 POST /api/decode(multipart file upload)

测试: 72 passed (58 原有 + 14 新增 decoder 测试)
This commit is contained in:
2026-06-19 20:36:12 +08:00
parent 87aa3f4574
commit effc88c6d7
20 changed files with 1832 additions and 31 deletions
+268
View File
@@ -0,0 +1,268 @@
//! QR 码定位图案检测与采样
//!
//! 在二值化图像中检测 3 个定位图案(1:1:3:1:1 比例),
//! 构建模块采样网格,提取布尔矩阵。
/// 定位图案检测结果(图像坐标,原点左上角)
struct FinderMatch {
cx: usize, // 中心 X
cy: usize, // 中心 Y
size: usize, // 探测器边长(像素)
}
/// QR 码检测结果
#[allow(dead_code)]
pub(crate) struct DetectResult {
pub modules: Vec<Vec<bool>>, // 布尔矩阵(含静区)
pub version_estimate: u8,
}
/// 水平扫描查找 1:1:3:1:1 比例
fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
// (列号,运行长度)
let mut runs: Vec<(usize, usize)> = Vec::new();
let width = if gray.is_empty() { 0 } else { gray[0].len() };
let mut col = 0;
while col < width {
let current = gray[row][col];
let mut run_len = 0;
while col < width && gray[row][col] == current {
run_len += 1;
col += 1;
}
runs.push((col - run_len, run_len));
}
// 找 5 连段符合 1:1:3:1:1 比例
let mut centers: Vec<(usize, usize)> = Vec::new();
for i in 0..runs.len().saturating_sub(4) {
let r0 = runs[i].1 as f32;
let r1 = runs[i + 1].1 as f32;
let r2 = runs[i + 2].1 as f32;
let r3 = runs[i + 3].1 as f32;
let r4 = runs[i + 4].1 as f32;
let avg = (r0 + r1 + r2 + r3 + r4) / 5.0;
if avg < 2.0 {
continue;
}
// 检查比例容差 ±40%
let tolerance = 0.4;
let check = |v: f32, expected: f32| (v - expected * avg).abs() < avg * tolerance;
if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) {
let cx = runs[i + 2].0 + runs[i + 2].1 / 2;
centers.push((cx, row));
}
}
centers
}
/// 垂直扫描查找 1:1:3:1:1 比例
fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
let height = gray.len();
let mut runs: Vec<(usize, usize)> = Vec::new();
let mut row = 0;
while row < height {
let current = gray[row][col];
let mut run_len = 0;
while row < height && gray[row][col] == current {
run_len += 1;
row += 1;
}
runs.push((row - run_len, run_len));
}
let mut centers: Vec<(usize, usize)> = Vec::new();
for i in 0..runs.len().saturating_sub(4) {
let r0 = runs[i].1 as f32;
let r1 = runs[i + 1].1 as f32;
let r2 = runs[i + 2].1 as f32;
let r3 = runs[i + 3].1 as f32;
let r4 = runs[i + 4].1 as f32;
let avg = (r0 + r1 + r2 + r3 + r4) / 5.0;
if avg < 2.0 {
continue;
}
let tolerance = 0.4;
let check = |v: f32, expected: f32| (v - expected * avg).abs() < avg * tolerance;
if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) {
let cy = runs[i + 2].0 + runs[i + 2].1 / 2;
centers.push((col, cy));
}
}
centers
}
/// 检测 3 个定位图案(交叉验证水平+垂直扫描)
fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
let height = gray.len();
let width = if height > 0 { gray[0].len() } else { 0 };
if width < 21 || height < 21 {
return None;
}
// 水平扫描
let mut h_centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size)
for row in (0..height).step_by(2) {
for (cx, cy) in scan_row(gray, row) {
// 交叉验证:垂直扫描
let v_matches = scan_col(gray, cx);
if v_matches
.iter()
.any(|&(_, vy)| (vy as i32 - cy as i32).abs() < 5)
{
let size = estimate_finder_size(gray, cx, cy);
h_centers.push((cx, cy, size));
}
}
}
if h_centers.len() < 3 {
return None;
}
// 聚类取前 3 个
let mut clusters: Vec<Vec<&(usize, usize, usize)>> = Vec::new();
for c in &h_centers {
let mut found = false;
for cluster in &mut clusters {
let avg_x: f64 = cluster.iter().map(|c| c.0 as f64).sum::<f64>() / cluster.len() as f64;
let avg_y: f64 = cluster.iter().map(|c| c.1 as f64).sum::<f64>() / cluster.len() as f64;
let dx = c.0 as f64 - avg_x;
let dy = c.1 as f64 - avg_y;
if (dx * dx + dy * dy).sqrt() < 20.0 {
cluster.push(c);
found = true;
break;
}
}
if !found {
clusters.push(vec![c]);
}
}
// 按聚类大小排序,取前 3
clusters.sort_by_key(|c| -(c.len() as i32));
if clusters.len() < 3 {
return None;
}
let mut finders: Vec<FinderMatch> = Vec::new();
for cluster in clusters.iter().take(3) {
let avg_x = cluster.iter().map(|c| c.0).sum::<usize>() / cluster.len();
let avg_y = cluster.iter().map(|c| c.1).sum::<usize>() / cluster.len();
let avg_size = cluster.iter().map(|c| c.2).sum::<usize>() / cluster.len();
finders.push(FinderMatch {
cx: avg_x,
cy: avg_y,
size: avg_size,
});
}
// 排序:左上、右上、左下
finders.sort_by(|a, b| {
let da = a.cx * a.cx + a.cy * a.cy; // 到原点的距离
let db = b.cx * b.cx + b.cy * b.cy;
da.cmp(&db)
});
// 区分右上(X 最大)和左下(Y 最大)
if finders[1].cx < finders[2].cx {
finders.swap(1, 2);
}
let f0 = finders.remove(0);
let f1 = finders.remove(0);
let f2 = finders.remove(0);
Some([f0, f1, f2])
}
/// 估算定位图案大小(像素)
fn estimate_finder_size(gray: &[Vec<bool>], cx: usize, cy: usize) -> usize {
// 从中心点水平扫描连续暗像素
let mut left = cx;
while left > 0 {
if cy < gray.len() && left < gray[0].len() && gray[cy][left] {
left -= 1;
} else {
break;
}
}
let mut right = cx;
while right + 1 < gray[0].len() {
if cy < gray.len() && gray[cy][right] {
right += 1;
} else {
break;
}
}
right - left
}
/// 从二值化图像中提取 QR 布尔矩阵
pub(crate) fn detect_and_extract(gray: &[Vec<bool>]) -> Result<DetectResult, String> {
let finders = find_finders(gray).ok_or("未找到 QR 码定位图案")?;
let tl = &finders[0]; // top-left
let tr = &finders[1]; // top-right
let _bl = &finders[2]; // bottom-left
// 估算模块大小
let module_size = (tl.size + tr.size) / 14; // finder = 7 modules wide
if module_size == 0 {
return Err("模块大小估算为零".into());
}
// 估算版本
let dx = tr.cx as f64 - tl.cx as f64;
let dy = tr.cy as f64 - tl.cy as f64;
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;
let version = ver.clamp(1, 40);
let size = 17 + version as usize * 4;
// 从采样网格构建模块矩阵
let mut modules: Vec<Vec<bool>> = Vec::with_capacity(size);
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;
let px = px.round() as i32;
let py = py.round() as i32;
let sample = if px >= 0
&& py >= 0
&& (py as usize) < gray.len()
&& (px as usize) < gray[0].len()
{
gray[py as usize][px as usize]
} else {
false
};
row.push(sample);
}
modules.push(row);
}
Ok(DetectResult {
modules,
version_estimate: version,
})
}