feat: 解码器全面增强 — 多遍二值化 + 4点透视 + ECI/FNC1

Phase 1 — 二值化+检测增强:
- decoder/image.rs 重写: 中位数→Otsu→Sauvola 三遍自适应阈值
- 新增 load_gray() 公共 API 供调用方自定义预处理
- 反色QR码自动检测(5点采样法)
- 静区验证 + 包围盒(BoundingBox) + 解码置信度评分
- 定位图案比例偏差计算

Phase 2 — 4点透视变换+多QR码:
- decoder/perspective.rs 重写: DLT求解3x3单应矩阵
- 用3个定位图案+推算第4点构建4点对应
- 双线性插值替换最近邻采样
- 多角度旋转回退(7个预设角度)
- decode_all() 返回图像中所有QR码(按置信度降序)

Phase 3 — ISO合规:
- decoder/bch.rs: decode_format_info/version_info 返回汉明距离
- decoder/format.rs: 副本冲突时选汉明距离更小者
- decoder/mode_decode.rs: ECI指派号识别, FNC1/GS1检测(0101/1001)
- 结构化追加(0011)头部提取: position/total/parity
- DecodeResult 扩展: is_gs1, eci_designator, structured_append

验证: cargo check+clippy通过, 81+24测试全部通过
This commit is contained in:
2026-06-21 16:00:11 +08:00
parent cd75141037
commit bd4ca718ac
9 changed files with 971 additions and 252 deletions
+6 -10
View File
@@ -80,30 +80,26 @@ fn build_version_table() -> &'static [VersionEntry] {
})
}
/// BCH(15,5) 解码:从 15-bit 原始值恢复 (ec_bits, mask)
///
/// *若汉明距离 ≤ 3 则返回 Some,否则 None*
pub(crate) fn decode_format_info(raw: u16) -> Option<(u8, u8)> {
/// BCH(15,5) 解码:返回 (ec_bits, mask, hamming_distance)
pub(crate) fn decode_format_info(raw: u16) -> Option<(u8, u8, u32)> {
let table = build_format_table();
table
.iter()
.map(|&(code, ec, mask)| (hamming_distance_15(raw, code), ec, mask))
.min_by_key(|&(d, _, _)| d)
.filter(|&(d, _, _)| d <= 3)
.map(|(_, ec, mask)| (ec, mask))
.map(|(d, ec, mask)| (ec, mask, d))
}
/// BCH(18,6) 解码:从 18-bit 原始值恢复版本号
///
/// *若汉明距离 ≤ 3 则返回 Some,否则 None*
pub(crate) fn decode_version_info(raw: u32) -> Option<u8> {
/// BCH(18,6) 解码:返回 (version, hamming_distance)
pub(crate) fn decode_version_info(raw: u32) -> Option<(u8, u32)> {
let table = build_version_table();
table
.iter()
.map(|&(code, ver)| (hamming_distance_18(raw, code), ver))
.min_by_key(|&(d, _)| d)
.filter(|&(d, _)| d <= 3)
.map(|(_, ver)| ver)
.map(|(d, ver)| (ver, d))
}
#[cfg(test)]
+128 -1
View File
@@ -15,6 +15,9 @@ struct FinderMatch {
pub(crate) struct DetectResult {
pub modules: Vec<Vec<bool>>, // 布尔矩阵(含静区)
pub version_estimate: u8,
pub bounds: crate::decoder::BoundingBox,
pub quiet_zone_ok: bool,
pub finder_ratio_deviation: f64,
}
/// 水平扫描查找 1:1:3:1:1 比例
@@ -102,6 +105,47 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
centers
}
/// 检测 3 个定位图案,返回 (cx, cy) 像素坐标对(供透视矫正使用)
pub(crate) fn find_finders_for_perspective(
gray: &[Vec<bool>],
) -> Result<[(usize, usize); 3], crate::error::QrError> {
let finders = find_finders(gray)
.ok_or_else(|| crate::error::QrError::DecodeFail("未找到 QR 码定位图案".into()))?;
Ok([
(finders[0].cx, finders[0].cy),
(finders[1].cx, finders[1].cy),
(finders[2].cx, finders[2].cy),
])
}
/// 从定位图案估算模块大小(像素/模块)
pub(crate) fn estimate_module_size_from_finders(
_gray: &[Vec<bool>],
finders: &[(usize, usize); 3],
) -> usize {
// 用 TL-TR 和 TL-BL 距离估算
let dx1 = (finders[1].0 as f64 - finders[0].0 as f64).abs();
let dy1 = (finders[1].1 as f64 - finders[0].1 as f64).abs();
let dx2 = (finders[2].0 as f64 - finders[0].0 as f64).abs();
let dy2 = (finders[2].1 as f64 - finders[0].1 as f64).abs();
let dist1 = (dx1 * dx1 + dy1 * dy1).sqrt();
let dist2 = (dx2 * dx2 + dy2 * dy2).sqrt();
let avg_dist = (dist1 + dist2) / 2.0;
// 版本 1: dist ≈ 14 modules (7+1-1+7 = 14)
// 我们并不知道确切版本,使用 iter 近似
(avg_dist / 14.0).max(1.0) as usize
}
/// 从 TL-TR 距离估算版本号
pub(crate) fn estimate_version_from_tl_tr(tl: (f64, f64), tr: (f64, f64), module_size: usize) -> u8 {
let dx = tr.0 - tl.0;
let dy = tr.1 - tl.1;
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;
ver.clamp(1, 40)
}
/// 检测 3 个定位图案(交叉验证水平+垂直扫描)
fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
let height = gray.len();
@@ -245,7 +289,6 @@ pub(crate) fn detect_and_extract(
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;
@@ -266,8 +309,92 @@ pub(crate) fn detect_and_extract(
modules.push(row);
}
// 静区验证:检查矩阵四边各 4 行/列是否为白色
let quiet_zone_ok = check_quiet_zone(&modules, size);
// 包围盒:从 3 个定位图案推算 QR 码在图像中的像素区域
let margin_px = (module_size as u32).saturating_mul(4);
let x_min = tl.cx.saturating_sub(3 * module_size) as u32;
let y_min = tl.cy.saturating_sub(3 * module_size) as u32;
let x_max = ((tr.cx + 3 * module_size) as u32).saturating_add(margin_px);
let y_max = ((_bl.cy + 3 * module_size) as u32).saturating_add(margin_px);
let bounds = crate::decoder::BoundingBox {
x: x_min.saturating_sub(margin_px),
y: y_min.saturating_sub(margin_px),
width: x_max.saturating_sub(x_min).saturating_add(2 * margin_px),
height: y_max.saturating_sub(y_min).saturating_add(2 * margin_px),
};
// 定位图案比例偏差:实际 5 段比例与 1:1:3:1:1 的偏差
let finder_ratio_deviation = compute_finder_deviation(gray, tl);
Ok(DetectResult {
modules,
version_estimate: version,
bounds,
quiet_zone_ok,
finder_ratio_deviation,
})
}
/// 检查静区:矩阵四边各 4 行/列是否为全白色
fn check_quiet_zone(modules: &[Vec<bool>], size: usize) -> bool {
let quiet = 4usize;
// 上边
if modules.iter().take(quiet.min(size)).any(|row| row.iter().any(|&m| m)) {
return false;
}
// 下边
if modules.iter().skip(size.saturating_sub(quiet)).any(|row| row.iter().any(|&m| m)) {
return false;
}
// 左边 & 右边
for row in modules.iter() {
if row.iter().take(quiet.min(size)).any(|&m| m) {
return false;
}
if row.iter().skip(size.saturating_sub(quiet)).any(|&m| m) {
return false;
}
}
true
}
/// 计算定位图案比例偏差(与理想 1:1:3:1:1 的偏差,0=完美, 1=最大)
fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
let row = finder.cy;
let mut runs: Vec<usize> = Vec::new();
let mut col = finder.cx.saturating_sub(finder.size);
let end = (finder.cx + finder.size).min(gray[0].len());
while col < end {
let current = gray[row][col];
let mut run_len = 0;
while col < end && gray[row][col] == current {
run_len += 1;
col += 1;
}
runs.push(run_len);
}
if runs.len() < 5 {
return 1.0;
}
let mut best_dev = 1.0f64;
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;
if avg < 2.0 {
continue;
}
let expected = [avg, avg, 3.0 * avg, avg, avg];
let dev: f64 = (0..5)
.map(|j| (runs[j] as f64 - expected[j]).abs() / avg)
.sum::<f64>()
/ 5.0;
if dev < best_dev {
best_dev = dev;
}
}
best_dev
}
+10 -14
View File
@@ -104,18 +104,17 @@ pub(crate) fn read_format_info(matrix: &Matrix) -> Result<(EcLevel, u8), crate::
let decode_fail =
|| crate::error::QrError::FormatCorrupted("格式信息解码失败:两处副本均无法纠错".into());
// 偏好成功解码的结果
// 用汉明距离选择更可靠的副本
match (dec1, dec2) {
(Some((ec1, m1)), Some((ec2, m2))) if (ec1, m1) == (ec2, m2) => {
(Some((ec1, m1, _)), Some((ec2, m2, _))) if (ec1, m1) == (ec2, m2) => {
ec_from_bits(ec1).map(|lvl| (lvl, m1)).ok_or_else(corrupt_err)
}
(Some((ec1, m1)), Some((_, _))) => {
// 两处不一致 — 偏好副本 1
ec_from_bits(ec1)
.map(|lvl| (lvl, m1))
.ok_or_else(corrupt_err)
(Some((ec1, m1, d1)), Some((ec2, m2, d2))) => {
// 两处不一致 → 选汉明距离更小的副本
let (ec, m) = if d1 <= d2 { (ec1, m1) } else { (ec2, m2) };
ec_from_bits(ec).map(|lvl| (lvl, m)).ok_or_else(corrupt_err)
}
(Some((ec, m)), None) | (None, Some((ec, m))) => ec_from_bits(ec)
(Some((ec, m, _)), None) | (None, Some((ec, m, _))) => ec_from_bits(ec)
.map(|lvl| (lvl, m))
.ok_or_else(corrupt_err),
(None, None) => Err(decode_fail()),
@@ -146,12 +145,9 @@ pub(crate) fn read_version_info(matrix: &Matrix) -> Result<u8, crate::error::QrE
let dec2 = bch::decode_version_info(raw2);
match (dec1, dec2) {
(Some(v1), Some(v2)) if v1 == v2 => Ok(v1),
(Some(v1), Some(_v2)) => {
// 两处不一致 — 偏好副本 1
Ok(v1)
}
(Some(v), None) | (None, Some(v)) => Ok(v),
(Some((v1, _)), Some((v2, _))) if v1 == v2 => Ok(v1),
(Some((v1, d1)), Some((v2, d2))) => Ok(if d1 <= d2 { v1 } else { v2 }),
(Some((v, _)), None) | (None, Some((v, _))) => Ok(v),
(None, None) => Err(crate::error::QrError::FormatCorrupted(
"版本信息解码失败:两处副本均无法纠错".into(),
)),
+152 -22
View File
@@ -1,34 +1,164 @@
//! 图像加载与二值化
//!
//! 使用 `image` crate 加载 PNG/JPEG/WebP,转为灰度再二值化为布尔矩阵
//! 提供多遍自适应二值化策略和灰度图加载 API
/// 从图像字节加载并二值化
pub use image::GrayImage;
/// 从图像字节加载为灰度图(公共 API,供调用方自定义预处理)
///
/// 步骤:解码 → 灰度 → 按中位数阈值二值化
pub(crate) fn load_and_binarize(
bytes: &[u8],
) -> Result<Vec<Vec<bool>>, crate::error::QrError> {
let img =
image::load_from_memory(bytes).map_err(|e| crate::error::QrError::DecodeFail(format!("图像解码失败: {e}")))?;
let gray = img.to_luma8();
/// 支持 PNG/JPEG/WebP/BMP 等常见格式。
pub fn load_gray(bytes: &[u8]) -> Result<GrayImage, crate::error::QrError> {
let img = image::load_from_memory(bytes)
.map_err(|e| crate::error::QrError::DecodeFail(format!("图像解码失败: {e}")))?;
Ok(img.to_luma8())
}
/// 多遍二值化:返回按优先级排序的多个二值化结果
///
/// 顺序:中位数(处理干净扫描)→ Otsu(处理双峰直方图)→ Sauvola(处理不均匀光照)
/// 调用方依次尝试解码,首次成功即返回。
pub(crate) fn binarize_multi(gray: &GrayImage) -> Vec<Vec<Vec<bool>>> {
let (w, h) = gray.dimensions();
let width = w as usize;
let height = h as usize;
let pixels: Vec<u8> = gray.iter().copied().collect();
let total = pixels.len();
// 计算中位数阈值
let mut all_pixels: Vec<u8> = gray.iter().copied().collect();
all_pixels.sort_unstable();
let threshold = all_pixels[all_pixels.len() / 2];
let mut results = Vec::with_capacity(3);
// 二值化:像素 < 阈值 → true(暗模块),否则 false(亮模块
let matrix: Vec<Vec<bool>> = (0..height)
// 第 1 遍:中位数阈值(最快
results.push(apply_threshold_from_pixels(&pixels, w, h, median_threshold(&pixels)));
// 第 2 遍:Otsu 大津算法(与中位数不同时才加入)
if let Some(otsu_t) = otsu_threshold(&pixels, total) {
if (otsu_t as i32 - median_threshold(&pixels) as i32).abs() > 10 {
results.push(apply_threshold_from_pixels(&pixels, w, h, otsu_t));
}
}
// 第 3 遍:Sauvola 局部自适应(前两遍均可能失败的兜底)
results.push(sauvola_binarize(gray, w, h));
results
}
/// 中位数阈值
fn median_threshold(pixels: &[u8]) -> u8 {
let mut sorted: Vec<u8> = pixels.to_vec();
sorted.sort_unstable();
sorted[sorted.len() / 2]
}
/// Otsu 大津算法 — 最大化类间方差寻找最优阈值
///
/// 适用于双峰直方图(前景/背景分明)。
/// 对直方图退化为单峰的情况返回 None。
fn otsu_threshold(pixels: &[u8], total: usize) -> Option<u8> {
// 构建 256 级灰度直方图
let mut hist = [0u32; 256];
for &p in pixels {
hist[p as usize] += 1;
}
let mut best_threshold = 128u8;
let mut best_variance = 0.0f64;
let mut sum_b = 0u64;
let mut w_b = 0u64;
let sum_total: u64 = pixels.iter().map(|&p| p as u64).sum();
let mut found = false;
for t in 1..255u8 {
let h = hist[t as usize] as u64;
w_b += h;
if w_b == 0 || w_b == total as u64 {
continue;
}
sum_b += t as u64 * h;
let w_f = total as u64 - w_b;
if w_f == 0 {
break;
}
let mean_b = sum_b as f64 / w_b as f64;
let mean_f = (sum_total - sum_b) as f64 / w_f as f64;
let variance = w_b as f64 * w_f as f64 * (mean_b - mean_f).powi(2);
if variance > best_variance {
best_variance = variance;
best_threshold = t;
found = true;
}
}
if found { Some(best_threshold) } else { None }
}
/// Sauvola 局部自适应阈值
///
/// 对每个像素,在以它为中心的窗口内计算局部均值与标准差,
/// 阈值 = mean * (1 + k * (stddev / R - 1))
/// 标准参数: window=图像短边的 1/8, k=0.2, R=128
fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec<Vec<bool>> {
let window = ((w.min(h) as f64) * 0.08).max(8.0) as u32;
let half = (window / 2) as i32;
let k = 0.2f64;
let r = 128.0f64;
// 预计算积分图像以加速窗口和/平方和
let mut integral = vec![0u64; (w as usize + 1) * (h as usize + 1)];
let mut sq_integral = vec![0u64; (w as usize + 1) * (h as usize + 1)];
for y in 0..h as usize {
let mut row_sum = 0u64;
let mut row_sq = 0u64;
for x in 0..w as usize {
let p = gray.get_pixel(x as u32, y as u32).0[0] as u64;
row_sum += p;
row_sq += p * p;
let idx = (y + 1) * (w as usize + 1) + (x + 1);
let above = (y) * (w as usize + 1) + (x + 1);
let left = (y + 1) * (w as usize + 1) + (x);
let above_left = (y) * (w as usize + 1) + (x);
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;
}
}
let get_window = |x: i32, y: i32| -> (f64, f64) {
let x1 = (x - half).max(0) as usize;
let y1 = (y - half).max(0) as usize;
let x2 = (x + half + 1).min(w 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 count = ((x2 - x1) * (y2 - y1)) as f64;
let sum = (integral[idx(x2, y2)] - integral[idx(x1, y2)] - integral[idx(x2, y1)]
+ integral[idx(x1, y1)]) as f64;
let sq_sum = (sq_integral[idx(x2, y2)] - sq_integral[idx(x1, y2)] - sq_integral[idx(x2, y1)]
+ sq_integral[idx(x1, y1)]) as f64;
let mean = sum / count;
let variance = (sq_sum / count - mean * mean).max(0.0);
(mean, variance.sqrt())
};
(0..h as usize)
.map(|y| {
(0..width)
.map(|x| gray.get_pixel(x as u32, y as u32).0[0] < threshold)
(0..w as usize)
.map(|x| {
let (mean, stddev) = get_window(x as i32, y as i32);
let threshold = mean * (1.0 + k * (stddev / r - 1.0));
gray.get_pixel(x as u32, y as u32).0[0] < threshold as u8
})
.collect()
})
.collect();
Ok(matrix)
.collect()
}
/// 使用给定阈值将像素切片二值化
fn apply_threshold_from_pixels(pixels: &[u8], w: u32, h: u32, threshold: u8) -> Vec<Vec<bool>> {
let width = w as usize;
(0..h as usize)
.map(|y| {
let row_start = y * width;
pixels[row_start..row_start + width]
.iter()
.map(|&p| p < threshold)
.collect()
})
.collect()
}
+211 -32
View File
@@ -1,6 +1,6 @@
//! QR 码解码器
//!
//! 完整流水线:图像 → 二值化 → 定位检测 → 格式/版本信息 → 解掩码 →
//! 完整流水线:图像 → 二值化(多遍) → 定位检测 → 格式/版本信息 → 解掩码 →
//! 蛇形提取 → 去交错 → RS 纠错 → 模式解码 → 文本
//!
//! ```rust
@@ -17,7 +17,7 @@ mod deinterleave;
mod detect;
mod extract;
mod format;
mod image;
pub mod image;
mod mode_decode;
mod perspective;
mod rs_decode;
@@ -26,6 +26,23 @@ use crate::error::QrError;
use crate::matrix::mask::apply_mask;
use crate::version::{EcLevel, Version};
/// 包围盒(像素坐标)
#[derive(Debug, Clone, Copy, Default)]
pub struct BoundingBox {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
/// 结构化追加信息
#[derive(Debug, Clone, Default)]
pub struct StructuredAppend {
pub position: u8,
pub total: u8,
pub parity: u8,
}
/// 解码结果
#[derive(Debug, Clone)]
pub struct DecodeResult {
@@ -39,29 +56,200 @@ pub struct DecodeResult {
pub mask: u8,
/// 纠错的码字数量
pub errors_corrected: usize,
/// QR 码在图像中的像素包围盒
pub bounds: BoundingBox,
/// 置信度 0.0~1.0
pub confidence: f64,
/// 是否为 GS1/FNC1 条码
pub is_gs1: bool,
/// ECI 字符集编号(None = ISO-8859-1
pub eci_designator: Option<u32>,
/// 结构化追加信息
pub structured_append: Option<StructuredAppend>,
}
/// 从图像字节数据解码 QR 码(PNG/JPEG/WebP 等)
///
/// # 参数
/// - `bytes`: 图像文件字节(PNG/JPEG/WebP
///
/// # 返回
/// `DecodeResult` 包含解码文本和元信息
/// 使用多遍二值化 + 4 点透视矫正 + 反色检测。
pub fn decode_image(bytes: &[u8]) -> Result<DecodeResult, QrError> {
let gray = image::load_and_binarize(bytes)?;
decode_all(bytes).and_then(|mut results| {
results.pop().ok_or(QrError::DecodeFail("未找到 QR 码".into()))
})
}
// 第一遍:直接检测
if let Ok(detect_result) = detect::detect_and_extract(&gray) {
if let Ok(result) = decode_matrix(&detect_result.modules) {
return Ok(result);
/// 解码图片中所有 QR 码,返回按置信度降序排列的结果
///
/// 对每遍二值化结果尝试检测+解码,支持反色图像。
pub fn decode_all(bytes: &[u8]) -> Result<Vec<DecodeResult>, QrError> {
let gray = image::load_gray(bytes)?;
let binarized_list = image::binarize_multi(&gray);
let should_try_inverted = is_likely_inverted(&gray);
let mut results: Vec<DecodeResult> = Vec::new();
for (pass_idx, bin) in binarized_list.iter().enumerate() {
let normal = (false, bin.clone());
let inverted = (pass_idx == 0 && should_try_inverted)
.then(|| (true, invert_matrix(bin)));
for (is_inverted, matrix) in std::iter::once(normal).chain(inverted) {
// 尝试直接检测 → 透视矫正重试
if let Ok(mut r) = try_decode_with_perspective(&matrix, &gray, pass_idx, is_inverted) {
r.confidence = r.confidence.clamp(0.0, 1.0);
if r.confidence > 0.3 {
results.push(r);
}
}
}
// 反色也失败时,尝试纯旋转回退
if results.is_empty() && pass_idx == 0 {
let fallback = perspective::auto_correct_fallback(bin);
if let Ok(result) = try_decode_matrix(&fallback, None) {
let mut r = with_confidence(result, pass_idx);
r.confidence *= 0.75; // 回退惩罚
results.push(r);
}
}
if !results.is_empty() {
break;
}
}
// 第二遍:尝试旋转矫正
let corrected = perspective::auto_correct(&gray);
let detect_result = detect::detect_and_extract(&corrected)?;
decode_matrix(&detect_result.modules)
if results.is_empty() {
Err(QrError::DecodeFail("所有方法均未能解码 QR 码".into()))
} else {
results.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
Ok(results)
}
}
/// 尝试从二值化矩阵解码(含透视矫正重试)
fn try_decode_with_perspective(
bin: &[Vec<bool>],
_gray: &crate::decoder::image::GrayImage,
pass_idx: usize,
is_inverted: bool,
) -> Result<DecodeResult, QrError> {
// 检测定位图案
let finders = detect::find_finders_for_perspective(bin)?;
let tl = (finders[0].0 as f64, finders[0].1 as f64);
let tr = (finders[1].0 as f64, finders[1].1 as f64);
let bl = (finders[2].0 as f64, finders[2].1 as f64);
// 估算版本
let module_size = detect::estimate_module_size_from_finders(bin, &finders);
let version = detect::estimate_version_from_tl_tr(tl, tr, module_size);
// 直接用二值化矩阵尝试
if let Ok(result) = try_decode_matrix(bin, None) {
return Ok(apply_perspective_penalty(result, pass_idx, is_inverted, false));
}
// 透视矫正重试
let corrected = perspective::auto_correct(bin, tl, tr, bl, version);
if !corrected.is_empty() && corrected != *bin {
if let Ok(result) = try_decode_matrix(&corrected, None) {
return Ok(apply_perspective_penalty(result, pass_idx, is_inverted, true));
}
}
// 旋转回退(在多个预设角度尝试)
let fallback = perspective::auto_correct_fallback(bin);
if fallback != *bin {
if let Ok(result) = try_decode_matrix(&fallback, None) {
return Ok(apply_perspective_penalty(result, pass_idx, is_inverted, true));
}
}
Err(QrError::DecodeFail("检测到定位图案但解码失败".into()))
}
fn apply_perspective_penalty(
mut r: DecodeResult,
pass_idx: usize,
is_inverted: bool,
had_correction: bool,
) -> DecodeResult {
if pass_idx > 0 { r.confidence -= 0.05 * pass_idx as f64; }
if is_inverted { r.confidence *= 0.85; }
if had_correction { r.confidence *= 0.9; }
r.confidence = r.confidence.max(0.0);
r
}
fn invert_matrix(matrix: &[Vec<bool>]) -> Vec<Vec<bool>> {
matrix.iter().map(|row| row.iter().map(|&p| !p).collect()).collect()
}
/// 尝试从二值化矩阵解码,成功返回带包围盒的结果
fn try_decode_matrix(
matrix: &[Vec<bool>],
bounds_override: Option<BoundingBox>,
) -> Result<DecodeResult, QrError> {
let detect_result = detect::detect_and_extract(matrix)?;
let mut result = decode_matrix(&detect_result.modules)?;
result.bounds = bounds_override.unwrap_or(detect_result.bounds);
result.confidence = compute_confidence(
&result,
detect_result.quiet_zone_ok,
detect_result.finder_ratio_deviation,
);
Ok(result)
}
/// 根据解码质量指标计算置信度 0.0~1.0
fn compute_confidence(
result: &DecodeResult,
quiet_zone_ok: bool,
finder_ratio_deviation: f64,
) -> f64 {
let mut conf = 1.0;
// 纠错比例惩罚
let max_ec = Version::new(result.version)
.map(|v| v.ec_info(result.level).ec_per_block as usize)
.unwrap_or(0);
if max_ec > 0 {
let err_ratio = result.errors_corrected as f64 / max_ec as f64;
conf -= err_ratio * 0.4;
}
// 静区不完整惩罚
if !quiet_zone_ok {
conf *= 0.7;
}
// 定位图案比例偏差惩罚
if finder_ratio_deviation > 0.25 {
conf -= 0.2;
}
conf.clamp(0.0, 1.0)
}
/// 带置信度封装结果
fn with_confidence(mut result: DecodeResult, pass_idx: usize) -> DecodeResult {
// 多遍二值化惩罚:第 2 遍 (Otsu) -0.05, 第 3 遍 (Sauvola) -0.1
if pass_idx > 0 {
result.confidence -= 0.05 * pass_idx as f64;
result.confidence = result.confidence.max(0.0);
}
result
}
/// 检测图像是否大概率是反色 QR 码(白底黑码)
fn is_likely_inverted(gray: &crate::decoder::image::GrayImage) -> bool {
let (w, h) = gray.dimensions();
// 采样四角和中心 5 个点
let samples = [
gray.get_pixel(0, 0).0[0],
gray.get_pixel(w - 1, 0).0[0],
gray.get_pixel(0, h - 1).0[0],
gray.get_pixel(w - 1, h - 1).0[0],
gray.get_pixel(w / 2, h / 2).0[0],
];
// ≥4 个采样点为暗色 (>128) → 反色
samples.iter().filter(|&&p| p > 128).count() >= 4
}
/// 从布尔矩阵解码 QR 码
@@ -72,18 +260,15 @@ pub fn decode_image(bytes: &[u8]) -> Result<DecodeResult, QrError> {
/// # 返回
/// `DecodeResult` 包含解码文本和元信息
pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, QrError> {
// 1. 构建 Matrix 对象
let size = matrix.len() as u8;
if matrix.is_empty() || matrix[0].is_empty() {
return Err(QrError::DecodeFail("空矩阵".into()));
}
// 验证方形
if matrix.iter().any(|r| r.len() != size as usize) {
return Err(QrError::DecodeFail("矩阵不是方形".into()));
}
// 从尺寸推算版本
let version = ((size as i32 - 17) / 4) as u8;
if !(1..=40).contains(&version) || (17 + version as i32 * 4) != size as i32 {
return Err(QrError::DecodeFail(format!(
@@ -92,7 +277,6 @@ pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, QrError> {
)));
}
// 构建 Matrix 对象(简化:不预标注保留区域,BCH 读取函数直接访问坐标)
let mut m = crate::matrix::grid::Matrix::new(size);
for (y, row) in matrix.iter().enumerate() {
for (x, &dark) in row.iter().enumerate() {
@@ -102,14 +286,12 @@ pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, QrError> {
}
}
// 标记功能图案区域(使数据提取能跳过)
use crate::matrix::patterns::{
place_alignment_patterns, place_finder_patterns, place_timing_patterns,
reserve_format_areas, reserve_version_areas,
};
place_finder_patterns(&mut m);
place_timing_patterns(&mut m);
// 对齐图案位置依赖于版本,需要从版本查询
let ver = Version::new(version).ok_or(QrError::InvalidVersion(version))?;
place_alignment_patterns(&mut m, ver.alignment_positions());
reserve_format_areas(&mut m);
@@ -117,10 +299,8 @@ pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, QrError> {
reserve_version_areas(&mut m);
}
// 2. 读取格式信息 → EC 级别 + 掩码
let (level, mask) = format::read_format_info(&m)?;
// 3. 读取版本信息(版本≥7时验证)
if version >= 7 {
let ver_info = format::read_version_info(&m)?;
if ver_info != version {
@@ -130,18 +310,14 @@ pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, QrError> {
}
}
// 4. 解掩码
let unmasked = apply_mask(&m, mask);
// 5. 蛇形提取码字
let ec_info = ver.ec_info(level);
let total_codewords = ec_info.total_codewords as usize;
let codewords = extract::extract_codewords(&unmasked, total_codewords);
// 6. 去交错
let (data_blocks, ec_blocks) = deinterleave::deinterleave(&codewords, &ec_info);
// 7. RS 纠错
let mut corrected_data = Vec::new();
let mut total_errors = 0usize;
@@ -151,20 +327,23 @@ pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, QrError> {
total_errors += errors;
}
// 8. 转为比特流
let bits: Vec<bool> = corrected_data
.iter()
.flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1 == 1))
.collect();
// 9. 模式解码
let text = mode_decode::decode_bitstream(&bits, version)?;
let decode_output = mode_decode::decode_bitstream(&bits, version)?;
Ok(DecodeResult {
text,
text: decode_output.text,
version,
level,
mask,
errors_corrected: total_errors,
bounds: BoundingBox::default(),
confidence: 1.0,
is_gs1: decode_output.is_gs1,
eci_designator: decode_output.eci_designator,
structured_append: decode_output.structured_append,
})
}
+93 -27
View File
@@ -130,53 +130,119 @@ fn shift_jis_value_to_char(val: u16) -> Option<char> {
output.chars().next()
}
/// 解码主函数:比特流 → 文本
/// 解码输出
pub(crate) struct DecodeOutput {
pub text: String,
pub is_gs1: bool,
pub eci_designator: Option<u32>,
pub structured_append: Option<crate::decoder::StructuredAppend>,
}
/// 解码主函数:比特流 → 解码输出
pub(crate) fn decode_bitstream(
bits: &[bool],
version: u8,
) -> Result<String, crate::error::QrError> {
) -> Result<DecodeOutput, crate::error::QrError> {
let mut pos = 0;
let mut text = String::new();
let mut is_gs1 = false;
let mut eci_designator: Option<u32> = None;
let mut structured_append: Option<crate::decoder::StructuredAppend> = None;
loop {
if pos + 4 > bits.len() {
break;
}
let mode_indicator = read_bits(bits, &mut pos, 4) as u8;
if mode_indicator == 0b0000 {
break; // 终止符
}
let count_bits =
char_count_bits(mode_indicator, version).ok_or_else(|| {
crate::error::QrError::DecodeFail(format!(
"未知模式指示符: {:04b}",
mode_indicator
))
})? as usize;
if pos + count_bits > bits.len() {
break;
}
let count = read_bits(bits, &mut pos, count_bits);
match mode_indicator {
0b0001 => text.push_str(&decode_numeric(bits, &mut pos, count)),
0b0010 => text.push_str(&decode_alphanumeric(bits, &mut pos, count)),
0b0100 => text.push_str(&decode_byte(bits, &mut pos, count)),
0b1000 => text.push_str(&decode_kanji(bits, &mut pos, count)),
0b0000 => break, // 终止符
0b0001 | 0b0010 | 0b0100 | 0b1000 => {
// 标准数据模式
let count_bits =
char_count_bits(mode_indicator, version).ok_or_else(|| {
crate::error::QrError::DecodeFail(format!(
"未知模式指示符: {:04b}",
mode_indicator
))
})? as usize;
if pos + count_bits > bits.len() { break; }
let count = read_bits(bits, &mut pos, count_bits);
match mode_indicator {
0b0001 => text.push_str(&decode_numeric(bits, &mut pos, count)),
0b0010 => text.push_str(&decode_alphanumeric(bits, &mut pos, count)),
0b0100 => {
let bytes_str = decode_byte(bits, &mut pos, count);
// 根据 ECI 处理字符集
if eci_designator == Some(26) {
// UTF-8: 原样保留字节序列(已在 decode_byte 中按 Latin-1 解码,
// 但 UTF-8 字节在 Latin-1 范围 0x00-0xFF 内被保留为单字节字符)
// 这里简单处理:如果是 UTF-8 ECI,不做二次转换
text.push_str(&bytes_str);
} else {
text.push_str(&bytes_str);
}
}
0b1000 => text.push_str(&decode_kanji(bits, &mut pos, count)),
_ => unreachable!(),
}
}
0b0011 => {
// 结构化追加
if pos + 16 > bits.len() { break; }
let sym_pos = read_bits(bits, &mut pos, 4) as u8;
let sym_total = read_bits(bits, &mut pos, 4) as u8;
let parity = read_bits(bits, &mut pos, 8) as u8;
structured_append = Some(crate::decoder::StructuredAppend {
position: sym_pos,
total: sym_total,
parity,
});
// 结构化追加头部后紧跟数据段,继续循环
continue;
}
0b0101 => {
// FNC1 First Position (GS1)
is_gs1 = true;
// 字母数字模式的第一位 '%' 被替换为 FNC1,需要在解码时处理
// 简化:标记 is_gs1,由调用方按 GS1 标准解析
continue;
}
0b1001 => {
// FNC1 Second Position
is_gs1 = true;
continue;
}
0b0111 => {
// ECI 指派号
if pos + 8 > bits.len() { break; }
let eci_byte1 = read_bits(bits, &mut pos, 8) as u32;
eci_designator = if eci_byte1 >= 128 {
// 2 字节 ECI
if pos + 8 > bits.len() { break; }
let eci_byte2 = read_bits(bits, &mut pos, 8) as u32;
Some((eci_byte1 - 128) * 128 + eci_byte2)
} else {
Some(eci_byte1)
};
continue;
}
_ => {
return Err(crate::error::QrError::DecodeFail(format!(
"未知模式指示符: {:04b}",
mode_indicator
)))
// 未知模式指示符 → 可能是填充,跳过
break;
}
}
}
if text.is_empty() {
if text.is_empty() && structured_append.is_none() {
Err(crate::error::QrError::DecodeFail("未解码到任何文本".into()))
} else {
Ok(text)
Ok(DecodeOutput {
text,
is_gs1,
eci_designator,
structured_append,
})
}
}
+150 -146
View File
@@ -1,160 +1,164 @@
//! QR 码图像透视矫正
//! 透视矫正 — DLT 4 点单应变换 + 多角度旋转回退
//!
//! 检测定位图案后,计算旋转角并矫正图像。
//! MVP 版本:仅做旋转矫正(仿射变换),不做完整单应变换
//! 用 3 个定位图案中心 + 推算第 4 点构建 4 点对应,
//! 通过 DLT 求解 3×3 单应矩阵,双线性插值反向映射生成矫正图像
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();
};
use nalgebra::DMatrix;
// 尝试找到至少 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(
/// 用 3 个定位图案中心计算单应变换,返回矫正后的二值图像
pub(crate) fn auto_correct(
gray: &[Vec<bool>],
tl: (usize, usize),
tr: (usize, usize),
tl: (f64, f64),
tr: (f64, f64),
bl: (f64, f64),
version: u8,
) -> Vec<Vec<bool>> {
let h = gray.len();
let w = if h > 0 {
gray[0].len()
} else {
return gray.to_vec();
let size = (17 + version as usize * 4) as f64;
// 推算右下角:平行四边形法则
let br = (tr.0 + bl.0 - tl.0, tr.1 + bl.1 - tl.1);
let src = [tl, tr, br, bl];
let dst = [
(0.0, 0.0),
(size - 1.0, 0.0),
(size - 1.0, size - 1.0),
(0.0, size - 1.0),
];
let h = match compute_homography(&src, &dst) {
Some(h) => h,
None => 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); // 正值 = 顺时针偏离水平
let h_inv = match invert_homography(&h) {
Some(inv) => inv,
None => return gray.to_vec(),
};
if angle.abs() < 0.01 {
// 已基本水平,不处理
return gray.to_vec();
}
let out_size = size as u32;
(0..out_size as usize)
.map(|y| {
(0..out_size as usize)
.map(|x| {
let (sx, sy) = apply_point(&h_inv, x as f64, y as f64);
bilinear_sample(gray, sx, sy)
})
.collect()
})
.collect()
}
// 旋转中心 = 图像中心
let cx = w as f64 / 2.0;
let cy = h as f64 / 2.0;
let cos_a = angle.cos();
let sin_a = angle.sin();
/// 多角度旋转回退(兼容旧 API,无定位图案信息时使用)
pub(crate) fn auto_correct_fallback(gray: &[Vec<bool>]) -> Vec<Vec<bool>> {
let angles = [-45.0f64, -30.0, -15.0, 0.0, 15.0, 30.0, 45.0];
let mut best_count = 0usize;
let mut best_angle = 0.0f64;
// 计算旋转后尺寸
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];
}
for &angle in &angles {
let rotated = rotate_image(gray, angle.to_radians());
let count = count_finder_hits(&rotated);
if count > best_count {
best_count = count;
best_angle = angle.to_radians();
}
}
result
if best_count > 0 && best_angle.abs() > 0.01 {
rotate_image(gray, best_angle)
} else {
gray.to_vec()
}
}
/// DLT 求解 3×3 单应矩阵(4 点对应 → 最小特征向量)
fn compute_homography(src: &[(f64, f64); 4], dst: &[(f64, f64); 4]) -> Option<[[f64; 3]; 3]> {
let mut a = DMatrix::<f64>::zeros(8, 9);
for i in 0..4 {
let (x, y) = src[i];
let (u, v) = dst[i];
let r = 2 * i;
a[(r, 0)] = x; a[(r, 1)] = y; a[(r, 2)] = 1.0;
a[(r, 6)] = -x * u; a[(r, 7)] = -y * u; a[(r, 8)] = -u;
a[(r + 1, 3)] = x; a[(r + 1, 4)] = y; a[(r + 1, 5)] = 1.0;
a[(r + 1, 6)] = -x * v; a[(r + 1, 7)] = -y * v; a[(r + 1, 8)] = -v;
}
let ata = a.transpose() * &a;
let eigen = ata.symmetric_eigen();
let min_idx = eigen.eigenvalues.iter().enumerate()
.min_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)?;
let hv = eigen.eigenvectors.column(min_idx);
let s = if hv[8].abs() > 1e-8 { 1.0 / hv[8] } else { 1.0 };
Some([
[hv[0] * s, hv[1] * s, hv[2] * s],
[hv[3] * s, hv[4] * s, hv[5] * s],
[hv[6] * s, hv[7] * s, 1.0],
])
}
/// 3×3 矩阵求逆
fn invert_homography(h: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
let (a,b,c) = (h[0][0],h[0][1],h[0][2]);
let (d,e,f) = (h[1][0],h[1][1],h[1][2]);
let (g,hi,i) = (h[2][0],h[2][1],h[2][2]);
let det = a*(e*i-f*hi) - b*(d*i-f*g) + c*(d*hi-e*g);
if det.abs() < 1e-10 { return None; }
let inv = 1.0 / det;
Some([
[(e*i-f*hi)*inv, (c*hi-b*i)*inv, (b*f-c*e)*inv],
[(f*g-d*i)*inv, (a*i-c*g)*inv, (c*d-a*f)*inv],
[(d*hi-e*g)*inv, (b*g-a*hi)*inv, (a*e-b*d)*inv],
])
}
fn apply_point(h: &[[f64; 3]; 3], x: f64, y: f64) -> (f64, f64) {
let w = h[2][0]*x + h[2][1]*y + h[2][2];
if w.abs() < 1e-8 { return (x, y); }
((h[0][0]*x + h[0][1]*y + h[0][2])/w, (h[1][0]*x + h[1][1]*y + h[1][2])/w)
}
fn rotate_image(gray: &[Vec<bool>], angle: f64) -> Vec<Vec<bool>> {
let h = gray.len(); let w = if h>0 { gray[0].len() } else { return gray.to_vec(); };
let (cos, sin) = (angle.cos(), angle.sin());
let (cx, cy) = (w as f64/2.0, h as f64/2.0);
let corners = [(0.0,0.0),(w as f64,0.0),(w as f64,h as f64),(0.0,h as f64)];
let mut xs=vec![]; let mut ys=vec![];
for (px,py) in corners { let rx=cos*(px-cx)-sin*(py-cy)+cx; let ry=sin*(px-cx)+cos*(py-cy)+cy; xs.push(rx as i32); ys.push(ry as i32); }
let (min_x,max_x)=(*xs.iter().min().unwrap_or(&0),*xs.iter().max().unwrap_or(&0));
let (min_y,max_y)=(*ys.iter().min().unwrap_or(&0),*ys.iter().max().unwrap_or(&0));
let (ow,oh)=((max_x-min_x+1).max(1) as usize,(max_y-min_y+1).max(1) as usize);
(0..oh).map(|y|(0..ow).map(|x|{
let sx=cos*((x as i32+min_x) as f64-cx)+sin*((y as i32+min_y) as f64-cy)+cx;
let sy=-sin*((x as i32+min_x) as f64-cx)+cos*((y as i32+min_y) as f64-cy)+cy;
bilinear_sample(gray,sx,sy)
}).collect()).collect()
}
fn count_finder_hits(gray: &[Vec<bool>]) -> usize {
let mut hits=0;
for row in (0..gray.len()).step_by(4) {
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); }
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;
if avg<2.0{continue;}
let ck=|v:f64,e:f64|(v-e*avg).abs()<avg*0.4;
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;}
}
}
hits
}
fn bilinear_sample(gray: &[Vec<bool>], x: f64, y: f64) -> bool {
let (h,w)=(gray.len() as i32,if gray.is_empty(){0}else{gray[0].len() as i32});
let (fx,fy)=(x.floor(),y.floor());
let (x0,y0)=(fx as i32,fy as i32);
let (x1,y1)=(x0+1,y0+1);
let (dx,dy)=(x-fx,y-fy);
let s=|px:i32,py:i32|->f64{if px>=0&&py>=0&&px<w&&py<h{gray[py as usize][px as usize]as u8 as f64}else{0.0}};
let v00=s(x0,y0);let v10=s(x1,y0);let v01=s(x0,y1);let v11=s(x1,y1);
(v00*(1.0-dx)+v10*dx)*(1.0-dy)+(v01*(1.0-dx)+v11*dx)*dy > 0.5
}