refactor(errors): 重构错误处理,支持强类型匹配并剥离 base64 依赖
- 新增 Other变体以及构造函数new - 剥离图像预处理中的 Base64 相关错误至业务层处理 - 引入强类型 `LogitsDimensionMismatch` 替代不便匹配的字符串错误 - 优化 `normalize_ocr_logits` 的转换流程,兼顾零拷贝性能与精细化报错 - 优化 全库错误处理
This commit is contained in:
@@ -18,59 +18,235 @@ pub(crate) const MODEL_DOWNLOAD_HELP: &str = "\
|
|||||||
================================================================================";
|
================================================================================";
|
||||||
|
|
||||||
use thiserror::Error;
|
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<usize>,
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// #[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<T> = std::result::Result<T, DdddError>;
|
||||||
|
// =====================================================================
|
||||||
|
// 1. 顶层全局 Error 分流器 (去 anyhow 化,完全基于标准库/自定义类型)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DdddError {
|
pub enum DdddError {
|
||||||
// 【新增】专门处理文件读取、路径不存在等原生 I/O 错误
|
// /// 系统文件、网络等原生 I/O 异常 (高优先级自动转换)
|
||||||
#[error("系统网络或文件 I/O 异常: {0}")]
|
// #[error("系统网络或文件 I/O 异常: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
// Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// 图像预处理阶段发生异常
|
||||||
#[error("图像预处理失败: {0}")]
|
#[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:?}")]
|
/// 框架内部不可恢复的逻辑断言错误(例如解析节点 Fact 失败)
|
||||||
DimensionMismatch {
|
#[error("内部严重逻辑错误: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
|
||||||
|
/// 【流派一核心】接替 anyhow::Error 的用户自定义扩展错误
|
||||||
|
/// 承载任何第三方扩展、解密、特定预处理插件在执行时产生的自定义错误
|
||||||
|
#[error("用户自定义扩展错误: {0}")]
|
||||||
|
Other(Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// 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,
|
expected: String,
|
||||||
actual: Vec<usize>,
|
actual: Vec<usize>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 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<usize>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 新增:针对后处理 Logits 矩阵变形(Reshape)失败的精细化错误
|
||||||
|
/// 直接包装 ndarray::ShapeError,保留强类型,完美支持 match
|
||||||
|
#[error("OCR Logits 矩阵变形失败: {0}")]
|
||||||
|
LogitsDimensionMismatch(#[from] ndarray::ShapeError),
|
||||||
|
|
||||||
|
/// 张量内存布局不是连续的
|
||||||
#[error("内存不连续,无法执行零拷贝操作")]
|
#[error("内存不连续,无法执行零拷贝操作")]
|
||||||
NonContiguousMemory,
|
NonContiguousMemory,
|
||||||
|
|
||||||
|
/// 模型的输出数据类型或格式不受支持
|
||||||
#[error("未知的模型输出格式")]
|
#[error("未知的模型输出格式")]
|
||||||
UnknownOutputFormat,
|
UnknownOutputFormat,
|
||||||
|
|
||||||
#[error("解析节点 Fact 失败")]
|
|
||||||
InternalError(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 专门服务于预处理的子错误枚举,保留全部底层上下文
|
// =====================================================================
|
||||||
|
// 4. 子领域 C: 算法解码错误类型
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ImagePreprocessReason {
|
pub enum DecodeReason {
|
||||||
#[error("图片加载或文件 I/O 失败: {0}")]
|
/// CTC 解码器解码过程中的逻辑报错
|
||||||
ImageIo(#[from] image::ImageError),
|
#[error("CTC 解码异常: {0}")]
|
||||||
|
CtcDecodeError(String),
|
||||||
#[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<T> = std::result::Result<T, DdddError>;
|
// 5. 【自定义错误安全注入】不使用全局 `#[from]`,采用显式包装避免特化冲突
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
impl DdddError {
|
||||||
|
/// 提供类似 std::io::Error::new 的构造函数,方便手动且无痛地包装任意第三方错误
|
||||||
|
pub fn new<E>(error: E) -> Self
|
||||||
|
where
|
||||||
|
E: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||||
|
{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::ocr::metadata::Resize;
|
|||||||
|
|
||||||
use crate::ocr::color_filter::{HsvRange, apply_to_image};
|
use crate::ocr::color_filter::{HsvRange, apply_to_image};
|
||||||
// use ddddocr_tract::session::{ModelOutput, OcrSession};
|
// 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 crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
|
|||||||
@@ -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;
|
||||||
use crate::utils::image_processor::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray};
|
use crate::utils::image_processor::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
pub mod image_io;
|
pub mod image_convert;
|
||||||
pub mod image_processor;
|
pub mod image_processor;
|
||||||
mod tensor_transform;
|
mod tensor_transform;
|
||||||
mod image_helper;
|
mod image_helper;
|
||||||
|
|
||||||
// 对外统一暴露干净的 API 语义层
|
// 对外统一暴露干净的 API 语义层
|
||||||
pub use tensor_transform::normalize_ocr_logits;
|
pub use tensor_transform::normalize_ocr_logits;
|
||||||
pub use image_io::{ColorMode};
|
pub use image_convert::{ColorMode};
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use crate::error::{DdddError, ImagePreprocessReason, Result};
|
use crate::error::{DdddError, ImagePreprocessReason, Result};
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
|
||||||
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
|
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
|
||||||
use ndarray::{Array3, ArrayD, ArrayViewD};
|
use ndarray::{Array3, ArrayViewD};
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ColorMode {
|
pub enum ColorMode {
|
||||||
@@ -11,40 +8,10 @@ pub enum ColorMode {
|
|||||||
RGBA,
|
RGBA,
|
||||||
L,
|
L,
|
||||||
}
|
}
|
||||||
/// 将base64编码的图片转换为 DynamicImage
|
|
||||||
pub fn base64_to_image(b64_str: &str) -> Result<DynamicImage> {
|
|
||||||
// 过滤掉可能存在的 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<P: AsRef<Path>>(image_path: P) -> Result<String> {
|
|
||||||
// 1. 读取文件原始字节流
|
|
||||||
// 使用 AsRef<Path> 泛型可以让函数同时支持 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
|
// 对应 Python 版 _numpy_to_pil_image
|
||||||
pub(crate) fn ndarray_to_pil_image(array: ArrayViewD<u8>) -> Result<DynamicImage> {
|
pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage> {
|
||||||
let shape = array.shape();
|
let shape = array.shape();
|
||||||
let dim = shape.len();
|
let dim = shape.len();
|
||||||
|
|
||||||
@@ -68,17 +35,19 @@ pub(crate) fn ndarray_to_pil_image(array: ArrayViewD<u8>) -> Result<DynamicImage
|
|||||||
// 对应 Python: array.shape[2] == 4 (RGBA H, W, 4)
|
// 对应 Python: array.shape[2] == 4 (RGBA H, W, 4)
|
||||||
4 => ColorMode::RGBA,
|
4 => ColorMode::RGBA,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DdddError::PreprocessError(
|
return Err(DdddError::Preprocess(
|
||||||
ImagePreprocessReason::UnsupportedChannels(c),
|
ImagePreprocessReason::UnsupportedChannels(c),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DdddError::DimensionMismatch {
|
return Err(DdddError::Preprocess(
|
||||||
|
ImagePreprocessReason::InvalidImageDimensions {
|
||||||
expected: "2D (H,W) 或 3D (H,W,C)".to_string(),
|
expected: "2D (H,W) 或 3D (H,W,C)".to_string(),
|
||||||
actual: shape.to_vec(),
|
actual: shape.to_vec(),
|
||||||
});
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
from_ndarray(array, color_mode)
|
from_ndarray(array, color_mode)
|
||||||
@@ -153,10 +122,12 @@ pub fn ndarray_to_image(array: ArrayViewD<u8>, mode: ColorMode) -> Result<Dynami
|
|||||||
|
|
||||||
// 基础边界检查:至少要有 H 和 W 两个维度
|
// 基础边界检查:至少要有 H 和 W 两个维度
|
||||||
if shape.len() < 2 {
|
if shape.len() < 2 {
|
||||||
return Err(DdddError::DimensionMismatch {
|
return Err(DdddError::Preprocess(
|
||||||
expected: "At least 2D array [H, W]".to_string(),
|
ImagePreprocessReason::InvalidImageDimensions {
|
||||||
|
expected: "至少为 2D array [H, W]".to_string(),
|
||||||
actual: shape.to_vec(),
|
actual: shape.to_vec(),
|
||||||
});
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
from_ndarray(array, mode)
|
from_ndarray(array, mode)
|
||||||
}
|
}
|
||||||
@@ -173,29 +144,37 @@ fn from_ndarray(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage>
|
|||||||
let standard = array.as_standard_layout();
|
let standard = array.as_standard_layout();
|
||||||
let (raw_data, _) = standard.to_owned().into_raw_vec_and_offset();
|
let (raw_data, _) = standard.to_owned().into_raw_vec_and_offset();
|
||||||
let raw_len = raw_data.len();
|
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
|
// 2. 重新解释内存并构建 ImageBuffer
|
||||||
match mode {
|
match mode {
|
||||||
ColorMode::L => ImageBuffer::<Luma<u8>, _>::from_raw(width, height, raw_data)
|
ColorMode::L => ImageBuffer::<Luma<u8>, _>::from_raw(width, height, raw_data)
|
||||||
.map(DynamicImage::ImageLuma8).ok_or_else(|| {
|
.map(DynamicImage::ImageLuma8)
|
||||||
DdddError::PreprocessError(ImagePreprocessReason::Custom(format!(
|
.ok_or_else(make_err),
|
||||||
"Failed to construct Luma image: buffer size {} does not match expected {} ({}x{}x1)",
|
|
||||||
raw_len, width * height * 1, width, height
|
|
||||||
)))
|
|
||||||
}),
|
|
||||||
ColorMode::RGB => ImageBuffer::<Rgb<u8>, _>::from_raw(width, height, raw_data)
|
ColorMode::RGB => ImageBuffer::<Rgb<u8>, _>::from_raw(width, height, raw_data)
|
||||||
.map(DynamicImage::ImageRgb8).ok_or_else(|| {
|
.map(DynamicImage::ImageRgb8)
|
||||||
DdddError::PreprocessError(ImagePreprocessReason::Custom(format!(
|
.ok_or_else(make_err),
|
||||||
"Failed to construct RGB image: buffer size {} does not match expected {} ({}x{}x3)",
|
|
||||||
raw_len, width * height * 3, width, height
|
|
||||||
)))
|
|
||||||
}),
|
|
||||||
ColorMode::RGBA => ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, raw_data)
|
ColorMode::RGBA => ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, raw_data)
|
||||||
.map(DynamicImage::ImageRgba8).ok_or_else(|| {
|
.map(DynamicImage::ImageRgba8)
|
||||||
DdddError::PreprocessError(ImagePreprocessReason::Custom(format!(
|
.ok_or_else(make_err),
|
||||||
"Failed to construct RGBA image: buffer size {} does not match expected {} ({}x{}x4)",
|
|
||||||
raw_len, width * height * 4, width, height
|
|
||||||
)))
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
use crate::error::{DdddError, ImagePreprocessReason, Result};
|
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 image::DynamicImage;
|
||||||
use ndarray::ArrayViewD;
|
use ndarray::ArrayViewD;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub struct Base64<'a>(pub &'a str);
|
pub struct Base64<'a>(pub &'a str);
|
||||||
/// 专属图像输入源转换器
|
/// 专属图像输入源转换器
|
||||||
pub struct ImageSource {
|
pub struct ImageSource {
|
||||||
@@ -29,6 +32,21 @@ impl TryFromImage<DynamicImage> for ImageSource {
|
|||||||
Ok(Self { inner: img })
|
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 (最常用)
|
// 2.2 路径类型 A: &str (最常用)
|
||||||
impl<'a> TryFromImage<&'a str> for ImageSource {
|
impl<'a> TryFromImage<&'a str> for ImageSource {
|
||||||
fn try_from_image(path_or_b64: &'a str) -> Result<Self> {
|
fn try_from_image(path_or_b64: &'a str) -> Result<Self> {
|
||||||
@@ -37,13 +55,13 @@ impl<'a> TryFromImage<&'a str> for ImageSource {
|
|||||||
// 提取出真正的 base64 数据部分
|
// 提取出真正的 base64 数据部分
|
||||||
let (_, clean_b64) = path_or_b64.split_once(";base64,").ok_or_else(|| {
|
let (_, clean_b64) = path_or_b64.split_once(";base64,").ok_or_else(|| {
|
||||||
// 返回一个明确的、可读性极佳的格式错误
|
// 返回一个明确的、可读性极佳的格式错误
|
||||||
DdddError::PreprocessError(ImagePreprocessReason::InvalidBase64Header)
|
DdddError::new(Base64ProcessError::InvalidBase64Header)
|
||||||
})?;
|
})?;
|
||||||
// 转换为 Base64 包装器,并复用其 TryFromImage 实现
|
// 转换为 Base64 包装器,并复用其 TryFromImage 实现
|
||||||
Self::try_from_image(Base64(clean_b64))
|
Self::try_from_image(Base64(clean_b64))
|
||||||
} else {
|
} else {
|
||||||
// 2. 否则,老老实实当作本地路径打开
|
// 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 })
|
Ok(Self { inner: img })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +70,7 @@ impl<'a> TryFromImage<&'a str> for ImageSource {
|
|||||||
// 2.3 路径类型 B: &Path (标准借用)
|
// 2.3 路径类型 B: &Path (标准借用)
|
||||||
impl<'a> TryFromImage<&'a Path> for ImageSource {
|
impl<'a> TryFromImage<&'a Path> for ImageSource {
|
||||||
fn try_from_image(path: &'a Path) -> Result<Self> {
|
fn try_from_image(path: &'a Path) -> Result<Self> {
|
||||||
let img = image::open(path).map_err(ImagePreprocessReason::from)?;
|
let img = image::open(path).map_err(DdddError::new)?;
|
||||||
Ok(Self { inner: img })
|
Ok(Self { inner: img })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +89,7 @@ impl TryFromImage<String> for ImageSource {
|
|||||||
// 2. 支持带有生命周期的借用:直接支持 &[u8](不强绑生命周期到 ImageSource 结构体上!)
|
// 2. 支持带有生命周期的借用:直接支持 &[u8](不强绑生命周期到 ImageSource 结构体上!)
|
||||||
impl<'a> TryFromImage<&'a [u8]> for ImageSource {
|
impl<'a> TryFromImage<&'a [u8]> for ImageSource {
|
||||||
fn try_from_image(bytes: &'a [u8]) -> Result<Self> {
|
fn try_from_image(bytes: &'a [u8]) -> Result<Self> {
|
||||||
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 })
|
Ok(Self { inner: img })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +97,7 @@ impl<'a> TryFromImage<&'a [u8]> for ImageSource {
|
|||||||
// 4. 完美支持 ndarray 的借用 ArrayViewD
|
// 4. 完美支持 ndarray 的借用 ArrayViewD
|
||||||
impl<'a> TryFromImage<ArrayViewD<'a, u8>> for ImageSource {
|
impl<'a> TryFromImage<ArrayViewD<'a, u8>> for ImageSource {
|
||||||
fn try_from_image(array: ArrayViewD<'a, u8>) -> Result<Self> {
|
fn try_from_image(array: ArrayViewD<'a, u8>) -> Result<Self> {
|
||||||
let img = ndarray_to_pil_image(array)?;
|
let img = ndarray_to_hwc_image(array)?;
|
||||||
Ok(Self { inner: img })
|
Ok(Self { inner: img })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,3 +118,35 @@ where
|
|||||||
let img = ImageSource::try_from_image(input)?.into_image();
|
let img = ImageSource::try_from_image(input)?.into_image();
|
||||||
Ok(img)
|
Ok(img)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将base64编码的图片转换为 DynamicImage
|
||||||
|
pub fn base64_to_image(b64_str: &str) -> Result<DynamicImage> {
|
||||||
|
// 过滤掉可能存在的 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<P: AsRef<Path>>(image_path: P) -> Result<String> {
|
||||||
|
// 1. 读取文件原始字节流
|
||||||
|
// 使用 AsRef<Path> 泛型可以让函数同时支持 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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use ndarray::s;
|
|
||||||
use crate::error::{DdddError,Result};
|
|
||||||
use crate::OcrOutput;
|
use crate::OcrOutput;
|
||||||
/// 🌟 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵
|
use crate::error::{DdddError, Result, TensorErrorReason};
|
||||||
pub fn normalize_ocr_logits(array: ndarray::ArrayD<f32>, shape: &[usize]) -> Result<OcrOutput> {
|
use ndarray::s;
|
||||||
|
/// 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵
|
||||||
|
pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) -> Result<OcrOutput> {
|
||||||
let (steps, classes, data_dyn_view) = match shape.len() {
|
let (steps, classes, data_dyn_view) = match shape.len() {
|
||||||
3 => {
|
3 => {
|
||||||
if shape[1] == 1 {
|
if shape[1] == 1 {
|
||||||
@@ -24,19 +24,26 @@ pub fn normalize_ocr_logits(array: ndarray::ArrayD<f32>, shape: &[usize]) -> Res
|
|||||||
// 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
|
// 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
|
||||||
1 => (1, shape[0], array),
|
1 => (1, shape[0], array),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DdddError::DimensionMismatch {
|
return Err(DdddError::Inference(
|
||||||
|
TensorErrorReason::TensorDimensionMismatch {
|
||||||
expected: "1D, 2D, or 3D OCR Logits".to_string(),
|
expected: "1D, 2D, or 3D OCR Logits".to_string(),
|
||||||
actual: shape.to_vec(),
|
actual: shape.to_vec(),
|
||||||
});
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 转换为标准的 2D 静态矩阵 [Steps, Classes]
|
// 转换为标准的 2D 静态矩阵 [Steps, Classes]
|
||||||
let matrix_cow = data_dyn_view
|
let matrix_cow = data_dyn_view
|
||||||
.to_shape(ndarray::Ix2(steps, classes))
|
.to_shape(ndarray::Ix2(steps, classes))
|
||||||
.map_err(|_| DdddError::DimensionMismatch {
|
.map_err(|shape_err| {
|
||||||
expected: format!("无法将形状调整为 [{}, {}]", steps, classes),
|
// 如果是因为切片导致不连续且无法进行零拷贝变换,抛出 NonContiguousMemory
|
||||||
actual: shape.to_vec(),
|
if !data_dyn_view.is_standard_layout() {
|
||||||
|
DdddError::Inference(TensorErrorReason::NonContiguousMemory)
|
||||||
|
} else {
|
||||||
|
// 否则,说明是纯粹的数据元素数量不对(Shape 不匹配),抛出专属的强类型错误
|
||||||
|
DdddError::Inference(TensorErrorReason::LogitsDimensionMismatch(shape_err))
|
||||||
|
}
|
||||||
})?
|
})?
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::loader::ModelLoader;
|
use crate::loader::ModelLoader;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use ddddocr_core::error::{DdddError, Result};
|
use ddddocr_core::error::{DdddError, Result, TensorErrorReason};
|
||||||
use ddddocr_core::{DetEngine, DetOutput, InferenceEngine};
|
use ddddocr_core::{DetEngine, DetOutput, InferenceEngine};
|
||||||
use ndarray::Ix3;
|
use ndarray::Ix3;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -42,25 +42,27 @@ impl InferenceEngine for DetSession {
|
|||||||
// let result = self.ocr.run(tvec!(tensor.into()))?;
|
// let result = self.ocr.run(tvec!(tensor.into()))?;
|
||||||
let tensor = Tensor::from(input_array);
|
let tensor = Tensor::from(input_array);
|
||||||
|
|
||||||
let mut result = self
|
let mut result = self.session.run(tvec!(tensor.into())).map_err(|_| {
|
||||||
.session
|
DdddError::Inference(TensorErrorReason::EngineError(
|
||||||
.run(tvec!(tensor.into()))
|
"执行模型推理失败".to_string(),
|
||||||
.context("执行模型推理失败")?;
|
))
|
||||||
|
})?;
|
||||||
println!("模型输出原始数据: {:?}", result);
|
println!("模型输出原始数据: {:?}", result);
|
||||||
// Ok(result.swap_remove(0).into_tensor())
|
// Ok(result.swap_remove(0).into_tensor())
|
||||||
let raw_tensor = result.swap_remove(0).into_tensor();
|
let raw_tensor = result.swap_remove(0).into_tensor();
|
||||||
let array_d = raw_tensor
|
let array_d = raw_tensor.into_array::<f32>().map_err(|_| {
|
||||||
.into_array::<f32>()
|
DdddError::Inference(TensorErrorReason::EngineError(
|
||||||
.context("Tract 实体张量无法转换为 ndarray::ArrayD")?;
|
"Tract 实体张量无法转换为 ndarray::ArrayD".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
// 提前利用克隆(Clone)备份好当前未转维度前的真实 shape (Vec<usize>)
|
// 提前利用克隆(Clone)备份好当前未转维度前的真实 shape (Vec<usize>)
|
||||||
let actual_shape = array_d.shape().to_vec();
|
let actual_shape = array_d.shape().to_vec();
|
||||||
|
|
||||||
let array3 =
|
let array3 = array_d.into_dimensionality::<Ix3>().map_err(|_| {
|
||||||
array_d
|
DdddError::Inference(TensorErrorReason::TensorDimensionMismatch {
|
||||||
.into_dimensionality::<Ix3>()
|
|
||||||
.map_err(|_| DdddError::DimensionMismatch {
|
|
||||||
expected: "3D 检测矩阵 [Batch, Box_Count, Box_Attributes]".to_string(),
|
expected: "3D 检测矩阵 [Batch, Box_Count, Box_Attributes]".to_string(),
|
||||||
actual: actual_shape, // 优雅降维失败时动态捕获
|
actual: actual_shape, // 优雅降维失败时动态捕获
|
||||||
|
})
|
||||||
})?;
|
})?;
|
||||||
Ok(DetOutput::Detection(array3))
|
Ok(DetOutput::Detection(array3))
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ impl ModelLoader {
|
|||||||
P: AsRef<std::path::Path>,
|
P: AsRef<std::path::Path>,
|
||||||
{
|
{
|
||||||
let session = onnx()
|
let session = onnx()
|
||||||
.model_for_path(model_path)
|
.model_for_path(model_path).map_err(DdddError::new)?
|
||||||
.with_context(|| "加载 ONNX 模型失败,请检查路径是否正确")?
|
// .with_context(|| "加载 ONNX 模型失败,请检查路径是否正确")?
|
||||||
.into_optimized()
|
.into_optimized().map_err(DdddError::new)?
|
||||||
.with_context(|| "优化 Tract 模型图失败")?
|
// .with_context(|| "优化 Tract 模型图失败")?
|
||||||
.into_runnable()
|
.into_runnable().map_err(DdddError::new)?;
|
||||||
.with_context(|| "构建可运行 Tract 实例失败")?;
|
// .with_context(|| "构建可运行 Tract 实例失败")?;
|
||||||
Ok(Self { session })
|
Ok(Self { session })
|
||||||
}
|
}
|
||||||
/// 策略 B:从内存字节流加载模型(配合 include_bytes! 使用)
|
/// 策略 B:从内存字节流加载模型(配合 include_bytes! 使用)
|
||||||
@@ -71,11 +71,13 @@ impl ModelLoader {
|
|||||||
|
|
||||||
let session = onnx()
|
let session = onnx()
|
||||||
.model_for_read(&mut cursor)
|
.model_for_read(&mut cursor)
|
||||||
.with_context(|| "从内存字节流解析 ONNX 模型失败")?
|
.map_err(DdddError::new)?
|
||||||
|
// .with_context(|| "从内存字节流解析 ONNX 模型失败")?
|
||||||
.into_optimized()
|
.into_optimized()
|
||||||
.with_context(|| "优化 Tract 模型图失败")?
|
.map_err(DdddError::new)?
|
||||||
.into_runnable()
|
// .with_context(|| "优化 Tract 模型图失败")?
|
||||||
.with_context(|| "构建可运行 Tract 实例失败")?;
|
.into_runnable().map_err(DdddError::new)?;
|
||||||
|
// .with_context(|| "构建可运行 Tract 实例失败")?;
|
||||||
|
|
||||||
Ok(Self { session })
|
Ok(Self { session })
|
||||||
}
|
}
|
||||||
@@ -90,13 +92,13 @@ impl ModelLoader {
|
|||||||
// 使用私有辅助函数统一处理,消除重复代码
|
// 使用私有辅助函数统一处理,消除重复代码
|
||||||
let inputs = self.resolve_tensors(
|
let inputs = self.resolve_tensors(
|
||||||
model
|
model
|
||||||
.input_outlets()
|
.input_outlets().map_err(DdddError::new)?
|
||||||
.map_err(|e| DdddError::InternalError(format!("获取输入节点失败: {:?}", e)))?,
|
// .map_err(|e| DdddError::InternalError(format!("获取输入节点失败: {:?}", e)))?,
|
||||||
)?;
|
)?;
|
||||||
let outputs = self.resolve_tensors(
|
let outputs = self.resolve_tensors(
|
||||||
model
|
model
|
||||||
.output_outlets()
|
.output_outlets().map_err(DdddError::new)?
|
||||||
.map_err(|e| DdddError::InternalError(format!("获取输出节点失败: {:?}", e)))?,
|
// .map_err(|e| DdddError::InternalError(format!("获取输出节点失败: {:?}", e)))?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(ModelInfo {
|
Ok(ModelInfo {
|
||||||
@@ -113,9 +115,10 @@ impl ModelLoader {
|
|||||||
outlets
|
outlets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&outlet_id| {
|
.map(|&outlet_id| {
|
||||||
let fact = model.outlet_fact(outlet_id).map_err(|e| {
|
let fact = model.outlet_fact(outlet_id).map_err(DdddError::new)?;
|
||||||
DdddError::InternalError(format!("解析节点 Fact 失败: {:?}", e))
|
// .map_err(|e| {
|
||||||
})?;
|
// DdddError::InternalError(format!("解析节点 Fact 失败: {:?}", e))
|
||||||
|
// })?;
|
||||||
|
|
||||||
let shape = self.resolve_shape(&fact.shape)?;
|
let shape = self.resolve_shape(&fact.shape)?;
|
||||||
let node_name = model.node(outlet_id.node).name.clone();
|
let node_name = model.node(outlet_id.node).name.clone();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::loader::ModelLoader;
|
use crate::loader::ModelLoader;
|
||||||
use anyhow::Context;
|
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 ddddocr_core::{InferenceEngine, ModelMetadata, OcrEngine, OcrOutput};
|
||||||
use ndarray::s;
|
use ndarray::s;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -47,7 +48,12 @@ impl InferenceEngine for OcrSession {
|
|||||||
let mut result = self
|
let mut result = self
|
||||||
.session
|
.session
|
||||||
.run(tvec!(tensor.into()))
|
.run(tvec!(tensor.into()))
|
||||||
.context("执行模型推理失败")?;
|
.map_err(|_| {
|
||||||
|
DdddError::Inference(TensorErrorReason::EngineError(
|
||||||
|
"执行模型推理失败".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
// .context("执行模型推理失败")?;
|
||||||
println!("模型输出原始数据: {:?}", result);
|
println!("模型输出原始数据: {:?}", result);
|
||||||
// Ok(result.swap_remove(0).into_tensor())
|
// Ok(result.swap_remove(0).into_tensor())
|
||||||
let raw_tensor = 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 => {
|
DatumType::I64 => {
|
||||||
let array_d = raw_tensor
|
let array_d = raw_tensor
|
||||||
.into_array::<i64>()
|
.into_array::<i64>()
|
||||||
.context("Tract 无法获取 i64 内存视图")?;
|
.map_err(|_| {
|
||||||
|
DdddError::Inference(TensorErrorReason::EngineError(
|
||||||
|
"Tract 无法获取 i64 内存视图".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
// .context("Tract 无法获取 i64 内存视图")?;
|
||||||
// 🌟 提前提取真实维度
|
// 🌟 提前提取真实维度
|
||||||
let actual_shape = array_d.shape().to_vec();
|
let actual_shape = array_d.shape().to_vec();
|
||||||
// 转成标准的 Array1 传给 core
|
// 转成标准的 Array1 传给 core
|
||||||
let array1 = array_d
|
let array1 = array_d
|
||||||
.to_owned()
|
.to_owned()
|
||||||
.into_dimensionality::<ndarray::Ix1>()
|
.into_dimensionality::<ndarray::Ix1>()
|
||||||
.map_err(|_| DdddError::DimensionMismatch {
|
.map_err(|_| {
|
||||||
|
DdddError::Inference(TensorErrorReason::TensorDimensionMismatch {
|
||||||
expected: "1D 字符索引静态矩阵".to_string(),
|
expected: "1D 字符索引静态矩阵".to_string(),
|
||||||
actual: actual_shape,
|
actual: actual_shape,
|
||||||
|
})
|
||||||
})?;
|
})?;
|
||||||
Ok(OcrOutput::Indices(array1))
|
Ok(OcrOutput::Indices(array1))
|
||||||
}
|
}
|
||||||
@@ -74,51 +87,17 @@ impl InferenceEngine for OcrSession {
|
|||||||
println!("模型输出shape数据: {:?}", shape);
|
println!("模型输出shape数据: {:?}", shape);
|
||||||
let view = raw_tensor
|
let view = raw_tensor
|
||||||
.to_array_view::<f32>()
|
.to_array_view::<f32>()
|
||||||
.context("Tract 无法获取 f32 内存视图")?;
|
.map_err(|_| {
|
||||||
|
DdddError::Inference(TensorErrorReason::EngineError(
|
||||||
|
"Tract 无法获取 f32 内存视图".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
// 1. 极其纯粹的、无拷贝的多维 Shape 压扁清洗
|
// 1. 极其纯粹的、无拷贝的多维 Shape 压扁清洗
|
||||||
let (steps, classes, data_dyn_view) = match shape.len() {
|
normalize_ocr_logits(view, shape)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
_ => Err(
|
_ => Err(
|
||||||
// anyhow::anyhow!("不支持的模型输出数据类型: {:?}",raw_tensor.datum_type())
|
// anyhow::anyhow!("不支持的模型输出数据类型: {:?}",raw_tensor.datum_type())
|
||||||
DdddError::UnknownOutputFormat,
|
DdddError::Inference(TensorErrorReason::UnknownOutputFormat)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ fn test_real_slide_comparison() {
|
|||||||
fn test_resolve_shape_logic_direct() {
|
fn test_resolve_shape_logic_direct() {
|
||||||
// 创建一个哑 ModelLoader 实例(session 用不上,因为我们直接测私有方法)
|
// 创建一个哑 ModelLoader 实例(session 用不上,因为我们直接测私有方法)
|
||||||
let loader = ModelLoader::model_for_path(
|
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("建立测试模型图失败");
|
.expect("建立测试模型图失败");
|
||||||
let md_info = &loader.model_info().context("信息");
|
let md_info = &loader.model_info().context("信息");
|
||||||
|
|||||||
Reference in New Issue
Block a user