86d788e57c
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 前端全部通过
161 lines
4.6 KiB
Rust
161 lines
4.6 KiB
Rust
//! 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
|
|
}
|