feat: vCard 扩展 + 格式扩展 + 解码透视矫正 — v0.3.0
Phase 1: 格式扩展 - png.rs → image.rs,OutputFormat 枚举 (PNG/BMP/JPEG/WebP) - CLI -f/--format,Web fmt 参数扩展,image crate +bmp feature Phase 2: 解码增强 - 新增 decoder/perspective.rs — 旋转矫正(MVP) - auto_correct: finder 检测→计算旋转角→仿射变换→再解码 - decode_image 自动重试矫正流水线 Phase 3: vCard 扩展 - 新增 5 字段:TITLE/URL/BDAY/NOTE/PHOTO - Rust text_builder + TS qrText + VCardMode UI 同步 - CLI 新增 --title --vcard-url --birthday --note --photo - 中/英 i18n 翻译 测试: 81 Rust + 19 前端全部通过
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
//! QR 码图像透视矫正
|
||||
//!
|
||||
//! 检测定位图案后,计算旋转角并矫正图像。
|
||||
//! MVP 版本:仅做旋转矫正(仿射变换),不做完整单应变换。
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
// 尝试找到至少 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(
|
||||
gray: &[Vec<bool>],
|
||||
tl: (usize, usize),
|
||||
tr: (usize, usize),
|
||||
) -> Vec<Vec<bool>> {
|
||||
let h = gray.len();
|
||||
let w = if h > 0 {
|
||||
gray[0].len()
|
||||
} else {
|
||||
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); // 正值 = 顺时针偏离水平
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user