diff --git a/ddddocr-core/src/error.rs b/ddddocr-core/src/error.rs index 529ad7f..c74aaf5 100644 --- a/ddddocr-core/src/error.rs +++ b/ddddocr-core/src/error.rs @@ -21,8 +21,12 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum DdddError { + // 【新增】专门处理文件读取、路径不存在等原生 I/O 错误 + #[error("系统网络或文件 I/O 异常: {0}")] + Io(#[from] std::io::Error), + #[error("图像预处理失败: {0}")] - PreprocessError(String), + PreprocessError(#[from] ImagePreprocessReason), #[error("模型推理引擎内部发生异常: {0}")] EngineError(#[from] anyhow::Error), @@ -46,5 +50,27 @@ pub enum DdddError { InternalError(String), } +/// 专门服务于预处理的子错误枚举,保留全部底层上下文 +#[derive(Error, Debug)] +pub enum ImagePreprocessReason { + #[error("图片加载或文件 I/O 失败: {0}")] + ImageIo(#[from] image::ImageError), + + #[error("图片转矩阵矩阵(ndarray)失败: {0}")] + NdarrayError(#[from] ndarray::ShapeError), + + #[error("Base64 解码失败: {0}")] + Base64(#[from] base64::DecodeError), + + #[error("Base64 头部格式不正确,缺少 ';base64,' 分隔符")] + InvalidBase64Header, + + #[error("不支持的通道数: {0}")] + UnsupportedChannels(usize), + + #[error("其他预处理错误: {0}")] + Custom(String), +} + /// 统一用我们自己的 DdddError 包装 Result pub type Result = std::result::Result; diff --git a/ddddocr-core/src/slide.rs b/ddddocr-core/src/slide.rs index e9f7ad5..87beada 100644 --- a/ddddocr-core/src/slide.rs +++ b/ddddocr-core/src/slide.rs @@ -1,4 +1,4 @@ -use crate::utils::image_io::image_to_ndarray; +use crate::utils::image_io::{image_to_ndarray,ColorMode}; use crate::utils::image_processor; use crate::utils::image_processor::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray}; use anyhow::{Result, anyhow}; @@ -42,8 +42,8 @@ impl Slider { background_image: &DynamicImage, simple_target: bool, ) -> Result { - let target_array = image_to_ndarray(target_image); - let background_array = image_to_ndarray(background_image); + let target_array = image_to_ndarray(target_image,ColorMode::RGB)?; + let background_array = image_to_ndarray(background_image,ColorMode::RGB)?; self.perform_slide_match(target_array.view(), background_array.view(), simple_target) } @@ -55,8 +55,8 @@ impl Slider { background_image: &DynamicImage, ) -> Result { // 1. 转换为 ndarray (HWC RGB) - let target_array = image_to_ndarray(target_image); - let background_array = image_to_ndarray(background_image); + let target_array = image_to_ndarray(target_image,ColorMode::RGB)?; + let background_array = image_to_ndarray(background_image,ColorMode::RGB)?; // 2. 执行比较逻辑 (对应 _perform_slide_comparison) self.perform_slide_comparison(target_array.view(), background_array.view()) diff --git a/ddddocr-core/src/utils.rs b/ddddocr-core/src/utils.rs index f8020b5..0163135 100644 --- a/ddddocr-core/src/utils.rs +++ b/ddddocr-core/src/utils.rs @@ -1,5 +1,8 @@ pub mod image_io; pub mod image_processor; mod tensor_transform; +mod image_helper; + // 对外统一暴露干净的 API 语义层 pub use tensor_transform::normalize_ocr_logits; +pub use image_io::{ColorMode}; diff --git a/ddddocr-core/src/utils/image_helper.rs b/ddddocr-core/src/utils/image_helper.rs new file mode 100644 index 0000000..2863472 --- /dev/null +++ b/ddddocr-core/src/utils/image_helper.rs @@ -0,0 +1,102 @@ +use crate::error::{DdddError, ImagePreprocessReason, Result}; +use crate::utils::image_io::{base64_to_image, ndarray_to_pil_image}; +use image::DynamicImage; +use ndarray::ArrayViewD; +use std::path::Path; +use std::path::PathBuf; + +pub struct Base64<'a>(pub &'a str); +/// 专属图像输入源转换器 +pub struct ImageSource { + inner: DynamicImage, +} +// 1. 将 into_inner 优化为 into_image,符合 Rust 官方命名规范 +impl ImageSource { + /// 消耗当前包装器,获取最终的 DynamicImage + pub fn into_image(self) -> DynamicImage { + self.inner + } +} + +pub trait TryFromImage: Sized { + // 唯一的转换入口,通过目标类型来调用 + fn try_from_image(value: T) -> Result; +} + +// 1. 本身是 DynamicImage +impl TryFromImage for ImageSource { + fn try_from_image(img: DynamicImage) -> Result { + Ok(Self { inner: img }) + } +} +// 2.2 路径类型 A: &str (最常用) +impl<'a> TryFromImage<&'a str> for ImageSource { + fn try_from_image(path_or_b64: &'a str) -> Result { + // 1. 嗅探:如果包含 Base64 特征 + if path_or_b64.starts_with("data:image/") && path_or_b64.contains(";base64,") { + // 提取出真正的 base64 数据部分 + let (_, clean_b64) = path_or_b64.split_once(";base64,").ok_or_else(|| { + // 返回一个明确的、可读性极佳的格式错误 + DdddError::PreprocessError(ImagePreprocessReason::InvalidBase64Header) + })?; + // 转换为 Base64 包装器,并复用其 TryFromImage 实现 + Self::try_from_image(Base64(clean_b64)) + } else { + // 2. 否则,老老实实当作本地路径打开 + let img = image::open(path_or_b64).map_err(ImagePreprocessReason::from)?; + Ok(Self { inner: img }) + } + } +} + +// 2.3 路径类型 B: &Path (标准借用) +impl<'a> TryFromImage<&'a Path> for ImageSource { + fn try_from_image(path: &'a Path) -> Result { + let img = image::open(path).map_err(ImagePreprocessReason::from)?; + Ok(Self { inner: img }) + } +} +// 2.4 路径类型 C: PathBuf / String (拥有所有权,透传给借用) +impl TryFromImage for ImageSource { + fn try_from_image(path: PathBuf) -> Result { + Self::try_from_image(path.as_path()) + } +} + +impl TryFromImage for ImageSource { + fn try_from_image(path_or_b64: String) -> Result { + Self::try_from_image(path_or_b64.as_str()) + } +} +// 2. 支持带有生命周期的借用:直接支持 &[u8](不强绑生命周期到 ImageSource 结构体上!) +impl<'a> TryFromImage<&'a [u8]> for ImageSource { + fn try_from_image(bytes: &'a [u8]) -> Result { + let img = image::load_from_memory(bytes).map_err(ImagePreprocessReason::from)?; + Ok(Self { inner: img }) + } +} + +// 4. 完美支持 ndarray 的借用 ArrayViewD +impl<'a> TryFromImage> for ImageSource { + fn try_from_image(array: ArrayViewD<'a, u8>) -> Result { + let img = ndarray_to_pil_image(array)?; + Ok(Self { inner: img }) + } +} + +impl<'a> TryFromImage> for ImageSource { + fn try_from_image(b64_str: Base64<'a>) -> Result { + let img = base64_to_image(b64_str.0)?; + Ok(Self { inner: img }) + } +} + +/// 模拟 Python 的 load_image_from_input +#[allow(dead_code)] +pub fn load_image_from_input(input: I) -> Result +where + ImageSource: TryFromImage, +{ + let img = ImageSource::try_from_image(input)?.into_image(); + Ok(img) +} diff --git a/ddddocr-core/src/utils/image_io.rs b/ddddocr-core/src/utils/image_io.rs index 4f16cd8..57a13cc 100644 --- a/ddddocr-core/src/utils/image_io.rs +++ b/ddddocr-core/src/utils/image_io.rs @@ -1,43 +1,18 @@ -use anyhow::{Context, Result, anyhow, bail}; +use crate::error::{DdddError, ImagePreprocessReason, Result}; use base64::{Engine as _, engine::general_purpose}; -use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, Luma, Rgb, RgbImage, Rgba}; +use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba}; +use ndarray::{Array3, ArrayD, ArrayViewD}; use std::fs; use std::path::{Path, PathBuf}; -use ndarray::{Array3, ArrayD, ArrayViewD}; + #[derive(Debug)] pub enum ColorMode { RGB, RGBA, L, } -/// 定义支持的输入类型枚举 -pub enum ImageInput { - Bytes(Vec), - Array(ArrayD), // 对应 numpy 数组 - Path(PathBuf), - Base64(String), - DynamicImage(DynamicImage), -} -/// 模拟 Python 的 load_image_from_input -#[allow(dead_code)] -pub fn load_image_from_input(img_input: ImageInput) -> Result { - match img_input { - // 2. 处理字节流 (Bytes) - ImageInput::Bytes(bytes) => { - image::load_from_memory(&bytes).context("Failed to load utils from bytes") - } - // 1. 已经是 DynamicImage - ImageInput::DynamicImage(i) => Ok(i), - // 5. 处理 ndarray (Numpy-like) - // 假设输入是 HWC 格式的 Array3 - ImageInput::Array(a) => numpy_to_pil_image(a.view()), - // 4. 处理 Base64 字符串 - ImageInput::Base64(b) => base64_to_image(&b), - // 3. 处理文件路径 (Path) - ImageInput::Path(p) => image::open(p).context("Failed to open utils from path"), - } -} -fn base64_to_image(b64_str: &str) -> Result { +/// 将base64编码的图片转换为 DynamicImage +pub fn base64_to_image(b64_str: &str) -> Result { // 过滤掉可能存在的 base64 前缀,例如 "data:utils/png;base64," let clean_b64 = if let Some(pos) = b64_str.find(",") { &b64_str[pos + 1..] @@ -47,18 +22,18 @@ fn base64_to_image(b64_str: &str) -> Result { let bytes = general_purpose::STANDARD .decode(clean_b64.trim()) - .map_err(|e| anyhow!("Base64 decode error: {}", e))?; + .map_err(ImagePreprocessReason::from)?; - image::load_from_memory(&bytes).context("Failed to load utils from decoded base64") + let img = image::load_from_memory(&bytes).map_err(ImagePreprocessReason::from)?; + Ok(img) } /// 读取图片文件并转换为 base64 编码字符串 -/// 对应 Python 版 get_img_base64 -pub fn get_img_base64>(image_path: P) -> Result { +// 对应 Python 版 get_img_base64 +pub fn img_base64>(image_path: P) -> Result { // 1. 读取文件原始字节流 // 使用 AsRef 泛型可以让函数同时支持 String, &str, PathBuf 等类型 - let image_data = fs::read(&image_path) - .with_context(|| format!("Failed to read utils file: {:?}", image_path.as_ref()))?; + let image_data = fs::read(&image_path)?; // 2. 进行 Base64 编码 // 使用 STANDARD 引擎对齐 Python 的 base64.b64encode @@ -67,55 +42,50 @@ pub fn get_img_base64>(image_path: P) -> Result { Ok(b64_string) } -/// 封装数组转图像的逻辑,对齐 Python 版 _numpy_to_pil_image -fn numpy_to_pil_image(array: ArrayViewD) -> Result { +/// 封装数组转图像的逻辑, +// 对应 Python 版 _numpy_to_pil_image +pub(crate) fn ndarray_to_pil_image(array: ArrayViewD) -> Result { let shape = array.shape(); let dim = shape.len(); // 1. 确保数据在内存中是连续的 (C order / Standard Layout) // 如果 arr 是经过切片或转置的,这一步会进行必要的内存拷贝 - let standard = array.as_standard_layout(); - let (raw_data, _offset) = standard.to_owned().into_raw_vec_and_offset(); + // let standard = array.as_standard_layout(); + // let (raw_data, _offset) = standard.to_owned().into_raw_vec_and_offset(); - match dim { + let color_mode = match dim { // 对应 Python: len(array.shape) == 2 (灰度图 H, W) - 2 => { - let (h, w) = (shape[0], shape[1]); - ImageBuffer::, _>::from_raw(w as u32, h as u32, raw_data) - .map(DynamicImage::ImageLuma8) - .ok_or_else(|| anyhow!("Failed to create Luma utils from 2D array")) - } + 2 => ColorMode::L, // 对应 Python: len(array.shape) == 3 (H, W, C) 3 => { - let (h, w, c) = (shape[0], shape[1], shape[2]); + let (_h, _w, c) = (shape[0], shape[1], shape[2]); match c { // 对应 Python: array.shape[2] == 1 (单通道 H, W, 1) - 1 => ImageBuffer::, _>::from_raw(w as u32, h as u32, raw_data) - .map(DynamicImage::ImageLuma8), - + 1 => ColorMode::L, // 对应 Python: array.shape[2] == 3 (RGB H, W, 3) - 3 => ImageBuffer::, _>::from_raw(w as u32, h as u32, raw_data) - .map(DynamicImage::ImageRgb8), - + 3 => ColorMode::RGB, // 对应 Python: array.shape[2] == 4 (RGBA H, W, 4) - 4 => ImageBuffer::, _>::from_raw(w as u32, h as u32, raw_data) - .map(DynamicImage::ImageRgba8), - + 4 => ColorMode::RGBA, _ => { - return Err(anyhow!("不支持的通道数: {}", c)); + return Err(DdddError::PreprocessError( + ImagePreprocessReason::UnsupportedChannels(c), + )); } } - .ok_or_else(|| anyhow!("转换彩色图失败")) } - - _ => Err(anyhow!("不支持的数组维度: {},仅支持 2D 或 3D", dim)), - } + _ => { + return Err(DdddError::DimensionMismatch { + expected: "2D (H,W) 或 3D (H,W,C)".to_string(), + actual: shape.to_vec(), + }); + } + }; + from_ndarray(array, color_mode) } -/// 对应 Python 的 png_rgba_black_preprocess -/// 将带有透明通道的图片转换为白色背景的 RGB 图片 - +/// 处理PNG图片的RGBA透明背景,将透明部分设置为白色背景 +// 对应 Python 的 png_rgba_black_preprocess pub fn png_rgba_white_preprocess(img: &DynamicImage) -> DynamicImage { // 1. 检查是否包含透明通道,如果没有,直接克隆并返回 if !img.color().has_alpha() { @@ -161,104 +131,71 @@ pub fn png_rgba_white_preprocess(img: &DynamicImage) -> DynamicImage { DynamicImage::ImageRgb8(background) } -pub fn image_to_numpy(image: &DynamicImage, mode: ColorMode) -> Result> { +/// 将 DynamicImage 转换为 array 数组 +pub fn image_to_ndarray(image: &DynamicImage, mode: ColorMode) -> Result> { // 1. 模式转换 (对应 utils.convert(target_mode)),此函数在时保留看后续优化是否需要替代image_to_ndarray // Rust utils 库通过 to_rgb8, to_luma8 等方法实现转换 let (width, height) = image.dimensions(); let (channels, raw) = match mode { - ColorMode::RGB => (3, image.to_rgb8().into_raw()), ColorMode::L => (1, image.to_luma8().into_raw()), + ColorMode::RGB => (3, image.to_rgb8().into_raw()), ColorMode::RGBA => (4, image.to_rgba8().into_raw()), }; - Array3::from_shape_vec((height as usize, width as usize, channels), raw) - .map_err(|e| anyhow!("Failed to build ndarray: {}", e)) + let array = Array3::from_shape_vec((height as usize, width as usize, channels), raw) + .map_err(ImagePreprocessReason::from)?; + Ok(array) +} +/// 将 array 数组转换为 DynamicImage +pub fn ndarray_to_image(array: ArrayViewD, mode: ColorMode) -> Result { + let shape = array.shape(); + + // 基础边界检查:至少要有 H 和 W 两个维度 + if shape.len() < 2 { + return Err(DdddError::DimensionMismatch { + expected: "At least 2D array [H, W]".to_string(), + actual: shape.to_vec(), + }); + } + from_ndarray(array, mode) } -pub fn numpy_to_image(array: ArrayViewD, mode: ColorMode) -> Result { +fn from_ndarray(array: ArrayViewD, mode: ColorMode) -> Result { let shape = array.shape(); - // 1. 基础维度检查 (必须是 H, W, C 三维数组) - if shape.len() != 3 { - bail!("Expected a 3D array (H, W, C), but got {}D", shape.len()); - } + // 映射:ndarray 的 shape 默认是 [Height, Width, (Channels)] + // image 库的 from_raw 接收 (width, height) let height = shape[0] as u32; let width = shape[1] as u32; - let channels = shape[2]; - // 2. 检查通道数是否与模式匹配 - let expected_channels = match mode { - ColorMode::L => 1, - ColorMode::RGB => 3, - ColorMode::RGBA => 4, - }; - if channels != expected_channels { - bail!( - "Mode {:?} expects {} channels, but array has {}", - mode, - expected_channels, - channels - ); - } - // 确保数据连续性 (C-order) + + // 1. 确保数据在内存中是连续的 (C order) let standard = array.as_standard_layout(); let (raw_data, _) = standard.to_owned().into_raw_vec_and_offset(); - + let raw_len = raw_data.len(); + // 2. 重新解释内存并构建 ImageBuffer match mode { ColorMode::L => ImageBuffer::, _>::from_raw(width, height, raw_data) - .map(DynamicImage::ImageLuma8), + .map(DynamicImage::ImageLuma8).ok_or_else(|| { + DdddError::PreprocessError(ImagePreprocessReason::Custom(format!( + "Failed to construct Luma image: buffer size {} does not match expected {} ({}x{}x1)", + raw_len, width * height * 1, width, height + ))) + }), ColorMode::RGB => ImageBuffer::, _>::from_raw(width, height, raw_data) - .map(DynamicImage::ImageRgb8), + .map(DynamicImage::ImageRgb8).ok_or_else(|| { + DdddError::PreprocessError(ImagePreprocessReason::Custom(format!( + "Failed to construct RGB image: buffer size {} does not match expected {} ({}x{}x3)", + raw_len, width * height * 3, width, height + ))) + }), ColorMode::RGBA => ImageBuffer::, _>::from_raw(width, height, raw_data) - .map(DynamicImage::ImageRgba8), + .map(DynamicImage::ImageRgba8).ok_or_else(|| { + DdddError::PreprocessError(ImagePreprocessReason::Custom(format!( + "Failed to construct RGBA image: buffer size {} does not match expected {} ({}x{}x4)", + raw_len, width * height * 4, width, height + ))) + }), } - .ok_or_else(|| anyhow!("Failed to construct ImageBuffer. Buffer size might be incorrect.")) -} -pub fn image_to_ndarray(img: &DynamicImage) -> Array3 { - let (width, height) = img.dimensions(); - - // 1. 强制转为 RGB8 (丢弃 Alpha 通道,与 Python 的 target_mode='RGB' 对齐) - let rgb_img = img.to_rgb8(); - - // 2. 获取原始像素数据 - let raw_data = rgb_img.into_raw(); - - // 3. 构造数组 (通道数改为 3) - Array3::from_shape_vec((height as usize, width as usize, 3), raw_data) - .expect("Failed to construct ndarray from utils") // 建议显式报错,而不是返回全黑图 } -#[allow(dead_code)] -fn save_rust_result(result: &ImageBuffer, Vec>, filename: &str) { - let (width, height) = result.dimensions(); - - // 1. 寻找最值进行归一化 - let mut max_val = f32::MIN; - let mut min_val = f32::MAX; - for p in result.pixels() { - if p.0[0] > max_val { - max_val = p.0[0]; - } - if p.0[0] < min_val { - min_val = p.0[0]; - } - } - - // 2. 创建 8 位灰度图 - let mut out_buf = ImageBuffer::new(width, height); - for y in 0..height { - for x in 0..width { - let val = result.get_pixel(x, y).0[0]; - let normalized = if max_val > min_val { - ((val - min_val) / (max_val - min_val) * 255.0) as u8 - } else { - 0u8 - }; - out_buf.put_pixel(x, y, Luma([normalized])); - } - } - - // 3. 保存 - DynamicImage::ImageLuma8(out_buf).save(filename).unwrap(); - println!("Rust 结果热力图已保存至: {}", filename); -} diff --git a/ddddocr-core/src/utils/image_processor.rs b/ddddocr-core/src/utils/image_processor.rs index 7068f72..39d103c 100644 --- a/ddddocr-core/src/utils/image_processor.rs +++ b/ddddocr-core/src/utils/image_processor.rs @@ -1,6 +1,6 @@ -use image::{DynamicImage, GrayImage, ImageBuffer, Luma, imageops::FilterType}; +use image::{imageops::FilterType, DynamicImage, GrayImage, ImageBuffer, Luma}; -use ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip}; +use ndarray::{azip, Array2, Array3, ArrayView2, ArrayView3}; use std::cmp::{max, min}; // 模拟openCV @@ -174,8 +174,6 @@ pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) { (h_opencv, s_opencv, v_opencv) } - - /// 对应 Python 的 convert_to_grayscale /// 将图像转换为灰度图 (L模式) pub fn convert_to_grayscale(image: &DynamicImage) -> GrayImage { diff --git a/ddddocr-core/src/utils/tensor_transform.rs b/ddddocr-core/src/utils/tensor_transform.rs index 2910402..5516e95 100644 --- a/ddddocr-core/src/utils/tensor_transform.rs +++ b/ddddocr-core/src/utils/tensor_transform.rs @@ -6,16 +6,22 @@ pub fn normalize_ocr_logits(array: ndarray::ArrayD, shape: &[usize]) -> Res let (steps, classes, data_dyn_view) = match shape.len() { 3 => { if shape[1] == 1 { + // 形状: [Steps, 1, Classes] (shape[0], shape[2], array) } else if shape[0] == 1 { + // 形状: [1, Steps, Classes] (shape[1], shape[2], array) } else { + // 默认取第一个 batch: [Batch, Steps, Classes] // 使用 ndarray 的 s! 宏,对应 Python 的 output[0, :, :] let sliced = array.slice_move(s![0, .., ..]); (shape[1], shape[2], sliced.into_dyn()) } } + // 形状: [Steps, Classes] 2 => (shape[0], shape[1], array), + // 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑) + // 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑 1 => (1, shape[0], array), _ => { return Err(DdddError::DimensionMismatch { diff --git a/ddddocr-tract/tests/ocr_test.rs b/ddddocr-tract/tests/ocr_test.rs index fe0c9bb..a7daff1 100644 --- a/ddddocr-tract/tests/ocr_test.rs +++ b/ddddocr-tract/tests/ocr_test.rs @@ -1,10 +1,10 @@ +use anyhow::Context; use ddddocr_core::det::DetectionResult; -use ddddocr_core::{DetBuilder, Detector, ModelMetadata, Ocr, Slider}; // 假设你的包名是这个 -use ddddocr_tract::{DetSession,OcrSession}; -use image::{DynamicImage, Rgb}; +use ddddocr_core::{DetBuilder, Detector, ModelMetadata, Ocr, Slider}; // 假设你的包名是这个 +use ddddocr_tract::{DetSession, OcrSession}; +use image::{DynamicImage, ImageBuffer, Luma, Rgb}; use std::fs; use std::path::Path; -use anyhow::Context; use tract_onnx::prelude::{ShapeFact, TDim}; mod char_slice; @@ -67,7 +67,40 @@ fn save_debug_image( img.save(output_path)?; Ok(()) } +#[allow(dead_code)] +fn save_rust_result(result: &ImageBuffer, Vec>, filename: &str) { + let (width, height) = result.dimensions(); + // 1. 寻找最值进行归一化 + let mut max_val = f32::MIN; + let mut min_val = f32::MAX; + for p in result.pixels() { + if p.0[0] > max_val { + max_val = p.0[0]; + } + if p.0[0] < min_val { + min_val = p.0[0]; + } + } + + // 2. 创建 8 位灰度图 + let mut out_buf = ImageBuffer::new(width, height); + for y in 0..height { + for x in 0..width { + let val = result.get_pixel(x, y).0[0]; + let normalized = if max_val > min_val { + ((val - min_val) / (max_val - min_val) * 255.0) as u8 + } else { + 0u8 + }; + out_buf.put_pixel(x, y, Luma([normalized])); + } + } + + // 3. 保存 + DynamicImage::ImageLuma8(out_buf).save(filename).unwrap(); + println!("Rust 结果热力图已保存至: {}", filename); +} #[test] fn test_full_classification() { // 1. 初始化模型 @@ -84,7 +117,8 @@ fn test_full_classification() { .expect("模型加载失败"); // 2. 加载测试图片 - let img = image::open("D:/CNWei/CNW/Rust/ddddocr-rs/samples/code2.png").expect("测试图片不存在"); + let img = + image::open("D:/CNWei/CNW/Rust/ddddocr-rs/samples/code2.png").expect("测试图片不存在"); // 3. 执行识别 let result = Ocr::new(&ocr) @@ -117,7 +151,11 @@ fn test_det_load() -> anyhow::Result<()> { println!("未检测到任何目标。"); } else { // 如果 save_debug_image 报错,记得去把它的入参类型和内部访问也改为 DetectionResult - save_debug_image(&img, &bboxes, "D:/CNWei/CNW/Rust/ddddocr-rs/samples/result.jpg")?; + save_debug_image( + &img, + &bboxes, + "D:/CNWei/CNW/Rust/ddddocr-rs/samples/result.jpg", + )?; for (i, bbox) in bboxes.iter().enumerate() { // 【修改点 3】将原来的 bbox[0].. 索引访问改为结构体字段访问 @@ -133,8 +171,10 @@ fn test_real_slide_match() { // 1. 加载你准备好的测试图 // 假设图片放在项目根目录下的 assets 文件夹 - let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/hua.png").expect("请确保 samples/hua.png 存在"); - let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/huatu.png").expect("请确保 samples/huatu.png 存在"); + let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/hua.png") + .expect("请确保 samples/hua.png 存在"); + let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/huatu.png") + .expect("请确保 samples/huatu.png 存在"); // 2. 执行匹配 // 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false @@ -162,8 +202,10 @@ fn test_real_slide_comparison() { // 1. 加载你准备好的测试图 // 假设图片放在项目根目录下的 assets 文件夹 - let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/ken.jpg").expect("请确保 samples/ken.jpg 存在"); - let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/kenyuan.jpg").expect("请确保 samples/kenyuan.jpg 存在"); + let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/ken.jpg") + .expect("请确保 samples/ken.jpg 存在"); + let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/kenyuan.jpg") + .expect("请确保 samples/kenyuan.jpg 存在"); // 2. 执行匹配 // 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false @@ -189,7 +231,10 @@ fn test_real_slide_comparison() { #[test] fn test_resolve_shape_logic_direct() { // 创建一个哑 ModelLoader 实例(session 用不上,因为我们直接测私有方法) - let loader = ModelLoader::model_for_path("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",).expect("建立测试模型图失败"); - let md_info=&loader.model_info().context("信息"); - println!("md_info: {:?}",md_info); -} \ No newline at end of file + let loader = ModelLoader::model_for_path( + "D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx", + ) + .expect("建立测试模型图失败"); + let md_info = &loader.model_info().context("信息"); + println!("md_info: {:?}", md_info); +}