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:
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user