diff --git a/Cargo.toml b/Cargo.toml index 6202d92..f07d8f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,5 +19,5 @@ base64 = "0.22.1" imageproc = { version = "0.26.2", default-features = true } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" -ndarray="0.16.1" +ndarray = "0.16.1" thiserror = "1.0" # 刚好可以开始接入你需要的标准库错误处理 diff --git a/ddddocr-core/src/error.rs b/ddddocr-core/src/error.rs index c74aaf5..6d97359 100644 --- a/ddddocr-core/src/error.rs +++ b/ddddocr-core/src/error.rs @@ -18,59 +18,235 @@ pub(crate) const MODEL_DOWNLOAD_HELP: &str = "\ ================================================================================"; use thiserror::Error; +// +// #[derive(Error, Debug)] +// pub enum DdddError { +// // 【新增】专门处理文件读取、路径不存在等原生 I/O 错误 +// #[error("系统网络或文件 I/O 异常: {0}")] +// Io(#[from] std::io::Error), +// +// #[error("图像预处理失败: {0}")] +// PreprocessError(#[from] ImagePreprocessReason), +// +// #[error("模型推理引擎内部发生异常: {0}")] +// EngineError(#[from] anyhow::Error), +// +// #[error("CTC 解码错误: {0}")] +// DecodeError(String), +// +// #[error("维度转换失败,预期维度 {expected},实际形状为 {actual:?}")] +// DimensionMismatch { +// expected: String, +// actual: Vec, +// }, +// +// #[error("内存不连续,无法执行零拷贝操作")] +// NonContiguousMemory, +// +// #[error("未知的模型输出格式")] +// UnknownOutputFormat, +// +// #[error("解析节点 Fact 失败")] +// 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; +// ===================================================================== +// 1. 顶层全局 Error 分流器 (去 anyhow 化,完全基于标准库/自定义类型) +// ===================================================================== #[derive(Error, Debug)] pub enum DdddError { - // 【新增】专门处理文件读取、路径不存在等原生 I/O 错误 - #[error("系统网络或文件 I/O 异常: {0}")] - Io(#[from] std::io::Error), + // /// 系统文件、网络等原生 I/O 异常 (高优先级自动转换) + // #[error("系统网络或文件 I/O 异常: {0}")] + // Io(#[from] std::io::Error), + /// 图像预处理阶段发生异常 #[error("图像预处理失败: {0}")] - PreprocessError(#[from] ImagePreprocessReason), + Preprocess(#[from] ImagePreprocessReason), - #[error("模型推理引擎内部发生异常: {0}")] - EngineError(#[from] anyhow::Error), + /// 推理引擎与张量操作阶段发生异常 + #[error("推理与模型输入/输出张量异常: {0}")] + Inference(#[from] TensorErrorReason), - #[error("CTC 解码错误: {0}")] - DecodeError(String), + /// 算法后处理解码阶段发生异常 + #[error("后处理解码错误: {0}")] + Decode(#[from] DecodeReason), - #[error("维度转换失败,预期维度 {expected},实际形状为 {actual:?}")] - DimensionMismatch { + /// 框架内部不可恢复的逻辑断言错误(例如解析节点 Fact 失败) + #[error("内部严重逻辑错误: {0}")] + Internal(String), + + /// 【流派一核心】接替 anyhow::Error 的用户自定义扩展错误 + /// 承载任何第三方扩展、解密、特定预处理插件在执行时产生的自定义错误 + #[error("用户自定义扩展错误: {0}")] + Other(Box), +} + +// ===================================================================== +// 2. 子领域 A: 图像预处理错误类型 +// ===================================================================== + +#[derive(Error, Debug)] +pub enum ImagePreprocessReason { + // #[error("图片加载或解码失败: {0}")] + // ImageIo(#[from] image::ImageError), + // image_io + #[error("图片转矩阵(ndarray)基础操作失败: {0}")] + Ndarray(#[from] ndarray::ShapeError), + // image_io + #[error("图像矩阵维度不合规!预期: {expected},实际图像形状: {actual:?}")] + InvalidImageDimensions { expected: String, actual: Vec, }, + // image_io + /// 从 ndarray 原始数据构建图像缓冲区时,缓冲区长度与分辨率/通道数不匹配 + #[error("图像缓冲区长度不匹配!预期大小: {expected},实际大小: {actual} (分辨率: {width}x{height}, 通道数: {channels})")] + BufferLengthMismatch { + expected: usize, + actual: usize, + width: u32, + height: u32, + channels: usize, + }, + // image_io + #[error("不支持的图像通道数: {0} (仅支持单通道灰度L、3通道RGB、4通道RGBA)")] + UnsupportedChannels(usize), + + // #[error("Base64 解码失败: {0}")] + // Base64(#[from] base64::DecodeError), + // + // #[error("Base64 头部格式不正确,缺少 ';base64,' 分隔符")] + // InvalidBase64Header, + + // #[error("其他预处理错误: {0}")] + // Other(String), +} + +// ===================================================================== +// 3. 子领域 B: 推理与张量操作错误类型 +// ===================================================================== + +#[derive(Error, Debug)] +pub enum TensorErrorReason { + /// 替换原有的 anyhow::Error,明确将 Tract/ONNX 引擎底层报错序列化为干净的 String + #[error("推理引擎内部发生异常: {0}")] + EngineError(String), + + /// 模型张量维度不匹配 (原有的顶层 DimensionMismatch 被优雅地归入本模块) + #[error("模型张量维度不匹配!预期: {expected},实际 Tensor 形状: {actual:?}")] + TensorDimensionMismatch { + expected: String, + actual: Vec, + }, + + /// 新增:针对后处理 Logits 矩阵变形(Reshape)失败的精细化错误 + /// 直接包装 ndarray::ShapeError,保留强类型,完美支持 match + #[error("OCR Logits 矩阵变形失败: {0}")] + LogitsDimensionMismatch(#[from] ndarray::ShapeError), + + /// 张量内存布局不是连续的 #[error("内存不连续,无法执行零拷贝操作")] NonContiguousMemory, + /// 模型的输出数据类型或格式不受支持 #[error("未知的模型输出格式")] UnknownOutputFormat, - - #[error("解析节点 Fact 失败")] - InternalError(String), } -/// 专门服务于预处理的子错误枚举,保留全部底层上下文 +// ===================================================================== +// 4. 子领域 C: 算法解码错误类型 +// ===================================================================== + #[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), +pub enum DecodeReason { + /// CTC 解码器解码过程中的逻辑报错 + #[error("CTC 解码异常: {0}")] + CtcDecodeError(String), } -/// 统一用我们自己的 DdddError 包装 Result -pub type Result = std::result::Result; +// ===================================================================== +// 5. 【自定义错误安全注入】不使用全局 `#[from]`,采用显式包装避免特化冲突 +// ===================================================================== + +impl DdddError { + /// 提供类似 std::io::Error::new 的构造函数,方便手动且无痛地包装任意第三方错误 + pub fn new(error: E) -> Self + where + E: Into>, + { + DdddError::Other(error.into()) + } + // ----------------------------------------------------------------- + // 2.3 优化:提供一键判断与转换的快捷方法(Downcasting Helpers) + // ----------------------------------------------------------------- + + /// 快速判断是否是系统 I/O 错误 + // pub fn is_io_error(&self) -> bool { + // matches!(self, DdddError::Io(_)) + // } + + /// 尝试将错误转换为引用形式的 `std::io::Error` + // pub fn as_io_error(&self) -> Option<&std::io::Error> { + // match self { + // DdddError::Io(err) => Some(err), + // _ => None, + // } + // } + + /// 快速判断是否是预处理阶段的图片维度不合规错误 + pub fn is_invalid_dimensions(&self) -> bool { + matches!( + self, + DdddError::Preprocess(ImagePreprocessReason::InvalidImageDimensions { .. }) + ) + } + + /// 快速判断是否是因为图片通道数不合规导致的失败 + pub fn is_unsupported_channels(&self) -> bool { + matches!( + self, + DdddError::Preprocess(ImagePreprocessReason::UnsupportedChannels(_)) + ) + } + + /// 提取出底层最原始的那个错误(无论是 IO、预处理、推理、还是第三方扩展错误) + /// 方便外层统一打印更深层的 `source` 链条 + pub fn source_error(&self) -> Option<&(dyn std::error::Error + 'static)> { + use std::error::Error; + match self { + // DdddError::Io(err) => Some(err), + DdddError::Preprocess(err) => Some(err), + DdddError::Inference(err) => Some(err), + DdddError::Decode(err) => Some(err), + DdddError::Other(err) => Some(err.as_ref()), + DdddError::Internal(_) => None, // Internal 内部目前只有 String,没有底层的 Error source + } + } + +} diff --git a/ddddocr-core/src/ocr/executor.rs b/ddddocr-core/src/ocr/executor.rs index fd7071f..eeb0d6e 100644 --- a/ddddocr-core/src/ocr/executor.rs +++ b/ddddocr-core/src/ocr/executor.rs @@ -2,7 +2,7 @@ use crate::ocr::metadata::Resize; use crate::ocr::color_filter::{HsvRange, apply_to_image}; // use ddddocr_tract::session::{ModelOutput, OcrSession}; -use crate::utils::image_io::png_rgba_white_preprocess; +use crate::utils::image_convert::png_rgba_white_preprocess; use crate::utils::image_processor::{convert_to_grayscale, resize_image}; use anyhow::Result; use image::DynamicImage; diff --git a/ddddocr-core/src/slide.rs b/ddddocr-core/src/slide.rs index 87beada..3c87b32 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,ColorMode}; +use crate::utils::image_convert::{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}; diff --git a/ddddocr-core/src/utils.rs b/ddddocr-core/src/utils.rs index 0163135..bfe5fa4 100644 --- a/ddddocr-core/src/utils.rs +++ b/ddddocr-core/src/utils.rs @@ -1,8 +1,8 @@ -pub mod image_io; +pub mod image_convert; pub mod image_processor; mod tensor_transform; mod image_helper; // 对外统一暴露干净的 API 语义层 pub use tensor_transform::normalize_ocr_logits; -pub use image_io::{ColorMode}; +pub use image_convert::{ColorMode}; diff --git a/ddddocr-core/src/utils/image_io.rs b/ddddocr-core/src/utils/image_convert.rs similarity index 67% rename from ddddocr-core/src/utils/image_io.rs rename to ddddocr-core/src/utils/image_convert.rs index 57a13cc..c4a0e81 100644 --- a/ddddocr-core/src/utils/image_io.rs +++ b/ddddocr-core/src/utils/image_convert.rs @@ -1,9 +1,6 @@ use crate::error::{DdddError, ImagePreprocessReason, Result}; -use base64::{Engine as _, engine::general_purpose}; use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba}; -use ndarray::{Array3, ArrayD, ArrayViewD}; -use std::fs; -use std::path::{Path, PathBuf}; +use ndarray::{Array3, ArrayViewD}; #[derive(Debug)] pub enum ColorMode { @@ -11,40 +8,10 @@ pub enum ColorMode { RGBA, L, } -/// 将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..] - } else { - &b64_str - }; - - let bytes = general_purpose::STANDARD - .decode(clean_b64.trim()) - .map_err(ImagePreprocessReason::from)?; - - let img = image::load_from_memory(&bytes).map_err(ImagePreprocessReason::from)?; - Ok(img) -} - -/// 读取图片文件并转换为 base64 编码字符串 -// 对应 Python 版 get_img_base64 -pub fn img_base64>(image_path: P) -> Result { - // 1. 读取文件原始字节流 - // 使用 AsRef 泛型可以让函数同时支持 String, &str, PathBuf 等类型 - let image_data = fs::read(&image_path)?; - - // 2. 进行 Base64 编码 - // 使用 STANDARD 引擎对齐 Python 的 base64.b64encode - let b64_string = general_purpose::STANDARD.encode(image_data); - - Ok(b64_string) -} /// 封装数组转图像的逻辑, // 对应 Python 版 _numpy_to_pil_image -pub(crate) fn ndarray_to_pil_image(array: ArrayViewD) -> Result { +pub fn ndarray_to_hwc_image(array: ArrayViewD) -> Result { let shape = array.shape(); let dim = shape.len(); @@ -68,17 +35,19 @@ pub(crate) fn ndarray_to_pil_image(array: ArrayViewD) -> Result ColorMode::RGBA, _ => { - return Err(DdddError::PreprocessError( + return Err(DdddError::Preprocess( ImagePreprocessReason::UnsupportedChannels(c), )); } } } _ => { - return Err(DdddError::DimensionMismatch { - expected: "2D (H,W) 或 3D (H,W,C)".to_string(), - actual: shape.to_vec(), - }); + return Err(DdddError::Preprocess( + ImagePreprocessReason::InvalidImageDimensions { + expected: "2D (H,W) 或 3D (H,W,C)".to_string(), + actual: shape.to_vec(), + }, + )); } }; from_ndarray(array, color_mode) @@ -153,10 +122,12 @@ pub fn ndarray_to_image(array: ArrayViewD, mode: ColorMode) -> Result, mode: ColorMode) -> Result let standard = array.as_standard_layout(); let (raw_data, _) = standard.to_owned().into_raw_vec_and_offset(); let raw_len = raw_data.len(); + + // 获取当前模式对应的通道数 + let channels = match mode { + ColorMode::L => 1, + ColorMode::RGB => 3, + ColorMode::RGBA => 4, + }; + + let expected_len = (width * height) as usize * channels; + + // 构造通用错误闭包,避免 match 分支中重复编写冗长的错误对象 + let make_err = || { + DdddError::Preprocess(ImagePreprocessReason::BufferLengthMismatch { + expected: expected_len, + actual: raw_len, + width, + height, + channels, + }) + }; + // 2. 重新解释内存并构建 ImageBuffer match mode { ColorMode::L => ImageBuffer::, _>::from_raw(width, height, raw_data) - .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 - ))) - }), + .map(DynamicImage::ImageLuma8) + .ok_or_else(make_err), ColorMode::RGB => ImageBuffer::, _>::from_raw(width, height, raw_data) - .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 - ))) - }), + .map(DynamicImage::ImageRgb8) + .ok_or_else(make_err), ColorMode::RGBA => ImageBuffer::, _>::from_raw(width, height, raw_data) - .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 - ))) - }), + .map(DynamicImage::ImageRgba8) + .ok_or_else(make_err), } } - diff --git a/ddddocr-core/src/utils/image_helper.rs b/ddddocr-core/src/utils/image_helper.rs index 2863472..8d46de3 100644 --- a/ddddocr-core/src/utils/image_helper.rs +++ b/ddddocr-core/src/utils/image_helper.rs @@ -1,10 +1,13 @@ use crate::error::{DdddError, ImagePreprocessReason, Result}; -use crate::utils::image_io::{base64_to_image, ndarray_to_pil_image}; +use crate::utils::image_convert::ndarray_to_hwc_image; +use base64::{Engine as _, engine::general_purpose}; use image::DynamicImage; use ndarray::ArrayViewD; +use std::fmt; +use std::fmt::{Debug, Formatter}; +use std::fs; use std::path::Path; use std::path::PathBuf; - pub struct Base64<'a>(pub &'a str); /// 专属图像输入源转换器 pub struct ImageSource { @@ -29,6 +32,21 @@ impl TryFromImage for ImageSource { Ok(Self { inner: img }) } } +#[derive(Debug)] +enum Base64ProcessError { + InvalidBase64Header, +} +impl fmt::Display for Base64ProcessError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Base64ProcessError::InvalidBase64Header => { + write!(f, "Base64 头部格式不正确,缺少 ';base64,' 分隔符") + } + } + } +} + +impl std::error::Error for Base64ProcessError {} // 2.2 路径类型 A: &str (最常用) impl<'a> TryFromImage<&'a str> for ImageSource { fn try_from_image(path_or_b64: &'a str) -> Result { @@ -37,13 +55,13 @@ impl<'a> TryFromImage<&'a str> for ImageSource { // 提取出真正的 base64 数据部分 let (_, clean_b64) = path_or_b64.split_once(";base64,").ok_or_else(|| { // 返回一个明确的、可读性极佳的格式错误 - DdddError::PreprocessError(ImagePreprocessReason::InvalidBase64Header) + DdddError::new(Base64ProcessError::InvalidBase64Header) })?; // 转换为 Base64 包装器,并复用其 TryFromImage 实现 Self::try_from_image(Base64(clean_b64)) } else { // 2. 否则,老老实实当作本地路径打开 - let img = image::open(path_or_b64).map_err(ImagePreprocessReason::from)?; + let img = image::open(path_or_b64).map_err(DdddError::new)?; Ok(Self { inner: img }) } } @@ -52,7 +70,7 @@ impl<'a> TryFromImage<&'a str> for ImageSource { // 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)?; + let img = image::open(path).map_err(DdddError::new)?; Ok(Self { inner: img }) } } @@ -71,7 +89,7 @@ impl TryFromImage for ImageSource { // 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)?; + let img = image::load_from_memory(bytes).map_err(DdddError::new)?; Ok(Self { inner: img }) } } @@ -79,7 +97,7 @@ impl<'a> TryFromImage<&'a [u8]> for ImageSource { // 4. 完美支持 ndarray 的借用 ArrayViewD impl<'a> TryFromImage> for ImageSource { fn try_from_image(array: ArrayViewD<'a, u8>) -> Result { - let img = ndarray_to_pil_image(array)?; + let img = ndarray_to_hwc_image(array)?; Ok(Self { inner: img }) } } @@ -100,3 +118,35 @@ where let img = ImageSource::try_from_image(input)?.into_image(); Ok(img) } + +/// 将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..] + } else { + &b64_str + }; + + let bytes = general_purpose::STANDARD + .decode(clean_b64.trim()) + // .map_err(|e| DdddError::new(e))?; + .map_err(DdddError::new)?; + + let img = image::load_from_memory(&bytes).map_err(DdddError::new)?; + Ok(img) +} + +/// 读取图片文件并转换为 base64 编码字符串 +// 对应 Python 版 get_img_base64 +pub fn img_to_base64>(image_path: P) -> Result { + // 1. 读取文件原始字节流 + // 使用 AsRef 泛型可以让函数同时支持 String, &str, PathBuf 等类型 + let image_data = fs::read(&image_path).map_err(DdddError::new)?; + + // 2. 进行 Base64 编码 + // 使用 STANDARD 引擎对齐 Python 的 base64.b64encode + let b64_string = general_purpose::STANDARD.encode(image_data); + + Ok(b64_string) +} diff --git a/ddddocr-core/src/utils/tensor_transform.rs b/ddddocr-core/src/utils/tensor_transform.rs index 5516e95..268a26d 100644 --- a/ddddocr-core/src/utils/tensor_transform.rs +++ b/ddddocr-core/src/utils/tensor_transform.rs @@ -1,8 +1,8 @@ -use ndarray::s; -use crate::error::{DdddError,Result}; use crate::OcrOutput; -/// 🌟 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵 -pub fn normalize_ocr_logits(array: ndarray::ArrayD, shape: &[usize]) -> Result { +use crate::error::{DdddError, Result, TensorErrorReason}; +use ndarray::s; +/// 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵 +pub fn normalize_ocr_logits(array: ndarray::ArrayViewD, shape: &[usize]) -> Result { let (steps, classes, data_dyn_view) = match shape.len() { 3 => { if shape[1] == 1 { @@ -24,19 +24,26 @@ pub fn normalize_ocr_logits(array: ndarray::ArrayD, shape: &[usize]) -> Res // 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑 1 => (1, shape[0], array), _ => { - return Err(DdddError::DimensionMismatch { - expected: "1D, 2D, or 3D OCR Logits".to_string(), - actual: shape.to_vec(), - }); + return Err(DdddError::Inference( + TensorErrorReason::TensorDimensionMismatch { + expected: "1D, 2D, or 3D OCR Logits".to_string(), + actual: shape.to_vec(), + }, + )); } }; // 转换为标准的 2D 静态矩阵 [Steps, Classes] let matrix_cow = data_dyn_view .to_shape(ndarray::Ix2(steps, classes)) - .map_err(|_| DdddError::DimensionMismatch { - expected: format!("无法将形状调整为 [{}, {}]", steps, classes), - actual: shape.to_vec(), + .map_err(|shape_err| { + // 如果是因为切片导致不连续且无法进行零拷贝变换,抛出 NonContiguousMemory + if !data_dyn_view.is_standard_layout() { + DdddError::Inference(TensorErrorReason::NonContiguousMemory) + } else { + // 否则,说明是纯粹的数据元素数量不对(Shape 不匹配),抛出专属的强类型错误 + DdddError::Inference(TensorErrorReason::LogitsDimensionMismatch(shape_err)) + } })? .to_owned(); diff --git a/ddddocr-tract/src/det/session.rs b/ddddocr-tract/src/det/session.rs index f50fd28..bd75cd7 100644 --- a/ddddocr-tract/src/det/session.rs +++ b/ddddocr-tract/src/det/session.rs @@ -1,6 +1,6 @@ use crate::loader::ModelLoader; use anyhow::Context; -use ddddocr_core::error::{DdddError, Result}; +use ddddocr_core::error::{DdddError, Result, TensorErrorReason}; use ddddocr_core::{DetEngine, DetOutput, InferenceEngine}; use ndarray::Ix3; use std::path::Path; @@ -42,26 +42,28 @@ impl InferenceEngine for DetSession { // let result = self.ocr.run(tvec!(tensor.into()))?; let tensor = Tensor::from(input_array); - let mut result = self - .session - .run(tvec!(tensor.into())) - .context("执行模型推理失败")?; + let mut result = self.session.run(tvec!(tensor.into())).map_err(|_| { + DdddError::Inference(TensorErrorReason::EngineError( + "执行模型推理失败".to_string(), + )) + })?; println!("模型输出原始数据: {:?}", result); // Ok(result.swap_remove(0).into_tensor()) let raw_tensor = result.swap_remove(0).into_tensor(); - let array_d = raw_tensor - .into_array::() - .context("Tract 实体张量无法转换为 ndarray::ArrayD")?; + let array_d = raw_tensor.into_array::().map_err(|_| { + DdddError::Inference(TensorErrorReason::EngineError( + "Tract 实体张量无法转换为 ndarray::ArrayD".to_string(), + )) + })?; // 提前利用克隆(Clone)备份好当前未转维度前的真实 shape (Vec) let actual_shape = array_d.shape().to_vec(); - let array3 = - array_d - .into_dimensionality::() - .map_err(|_| DdddError::DimensionMismatch { - expected: "3D 检测矩阵 [Batch, Box_Count, Box_Attributes]".to_string(), - actual: actual_shape, // 优雅降维失败时动态捕获 - })?; + let array3 = array_d.into_dimensionality::().map_err(|_| { + DdddError::Inference(TensorErrorReason::TensorDimensionMismatch { + expected: "3D 检测矩阵 [Batch, Box_Count, Box_Attributes]".to_string(), + actual: actual_shape, // 优雅降维失败时动态捕获 + }) + })?; Ok(DetOutput::Detection(array3)) // 在引擎内部消化掉 DatumType 强耦合 diff --git a/ddddocr-tract/src/loader.rs b/ddddocr-tract/src/loader.rs index e4431bd..2cb0554 100644 --- a/ddddocr-tract/src/loader.rs +++ b/ddddocr-tract/src/loader.rs @@ -56,12 +56,12 @@ impl ModelLoader { P: AsRef, { let session = onnx() - .model_for_path(model_path) - .with_context(|| "加载 ONNX 模型失败,请检查路径是否正确")? - .into_optimized() - .with_context(|| "优化 Tract 模型图失败")? - .into_runnable() - .with_context(|| "构建可运行 Tract 实例失败")?; + .model_for_path(model_path).map_err(DdddError::new)? + // .with_context(|| "加载 ONNX 模型失败,请检查路径是否正确")? + .into_optimized().map_err(DdddError::new)? + // .with_context(|| "优化 Tract 模型图失败")? + .into_runnable().map_err(DdddError::new)?; + // .with_context(|| "构建可运行 Tract 实例失败")?; Ok(Self { session }) } /// 策略 B:从内存字节流加载模型(配合 include_bytes! 使用) @@ -71,11 +71,13 @@ impl ModelLoader { let session = onnx() .model_for_read(&mut cursor) - .with_context(|| "从内存字节流解析 ONNX 模型失败")? + .map_err(DdddError::new)? + // .with_context(|| "从内存字节流解析 ONNX 模型失败")? .into_optimized() - .with_context(|| "优化 Tract 模型图失败")? - .into_runnable() - .with_context(|| "构建可运行 Tract 实例失败")?; + .map_err(DdddError::new)? + // .with_context(|| "优化 Tract 模型图失败")? + .into_runnable().map_err(DdddError::new)?; + // .with_context(|| "构建可运行 Tract 实例失败")?; Ok(Self { session }) } @@ -90,13 +92,13 @@ impl ModelLoader { // 使用私有辅助函数统一处理,消除重复代码 let inputs = self.resolve_tensors( model - .input_outlets() - .map_err(|e| DdddError::InternalError(format!("获取输入节点失败: {:?}", e)))?, + .input_outlets().map_err(DdddError::new)? + // .map_err(|e| DdddError::InternalError(format!("获取输入节点失败: {:?}", e)))?, )?; let outputs = self.resolve_tensors( model - .output_outlets() - .map_err(|e| DdddError::InternalError(format!("获取输出节点失败: {:?}", e)))?, + .output_outlets().map_err(DdddError::new)? + // .map_err(|e| DdddError::InternalError(format!("获取输出节点失败: {:?}", e)))?, )?; Ok(ModelInfo { @@ -113,9 +115,10 @@ impl ModelLoader { outlets .iter() .map(|&outlet_id| { - let fact = model.outlet_fact(outlet_id).map_err(|e| { - DdddError::InternalError(format!("解析节点 Fact 失败: {:?}", e)) - })?; + let fact = model.outlet_fact(outlet_id).map_err(DdddError::new)?; + // .map_err(|e| { + // DdddError::InternalError(format!("解析节点 Fact 失败: {:?}", e)) + // })?; let shape = self.resolve_shape(&fact.shape)?; let node_name = model.node(outlet_id.node).name.clone(); diff --git a/ddddocr-tract/src/ocr/session.rs b/ddddocr-tract/src/ocr/session.rs index cbbe58e..311f83d 100644 --- a/ddddocr-tract/src/ocr/session.rs +++ b/ddddocr-tract/src/ocr/session.rs @@ -1,6 +1,7 @@ use crate::loader::ModelLoader; use anyhow::Context; -use ddddocr_core::error::{DdddError, Result}; +use ddddocr_core::error::{DdddError, Result, TensorErrorReason}; +use ddddocr_core::utils::normalize_ocr_logits; use ddddocr_core::{InferenceEngine, ModelMetadata, OcrEngine, OcrOutput}; use ndarray::s; use std::path::Path; @@ -47,7 +48,12 @@ impl InferenceEngine for OcrSession { let mut result = self .session .run(tvec!(tensor.into())) - .context("执行模型推理失败")?; + .map_err(|_| { + DdddError::Inference(TensorErrorReason::EngineError( + "执行模型推理失败".to_string(), + )) + })?; + // .context("执行模型推理失败")?; println!("模型输出原始数据: {:?}", result); // Ok(result.swap_remove(0).into_tensor()) let raw_tensor = result.swap_remove(0).into_tensor(); @@ -56,16 +62,23 @@ impl InferenceEngine for OcrSession { DatumType::I64 => { let array_d = raw_tensor .into_array::() - .context("Tract 无法获取 i64 内存视图")?; + .map_err(|_| { + DdddError::Inference(TensorErrorReason::EngineError( + "Tract 无法获取 i64 内存视图".to_string(), + )) + })?; + // .context("Tract 无法获取 i64 内存视图")?; // 🌟 提前提取真实维度 let actual_shape = array_d.shape().to_vec(); // 转成标准的 Array1 传给 core let array1 = array_d .to_owned() .into_dimensionality::() - .map_err(|_| DdddError::DimensionMismatch { - expected: "1D 字符索引静态矩阵".to_string(), - actual: actual_shape, + .map_err(|_| { + DdddError::Inference(TensorErrorReason::TensorDimensionMismatch { + expected: "1D 字符索引静态矩阵".to_string(), + actual: actual_shape, + }) })?; Ok(OcrOutput::Indices(array1)) } @@ -74,51 +87,17 @@ impl InferenceEngine for OcrSession { println!("模型输出shape数据: {:?}", shape); let view = raw_tensor .to_array_view::() - .context("Tract 无法获取 f32 内存视图")?; - + .map_err(|_| { + DdddError::Inference(TensorErrorReason::EngineError( + "Tract 无法获取 f32 内存视图".to_string(), + )) + })?; // 1. 极其纯粹的、无拷贝的多维 Shape 压扁清洗 - let (steps, classes, data_dyn_view) = match shape.len() { - 3 => { - if shape[1] == 1 { - // 形状: [Steps, 1, Classes] -> 你的原有逻辑 - (shape[0], shape[2], view.into_dyn()) - } else if shape[0] == 1 { - // 形状: [1, Steps, Classes] -> 另一种常见导出格式 - (shape[1], shape[2], view.into_dyn()) - } else { - // 默认取第一个 batch: [Batch, Steps, Classes] - // 使用 slice 对应 Python 的 output[0, :, :] - let sliced = view.slice(s![0, .., ..]); - (shape[1], shape[2], sliced.into_dyn()) - } - } - // 形状: [Steps, Classes] -> 已经剥离了 Batch 维度 - 2 => (shape[0], shape[1], view.into_dyn()), - // 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑) - // 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑 - 1 => (1, shape[0], view.into_dyn()), - _ => { - return Err(DdddError::DimensionMismatch { - expected: "1D, 2D, or 3D OCR Logits".to_string(), - actual: shape.to_vec(), - }); - } - }; - - // 转换为标准的 2D 静态矩阵 [Steps, Classes] - let matrix_cow = data_dyn_view - .to_shape(ndarray::Ix2(steps, classes)) - .map_err(|_| DdddError::DimensionMismatch { - expected: format!("无法将形状调整为 [{}, {}]", steps, classes), - actual: shape.to_vec(), - })? - .to_owned(); // 转换为 Owned,断开与 tract 内存生命周期的绑定,方便传递给 core - - Ok(OcrOutput::Logits(matrix_cow)) + normalize_ocr_logits(view, shape) } _ => Err( // anyhow::anyhow!("不支持的模型输出数据类型: {:?}",raw_tensor.datum_type()) - DdddError::UnknownOutputFormat, + DdddError::Inference(TensorErrorReason::UnknownOutputFormat) ), } } diff --git a/ddddocr-tract/tests/ocr_test.rs b/ddddocr-tract/tests/ocr_test.rs index a7daff1..ef40f6e 100644 --- a/ddddocr-tract/tests/ocr_test.rs +++ b/ddddocr-tract/tests/ocr_test.rs @@ -232,7 +232,8 @@ fn test_real_slide_comparison() { 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", + // "D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx", + "D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_huashi666_i64.onnx", ) .expect("建立测试模型图失败"); let md_info = &loader.model_info().context("信息");