Compare commits
3 Commits
5651dbf732
...
ce9c8b1b6e
| Author | SHA1 | Date | |
|---|---|---|---|
| ce9c8b1b6e | |||
| 309c9429ea | |||
| a03ab95ce5 |
+1
-1
@@ -266,7 +266,7 @@ fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result<String> {
|
|||||||
let pwd = opts
|
let pwd = opts
|
||||||
.password
|
.password
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.or_else(|| env_pwd.as_deref())
|
.or(env_pwd.as_deref())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
Ok(text_builder::build_wifi_text(
|
Ok(text_builder::build_wifi_text(
|
||||||
s,
|
s,
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ mod tests {
|
|||||||
result.is_some(),
|
result.is_some(),
|
||||||
"decode failed: ec_bits={ec_bits}, mask={mask}, encoded={encoded:#05X}"
|
"decode failed: ec_bits={ec_bits}, mask={mask}, encoded={encoded:#05X}"
|
||||||
);
|
);
|
||||||
let (dec_ec, dec_mask) = result.unwrap();
|
let (dec_ec, dec_mask, _d) = result.unwrap();
|
||||||
assert_eq!(ec_bits, dec_ec, "ec_bits mismatch");
|
assert_eq!(ec_bits, dec_ec, "ec_bits mismatch");
|
||||||
assert_eq!(mask, dec_mask, "mask mismatch");
|
assert_eq!(mask, dec_mask, "mask mismatch");
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,8 @@ mod tests {
|
|||||||
result.is_some(),
|
result.is_some(),
|
||||||
"decode failed: ver={ver}, encoded={encoded:#010X}"
|
"decode failed: ver={ver}, encoded={encoded:#010X}"
|
||||||
);
|
);
|
||||||
assert_eq!(ver, result.unwrap(), "version mismatch");
|
let (dec_ver, _d) = result.unwrap();
|
||||||
|
assert_eq!(ver, dec_ver, "version mismatch");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,12 +144,11 @@ mod tests {
|
|||||||
for ec_bits in 0u8..4 {
|
for ec_bits in 0u8..4 {
|
||||||
for mask in 0u8..8 {
|
for mask in 0u8..8 {
|
||||||
let original = encode_format_info(ec_bits, mask);
|
let original = encode_format_info(ec_bits, mask);
|
||||||
// 翻转每个比特,验证能纠错
|
|
||||||
for bit in 0..15 {
|
for bit in 0..15 {
|
||||||
let corrupted = original ^ (1 << bit);
|
let corrupted = original ^ (1 << bit);
|
||||||
let result = decode_format_info(corrupted);
|
let result = decode_format_info(corrupted);
|
||||||
assert!(result.is_some(), "1-bit error not corrected at bit {bit}");
|
assert!(result.is_some(), "1-bit error not corrected at bit {bit}");
|
||||||
let (dec_ec, dec_mask) = result.unwrap();
|
let (dec_ec, dec_mask, _d) = result.unwrap();
|
||||||
assert_eq!(ec_bits, dec_ec);
|
assert_eq!(ec_bits, dec_ec);
|
||||||
assert_eq!(mask, dec_mask);
|
assert_eq!(mask, dec_mask);
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,8 @@ mod tests {
|
|||||||
let corrupted = original ^ (1 << bit);
|
let corrupted = original ^ (1 << bit);
|
||||||
let result = decode_version_info(corrupted);
|
let result = decode_version_info(corrupted);
|
||||||
assert!(result.is_some(), "1-bit error not corrected at bit {bit}");
|
assert!(result.is_some(), "1-bit error not corrected at bit {bit}");
|
||||||
assert_eq!(ver, result.unwrap());
|
let (dec_ver, _d) = result.unwrap();
|
||||||
|
assert_eq!(ver, dec_ver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-52
@@ -21,7 +21,7 @@ pub(crate) struct DetectResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 水平扫描查找 1:1:3:1:1 比例
|
/// 水平扫描查找 1:1:3:1:1 比例
|
||||||
fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
|
fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize, usize)> {
|
||||||
// (列号,运行长度)
|
// (列号,运行长度)
|
||||||
let mut runs: Vec<(usize, usize)> = Vec::new();
|
let mut runs: Vec<(usize, usize)> = Vec::new();
|
||||||
let width = if gray.is_empty() { 0 } else { gray[0].len() };
|
let width = if gray.is_empty() { 0 } else { gray[0].len() };
|
||||||
@@ -37,8 +37,8 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
|
|||||||
runs.push((col - run_len, run_len));
|
runs.push((col - run_len, run_len));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找 5 连段符合 1:1:3:1:1 比例
|
// 找 5 连段符合 1:1:3:1:1 比例 — 返回 (cx, cy, total_size_px)
|
||||||
let mut centers: Vec<(usize, usize)> = Vec::new();
|
let mut centers: Vec<(usize, usize, usize)> = Vec::new();
|
||||||
for i in 0..runs.len().saturating_sub(4) {
|
for i in 0..runs.len().saturating_sub(4) {
|
||||||
let r0 = runs[i].1 as f32;
|
let r0 = runs[i].1 as f32;
|
||||||
let r1 = runs[i + 1].1 as f32;
|
let r1 = runs[i + 1].1 as f32;
|
||||||
@@ -46,18 +46,19 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
|
|||||||
let r3 = runs[i + 3].1 as f32;
|
let r3 = runs[i + 3].1 as f32;
|
||||||
let r4 = runs[i + 4].1 as f32;
|
let r4 = runs[i + 4].1 as f32;
|
||||||
|
|
||||||
let avg = (r0 + r1 + r2 + r3 + r4) / 5.0;
|
let total = r0 + r1 + r2 + r3 + r4;
|
||||||
if avg < 2.0 {
|
let base = total / 7.0;
|
||||||
|
if base < 2.0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let tolerance = 0.5; // ZXing 标准:moduleSize/2 容差
|
||||||
// 检查比例容差 ±40%
|
let check = |v: f32, expected: f32| (v - expected * base).abs() < base * tolerance;
|
||||||
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) {
|
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;
|
// ZXing 的 end-based 中心公式: end - r4 - r3 - r2/2 (浮点精度更高)
|
||||||
centers.push((cx, row));
|
let end = runs[i + 4].0 + runs[i + 4].1;
|
||||||
|
let cx = end as f32 - r4 - r3 - r2 / 2.0;
|
||||||
|
centers.push((cx as usize, row, total as usize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 垂直扫描查找 1:1:3:1:1 比例
|
/// 垂直扫描查找 1:1:3:1:1 比例
|
||||||
fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
|
fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize, usize)> {
|
||||||
let height = gray.len();
|
let height = gray.len();
|
||||||
let mut runs: Vec<(usize, usize)> = Vec::new();
|
let mut runs: Vec<(usize, usize)> = Vec::new();
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
|
|||||||
runs.push((row - run_len, run_len));
|
runs.push((row - run_len, run_len));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut centers: Vec<(usize, usize)> = Vec::new();
|
let mut centers: Vec<(usize, usize, usize)> = Vec::new();
|
||||||
for i in 0..runs.len().saturating_sub(4) {
|
for i in 0..runs.len().saturating_sub(4) {
|
||||||
let r0 = runs[i].1 as f32;
|
let r0 = runs[i].1 as f32;
|
||||||
let r1 = runs[i + 1].1 as f32;
|
let r1 = runs[i + 1].1 as f32;
|
||||||
@@ -88,17 +89,19 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
|
|||||||
let r3 = runs[i + 3].1 as f32;
|
let r3 = runs[i + 3].1 as f32;
|
||||||
let r4 = runs[i + 4].1 as f32;
|
let r4 = runs[i + 4].1 as f32;
|
||||||
|
|
||||||
let avg = (r0 + r1 + r2 + r3 + r4) / 5.0;
|
let total = r0 + r1 + r2 + r3 + r4;
|
||||||
if avg < 2.0 {
|
let base = total / 7.0;
|
||||||
|
if base < 2.0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tolerance = 0.4;
|
let tolerance = 0.5; // ZXing 标准:moduleSize/2 容差
|
||||||
let check = |v: f32, expected: f32| (v - expected * avg).abs() < avg * tolerance;
|
let check = |v: f32, expected: f32| (v - expected * base).abs() < base * tolerance;
|
||||||
|
|
||||||
if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) {
|
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;
|
let end = runs[i + 4].0 + runs[i + 4].1;
|
||||||
centers.push((col, cy));
|
let cy = end as f32 - r4 - r3 - r2 / 2.0;
|
||||||
|
centers.push((col, cy as usize, total as usize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +145,7 @@ pub(crate) fn estimate_version_from_tl_tr(tl: (f64, f64), tr: (f64, f64), module
|
|||||||
let dy = tr.1 - tl.1;
|
let dy = tr.1 - tl.1;
|
||||||
let dist_px = (dx * dx + dy * dy).sqrt() as f32;
|
let dist_px = (dx * dx + dy * dy).sqrt() as f32;
|
||||||
let dist_modules = dist_px / module_size as f32;
|
let dist_modules = dist_px / module_size as f32;
|
||||||
let ver = ((dist_modules as i32 - 14) / 4) as u8;
|
let ver = ((dist_modules as i32 - 10) / 4) as u8;
|
||||||
ver.clamp(1, 40)
|
ver.clamp(1, 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,18 +157,17 @@ fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 水平扫描
|
// 水平扫描(含 finder 总尺寸)
|
||||||
let mut h_centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size)
|
let mut h_centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size)
|
||||||
for row in (0..height).step_by(2) {
|
for row in (0..height).step_by(2) {
|
||||||
for (cx, cy) in scan_row(gray, row) {
|
for (cx, cy, total_size) in scan_row(gray, row) {
|
||||||
// 交叉验证:垂直扫描
|
// 交叉验证:垂直扫描
|
||||||
let v_matches = scan_col(gray, cx);
|
let v_matches = scan_col(gray, cx);
|
||||||
if v_matches
|
if v_matches
|
||||||
.iter()
|
.iter()
|
||||||
.any(|&(_, vy)| (vy as i32 - cy as i32).abs() < 5)
|
.any(|&(_, vy, _)| (vy as i32 - cy as i32).abs() < 5)
|
||||||
{
|
{
|
||||||
let size = estimate_finder_size(gray, cx, cy);
|
h_centers.push((cx, cy, total_size));
|
||||||
h_centers.push((cx, cy, size));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,43 +217,66 @@ fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
|
|||||||
|
|
||||||
// 排序:左上、右上、左下
|
// 排序:左上、右上、左下
|
||||||
finders.sort_by(|a, b| {
|
finders.sort_by(|a, b| {
|
||||||
let da = a.cx * a.cx + a.cy * a.cy; // 到原点的距离
|
let da = a.cx * a.cx + a.cy * a.cy;
|
||||||
let db = b.cx * b.cx + b.cy * b.cy;
|
let db = b.cx * b.cx + b.cy * b.cy;
|
||||||
da.cmp(&db)
|
da.cmp(&db)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 区分右上(X 最大)和左下(Y 最大)
|
|
||||||
if finders[1].cx < finders[2].cx {
|
if finders[1].cx < finders[2].cx {
|
||||||
finders.swap(1, 2);
|
finders.swap(1, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove 会使后续元素前移,必须每次 remove(0)
|
||||||
let f0 = finders.remove(0);
|
let f0 = finders.remove(0);
|
||||||
let f1 = finders.remove(0);
|
let f1 = finders.remove(0);
|
||||||
let f2 = finders.remove(0);
|
let f2 = finders.remove(0);
|
||||||
|
|
||||||
|
// ZXing 几何验证:module_size 一致性 + 勾股定理
|
||||||
|
if !validate_finder_geometry(&[&f0, &f1, &f2]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
Some([f0, f1, f2])
|
Some([f0, f1, f2])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 估算定位图案大小(像素)
|
/// ZXing 风格几何验证:3 个定位图案的 module_size 必须一致且构成近似直角三角形
|
||||||
fn estimate_finder_size(gray: &[Vec<bool>], cx: usize, cy: usize) -> usize {
|
fn validate_finder_geometry(finders: &[&FinderMatch; 3]) -> bool {
|
||||||
// 从中心点水平扫描连续暗像素
|
let s0 = finders[0].size as f64;
|
||||||
let mut left = cx;
|
let s1 = finders[1].size as f64;
|
||||||
while left > 0 {
|
let s2 = finders[2].size as f64;
|
||||||
if cy < gray.len() && left < gray[0].len() && gray[cy][left] {
|
|
||||||
left -= 1;
|
// module_size 一致性: 最大偏差 < 10%
|
||||||
} else {
|
let avg_size = (s0 + s1 + s2) / 3.0;
|
||||||
break;
|
let max_dev = avg_size * 0.10;
|
||||||
}
|
if (s0 - avg_size).abs() > max_dev
|
||||||
|
|| (s1 - avg_size).abs() > max_dev
|
||||||
|
|| (s2 - avg_size).abs() > max_dev
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
let mut right = cx;
|
|
||||||
while right + 1 < gray[0].len() {
|
// 勾股定理验证: |C - sqrt(A² + B²)| / min(C, sqrt(A² + B²)) < 15%
|
||||||
if cy < gray.len() && gray[cy][right] {
|
let ax = finders[1].cx as f64 - finders[0].cx as f64;
|
||||||
right += 1;
|
let ay = finders[1].cy as f64 - finders[0].cy as f64;
|
||||||
} else {
|
let bx = finders[2].cx as f64 - finders[0].cx as f64;
|
||||||
break;
|
let by = finders[2].cy as f64 - finders[0].cy as f64;
|
||||||
}
|
|
||||||
|
let a_len = (ax * ax + ay * ay).sqrt();
|
||||||
|
let b_len = (bx * bx + by * by).sqrt();
|
||||||
|
let c_len = {
|
||||||
|
let cx = finders[2].cx as f64 - finders[1].cx as f64;
|
||||||
|
let cy = finders[2].cy as f64 - finders[1].cy as f64;
|
||||||
|
(cx * cx + cy * cy).sqrt()
|
||||||
|
};
|
||||||
|
|
||||||
|
let hypotenuse = (a_len * a_len + b_len * b_len).sqrt();
|
||||||
|
let min = c_len.min(hypotenuse);
|
||||||
|
if min < 1.0 {
|
||||||
|
return true; // 太小,跳过检查
|
||||||
}
|
}
|
||||||
right - left
|
let deviation = (c_len - hypotenuse).abs() / min;
|
||||||
|
|
||||||
|
deviation < 0.15
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从二值化图像中提取 QR 布尔矩阵
|
/// 从二值化图像中提取 QR 布尔矩阵
|
||||||
@@ -279,7 +304,9 @@ pub(crate) fn detect_and_extract(
|
|||||||
let dy = tr.cy as f64 - tl.cy as f64;
|
let dy = tr.cy as f64 - tl.cy as f64;
|
||||||
let dist_px = (dx * dx + dy * dy).sqrt() as f32;
|
let dist_px = (dx * dx + dy * dy).sqrt() as f32;
|
||||||
let dist_modules = dist_px / module_size as f32;
|
let dist_modules = dist_px / module_size as f32;
|
||||||
let ver = ((dist_modules as i32 - 14) / 4) as u8;
|
// ZXing 公式: totalModules = dist/moduleSize + 7, version = (totalModules - 17) / 4
|
||||||
|
// 简化为: version = (dist_modules + 7 - 17) / 4 = (dist_modules - 10) / 4
|
||||||
|
let ver = ((dist_modules as i32 - 10) / 4) as u8;
|
||||||
let version = ver.clamp(1, 40);
|
let version = ver.clamp(1, 40);
|
||||||
|
|
||||||
let size = 17 + version as usize * 4;
|
let size = 17 + version as usize * 4;
|
||||||
@@ -360,7 +387,7 @@ fn check_quiet_zone(modules: &[Vec<bool>], size: usize) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 计算定位图案比例偏差(与理想 1:1:3:1:1 的偏差,0=完美, 1=最大)
|
/// 计算定位图案比例偏差(0=完美, 1=最大),使用 base=total/7 对齐 ZXing
|
||||||
fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
|
fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
|
||||||
let row = finder.cy;
|
let row = finder.cy;
|
||||||
let mut runs: Vec<usize> = Vec::new();
|
let mut runs: Vec<usize> = Vec::new();
|
||||||
@@ -382,14 +409,14 @@ fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
|
|||||||
|
|
||||||
let mut best_dev = 1.0f64;
|
let mut best_dev = 1.0f64;
|
||||||
for i in 0..runs.len().saturating_sub(4) {
|
for i in 0..runs.len().saturating_sub(4) {
|
||||||
let avg =
|
let total = (runs[i] + runs[i + 1] + runs[i + 2] + runs[i + 3] + runs[i + 4]) as f64;
|
||||||
(runs[i] + runs[i + 1] + runs[i + 2] + runs[i + 3] + runs[i + 4]) as f64 / 5.0;
|
let base = total / 7.0;
|
||||||
if avg < 2.0 {
|
if base < 2.0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let expected = [avg, avg, 3.0 * avg, avg, avg];
|
let expected = [base, base, 3.0 * base, base, base];
|
||||||
let dev: f64 = (0..5)
|
let dev: f64 = (0..5)
|
||||||
.map(|j| (runs[j] as f64 - expected[j]).abs() / avg)
|
.map(|j| (runs[i + j] as f64 - expected[j]).abs() / base)
|
||||||
.sum::<f64>()
|
.sum::<f64>()
|
||||||
/ 5.0;
|
/ 5.0;
|
||||||
if dev < best_dev {
|
if dev < best_dev {
|
||||||
|
|||||||
@@ -32,11 +32,8 @@ fn extract_bits(matrix: &Matrix, total_codewords: usize) -> Vec<bool> {
|
|||||||
}
|
}
|
||||||
col -= 2;
|
col -= 2;
|
||||||
going_up = !going_up;
|
going_up = !going_up;
|
||||||
|
// 垂直时序图案列(col 6)由 read_module 自动跳过保留区,
|
||||||
// 跳过垂直时序图案列(col 6)
|
// 无需显式 skip,否则会导致列配对错位、行序反转
|
||||||
if col == 6 {
|
|
||||||
col -= 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bits.truncate(target_bits);
|
bits.truncate(target_bits);
|
||||||
|
|||||||
+14
-16
@@ -113,21 +113,19 @@ fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec<Vec<bool>> {
|
|||||||
let r = 128.0f64;
|
let r = 128.0f64;
|
||||||
|
|
||||||
// 预计算积分图像以加速窗口和/平方和
|
// 预计算积分图像以加速窗口和/平方和
|
||||||
let mut integral = vec![0u64; (w as usize + 1) * (h as usize + 1)];
|
// 标准公式: I(y+1,x+1) = I(y,x+1) + I(y+1,x) - I(y,x) + pixel(y,x)
|
||||||
let mut sq_integral = vec![0u64; (w as usize + 1) * (h as usize + 1)];
|
let stride = w as usize + 1;
|
||||||
|
let mut integral = vec![0u64; stride * (h as usize + 1)];
|
||||||
|
let mut sq_integral = vec![0u64; stride * (h as usize + 1)];
|
||||||
for y in 0..h as usize {
|
for y in 0..h as usize {
|
||||||
let mut row_sum = 0u64;
|
|
||||||
let mut row_sq = 0u64;
|
|
||||||
for x in 0..w as usize {
|
for x in 0..w as usize {
|
||||||
let p = gray.get_pixel(x as u32, y as u32).0[0] as u64;
|
let p = gray.get_pixel(x as u32, y as u32).0[0] as u64;
|
||||||
row_sum += p;
|
let idx = (y + 1) * stride + (x + 1);
|
||||||
row_sq += p * p;
|
let above = y * stride + (x + 1);
|
||||||
let idx = (y + 1) * (w as usize + 1) + (x + 1);
|
let left = (y + 1) * stride + x;
|
||||||
let above = (y) * (w as usize + 1) + (x + 1);
|
let above_left = y * stride + x;
|
||||||
let left = (y + 1) * (w as usize + 1) + (x);
|
integral[idx] = integral[above] + integral[left] - integral[above_left] + p;
|
||||||
let above_left = (y) * (w as usize + 1) + (x);
|
sq_integral[idx] = sq_integral[above] + sq_integral[left] - sq_integral[above_left] + p * p;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +136,10 @@ fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec<Vec<bool>> {
|
|||||||
let y2 = (y + half + 1).min(h 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 idx = |xx: usize, yy: usize| yy * (w as usize + 1) + xx;
|
||||||
let count = ((x2 - x1) * (y2 - y1)) as f64;
|
let count = ((x2 - x1) * (y2 - y1)) as f64;
|
||||||
let sum = (integral[idx(x2, y2)] - integral[idx(x1, y2)] - integral[idx(x2, y1)]
|
let sum = (integral[idx(x2, y2)] as i64 - integral[idx(x1, y2)] as i64
|
||||||
+ integral[idx(x1, y1)]) as f64;
|
- integral[idx(x2, y1)] as i64 + integral[idx(x1, y1)] as i64) as f64;
|
||||||
let sq_sum = (sq_integral[idx(x2, y2)] - sq_integral[idx(x1, y2)] - sq_integral[idx(x2, y1)]
|
let sq_sum = (sq_integral[idx(x2, y2)] as i64 - sq_integral[idx(x1, y2)] as i64
|
||||||
+ sq_integral[idx(x1, y1)]) as f64;
|
- sq_integral[idx(x2, y1)] as i64 + sq_integral[idx(x1, y1)] as i64) as f64;
|
||||||
let mean = sum / count;
|
let mean = sum / count;
|
||||||
let variance = (sq_sum / count - mean * mean).max(0.0);
|
let variance = (sq_sum / count - mean * mean).max(0.0);
|
||||||
(mean, variance.sqrt())
|
(mean, variance.sqrt())
|
||||||
|
|||||||
@@ -143,9 +143,10 @@ fn count_finder_hits(gray: &[Vec<bool>]) -> usize {
|
|||||||
let (mut col,mut runs)=(0,vec![]);
|
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); }
|
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) {
|
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;
|
let total=(runs[i]+runs[i+1]+runs[i+2]+runs[i+3]+runs[i+4])as f64;
|
||||||
if avg<2.0{continue;}
|
let base=total/7.0;
|
||||||
let ck=|v:f64,e:f64|(v-e*avg).abs()<avg*0.4;
|
if base<2.0{continue;}
|
||||||
|
let ck=|v:f64,e:f64|(v-e*base).abs()<base*0.5;
|
||||||
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;}
|
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;}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,10 +324,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unicode_to_shift_jis_known() {
|
fn test_unicode_to_shift_jis_known() {
|
||||||
// 基本汉字应返回 Some
|
// 基本汉字应返回 Some(encoding_rs 精确映射)
|
||||||
assert!(unicode_to_shift_jis('中').is_some());
|
assert!(unicode_to_shift_jis('中').is_some(), "中 should map");
|
||||||
assert!(unicode_to_shift_jis('文').is_some());
|
assert!(unicode_to_shift_jis('文').is_some(), "文 should map");
|
||||||
assert!(unicode_to_shift_jis('你').is_some());
|
assert!(unicode_to_shift_jis('日').is_some(), "日 should map");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -305,13 +305,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_score_rule2() {
|
fn test_score_rule2() {
|
||||||
let mut m = Matrix::new(3);
|
let mut m = Matrix::new(3);
|
||||||
|
// 全部设为 dark → 4 个 2×2 同色方块
|
||||||
for y in 0..3u8 {
|
for y in 0..3u8 {
|
||||||
for x in 0..3u8 {
|
for x in 0..3u8 {
|
||||||
m.set(x, y, true);
|
m.set(x, y, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let view = MaskedView::new(&m, 0);
|
// 使用 raw 版(不经过掩码)验证
|
||||||
assert_eq!(score_rule2(&view), 4 * 3);
|
assert_eq!(score_rule2_raw(&m), 4 * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -419,4 +419,26 @@ mod tests {
|
|||||||
assert!(svg.contains("#FF0000"));
|
assert!(svg.contains("#FF0000"));
|
||||||
assert!(svg.contains("#0000FF"));
|
assert!(svg.contains("#0000FF"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_roundtrip_png() {
|
||||||
|
let config = QrConfig {
|
||||||
|
level: EcLevel::M,
|
||||||
|
version: VersionMode::Auto,
|
||||||
|
margin: 4,
|
||||||
|
};
|
||||||
|
let qr = QrCode::encode("Hello World", config).unwrap();
|
||||||
|
|
||||||
|
// 直接从矩阵解码
|
||||||
|
let matrix: Vec<Vec<bool>> = qr.modules().to_vec();
|
||||||
|
let result = crate::decoder::decode_matrix(&matrix)
|
||||||
|
.expect("矩阵解码往返失败");
|
||||||
|
assert_eq!(result.text, "Hello World");
|
||||||
|
|
||||||
|
// PNG 往返
|
||||||
|
let png_bytes = qr.to_png_bytes(8, None).unwrap();
|
||||||
|
let result = crate::decoder::decode_image(&png_bytes)
|
||||||
|
.expect("PNG 解码往返失败");
|
||||||
|
assert_eq!(result.text, "Hello World");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -69,7 +69,7 @@ fn generate_qr_inner(text: &str, level_str: &str, margin: u8, size: u8, fmt: &st
|
|||||||
if margin > 20 {
|
if margin > 20 {
|
||||||
return (StatusCode::BAD_REQUEST, "边距过大(最大 20)").into_response();
|
return (StatusCode::BAD_REQUEST, "边距过大(最大 20)").into_response();
|
||||||
}
|
}
|
||||||
if size < 1 || size > 20 {
|
if !(1..=20).contains(&size) {
|
||||||
return (StatusCode::BAD_REQUEST, "模块大小需在 1-20 之间").into_response();
|
return (StatusCode::BAD_REQUEST, "模块大小需在 1-20 之间").into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,6 @@ fn generate_qr_inner(text: &str, level_str: &str, margin: u8, size: u8, fmt: &st
|
|||||||
level,
|
level,
|
||||||
version: VersionMode::Auto,
|
version: VersionMode::Auto,
|
||||||
margin,
|
margin,
|
||||||
..Default::default()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let qr = match QrCode::encode(text, config) {
|
let qr = match QrCode::encode(text, config) {
|
||||||
|
|||||||
Reference in New Issue
Block a user