refactor(core): 提炼公共类型

- 将 AxisDim、TensorInfo 等公共类型下沉至 ddddocr_core::types
- 项目结构优化
This commit is contained in:
2026-07-30 16:55:58 +08:00
parent 7d159c5702
commit a3c4614574
22 changed files with 349 additions and 408 deletions

View File

@@ -1,24 +1,24 @@
use crate::types::Session;
use ddddocr_core::DetOutput;
use ddddocr_core::error::{Result, TensorError};
use ddddocr_core::{DetEngine, DetOutput, InferenceEngine};
use ddddocr_core::traits::{DetEngine, InferenceEngine};
use ndarray::Ix3;
use ort::inputs;
use ort::value::TensorRef;
// use tract_onnx::prelude::{tvec, IntoTensor, Tensor};
#[derive(Debug)]
pub struct DetSession {
pub struct DetRuntime {
pub session: Session,
}
impl DetSession {
impl DetRuntime {
pub fn new(session: Session) -> Self {
Self { session }
}
}
impl InferenceEngine for DetSession {
impl InferenceEngine for DetRuntime {
type Output = DetOutput; // 明确绑定 OCR 小枚举
fn inference(&self, input_array: ndarray::Array4<f32>) -> Result<Self::Output, TensorError> {
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
@@ -28,7 +28,6 @@ impl InferenceEngine for DetSession {
.lock()
.map_err(|_| TensorError::Engine("获取 Session 锁失败 (Poisoned)".to_string()))?;
let result = session_guard
.run(inputs![TensorRef::from_array_view(&input_array).map_err(
|e| TensorError::Engine(format!("构建输入失败: {e}"))
@@ -45,8 +44,7 @@ impl InferenceEngine for DetSession {
TensorError::Engine("Tract 实体张量无法转换为 ndarray::ArrayD".to_string())
})?;
// 提前利用克隆(Clone)备份好当前未转维度前的真实 shape (Vec<usize>)
let shape_vec: Vec<usize> =
shape_ref.to_vec().iter().map(|v| *v as usize).collect();
let shape_vec: Vec<usize> = shape_ref.to_vec().iter().map(|v| *v as usize).collect();
let shape_vec_slice = shape_vec.as_slice();
let view = ndarray::ArrayViewD::from_shape(shape_vec_slice, slice)
@@ -64,4 +62,4 @@ impl InferenceEngine for DetSession {
}
}
impl DetEngine for DetSession {}
impl DetEngine for DetRuntime {}

View File

@@ -5,5 +5,5 @@ mod types;
pub use ddddocr_core::{SlideResult, Slider,OcrBuilder};
pub use det::session::DetSession;
pub use ocr::session::OcrSession;
pub use det::session::DetRuntime;
pub use ocr::session::OcrRuntime;

View File

@@ -3,5 +3,5 @@ mod metadata;
mod model;
pub use error::{Error, ParseError, Result};
pub use metadata::{ModelMetadataDto, NormalizationDto, TractModelMetadata};
pub use model::OrtModelLoader;
pub use metadata::{ModelMetadataDto, NormalizationDto, Metadata};
pub use model::ModelLoader;

View File

@@ -42,7 +42,7 @@ fn default_normalization() -> NormalizationDto {
}
/// Tract 专属扩展trait 或 工具函数
pub trait TractModelMetadata: Sized {
pub trait Metadata: Sized {
fn from_json_str(json_str: &str) -> Result<Self>;
/// 机制 2从内存字节流加载极大地方便 include_bytes! 或网络下载)
fn from_json_bytes(bytes: &[u8]) -> Result<Self> {
@@ -50,7 +50,7 @@ pub trait TractModelMetadata: Sized {
Self::from_json_str(json_str)
}
}
impl TractModelMetadata for ModelMetadata {
impl Metadata for ModelMetadata {
// --- 优雅的工厂模式构造器 ---
fn from_json_str(json_str: &str) -> Result<ModelMetadata> {
let dto: ModelMetadataDto = serde_json::from_str(json_str)?;

View File

@@ -1,27 +1,27 @@
use crate::loader::Error;
use crate::loader::error::{BuildError, ParseError, Result};
use crate::types::Session;
use ddddocr_core::ModelBuilder;
use ddddocr_core::traits::Loader;
use ort::session::Session as OrtSession;
use ort::session::builder::SessionBuilder;
use std::sync::{Arc, Mutex};
pub struct OrtModelLoader;
impl OrtModelLoader {
/// 获取针对 ORT 后端的链式构建器
pub fn builder() -> OrtModelBuilder {
OrtModelBuilder::default()
}
}
// pub struct OrtModelLoader;
//
// impl OrtModelLoader {
// /// 获取针对 ORT 后端的链式构建器
// pub fn builder() -> OrtModelBuilder {
// OrtModelBuilder::default()
// }
// }
/// ORT 专用的链式构建器
#[derive(Debug, Clone)]
pub struct OrtModelBuilder {
pub struct ModelLoader {
use_gpu: bool,
device_id: i32,
intra_threads: Option<usize>,
}
impl Default for OrtModelBuilder {
impl Default for ModelLoader {
fn default() -> Self {
Self {
use_gpu: false,
@@ -30,7 +30,7 @@ impl Default for OrtModelBuilder {
}
}
}
impl OrtModelBuilder {
impl ModelLoader {
/// 开启或关闭 GPU 加速
pub fn use_gpu(mut self, enable: bool) -> Self {
self.use_gpu = enable;
@@ -83,11 +83,11 @@ impl OrtModelBuilder {
}
}
impl ModelBuilder for OrtModelBuilder {
impl Loader for ModelLoader {
type Session = Session;
type Error = Error;
fn model_for_path<P>(&self, model_path: P) -> Result<Session>
fn build_for_path<P>(&self, model_path: P) -> Result<Session>
where
P: AsRef<std::path::Path>,
{
@@ -102,7 +102,7 @@ impl ModelBuilder for OrtModelBuilder {
Ok(Arc::new(Mutex::new(session))) // 这里的session需要包装下
}
/// 策略 B从内存字节流加载模型配合 include_bytes! 使用)
fn model_from_bytes(&self, model_bytes: &[u8]) -> Result<Session> {
fn build_from_bytes(&self, model_bytes: &[u8]) -> Result<Session> {
let mut builder = self.create_session_builder()?;
let session = builder

View File

@@ -1,70 +1,69 @@
use crate::types::Session;
use ddddocr_core::ModelMetadata;
use ddddocr_core::OcrOutput;
use ddddocr_core::error::{DdddError, Result, TensorError};
use ddddocr_core::traits::{InferenceEngine, Info, OcrEngine};
use ddddocr_core::types::{AxisDim, ModelInfo, TensorInfo, TensorType};
use ddddocr_core::utils::normalize_ocr_logits;
use ddddocr_core::{InferenceEngine, OcrEngine, OcrOutput};
use ort::inputs;
use ort::value::{TensorElementType, TensorRef};
use std::sync::Mutex;
// 引入核心层的统一错误类型
/// 明确命名为 AxisDim代表模型某一个轴的维度特征
#[derive(Clone, PartialEq, Eq)]
pub enum AxisDim {
/// 静态固定维度(如通道数固定为 1高度固定为 64
Static(usize),
/// 动态符号维度(如宽度是动态的 "image_width"
Dynamic(String),
}
// #[derive(Clone, PartialEq, Eq)]
// pub enum AxisDim {
// /// 静态固定维度(如通道数固定为 1高度固定为 64
// Static(usize),
// /// 动态符号维度(如宽度是动态的 "image_width"
// Dynamic(String),
// }
impl AxisDim {
/// 便捷方法:判断是否为动态维度
pub fn is_dynamic(&self) -> bool {
matches!(self, AxisDim::Dynamic(_))
}
}
/// 自定义 Debug 格式化输出,彻底融化套娃外壳,保证日志干净漂亮
impl std::fmt::Debug for AxisDim {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AxisDim::Static(size) => write!(f, "{}", size),
AxisDim::Dynamic(expr) => write!(f, "Dynamic(\"{}\")", expr),
}
}
}
// impl AxisDim {
// /// 便捷方法:判断是否为动态维度
// pub fn is_dynamic(&self) -> bool {
// matches!(self, AxisDim::Dynamic(_))
// }
// }
// /// 自定义 Debug 格式化输出,彻底融化套娃外壳,保证日志干净漂亮
// impl std::fmt::Debug for AxisDim {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// match self {
// AxisDim::Static(size) => write!(f, "{}", size),
// AxisDim::Dynamic(expr) => write!(f, "Dynamic(\"{}\")", expr),
// }
// }
// }
/// 模拟 Python 的 input_info 和 output_info 结构
#[derive(Debug, Clone)]
pub struct TensorInfo {
pub name: String,
pub shape: Vec<AxisDim>, // 既包含 Fixed 静态维度,也包含 Dynamic 动态符号
pub data_type: TensorElementType, // 对应 Python 的 type
}
/// 最终返回的模型完整信息
#[derive(Debug, Clone)]
pub struct ModelInfo {
pub inputs: Vec<TensorInfo>,
pub outputs: Vec<TensorInfo>,
/// 硬件执行提供者(采用 Option 兼容不同底层的推理引擎)
pub providers: Option<Vec<String>>,
}
pub struct OcrSession {
// #[derive(Debug, Clone)]
// pub struct TensorInfo {
// pub name: String,
// pub shape: Vec<AxisDim>, // 既包含 Fixed 静态维度,也包含 Dynamic 动态符号
// pub data_type: TensorElementType, // 对应 Python 的 type
// }
//
// /// 最终返回的模型完整信息
// #[derive(Debug, Clone)]
// pub struct ModelInfo {
// pub inputs: Vec<TensorInfo>,
// pub outputs: Vec<TensorInfo>,
// /// 硬件执行提供者(采用 Option 兼容不同底层的推理引擎)
// pub providers: Option<Vec<String>>,
// }
pub struct OcrRuntime {
pub session: Session,
pub model_metadata: ModelMetadata,
pub metadata: ModelMetadata,
}
impl OcrSession {
pub fn new(session: Session, model_metadata: ModelMetadata) -> Self {
Self {
session,
model_metadata,
}
impl OcrRuntime {
pub fn new(session: Session, metadata: ModelMetadata) -> Self {
Self { session, metadata }
}
}
impl OcrEngine for OcrSession {
impl OcrEngine for OcrRuntime {
fn metadata(&self) -> &ModelMetadata {
&self.model_metadata
&self.metadata
}
}
impl InferenceEngine for OcrSession {
impl InferenceEngine for OcrRuntime {
type Output = OcrOutput;
/// 对应 Python 的 _inference
fn inference(&self, input_array: ndarray::Array4<f32>) -> Result<Self::Output, TensorError> {
@@ -76,18 +75,6 @@ impl InferenceEngine for OcrSession {
.lock()
.map_err(|_| TensorError::Engine("获取 Session 锁失败 (Poisoned)".to_string()))?;
// // 2. 获取输入节点名称
// let input_name = session_guard
// .inputs()
// .first()
// .map(|i| i.name())
// .unwrap_or("input");
//
// // 3. 在 session_guard (&mut Session) 上调用 run
// let outputs = session_guard
// .run(inputs![TensorRef::from_array_view(&input_array).map_err(|e| TensorError::Engine(format!("构建输入失败: {e}")))? )
// .map_err(|e| TensorError::Engine(format!("执行模型推理失败: {e}")))?;
let result = session_guard
.run(inputs![TensorRef::from_array_view(&input_array).map_err(
|e| TensorError::Engine(format!("构建输入失败: {e}"))
@@ -97,10 +84,7 @@ impl InferenceEngine for OcrSession {
println!("模型输出原始数据: {:?}", result);
// Ok(result.swap_remove(0).into_tensor())
let raw_value = &result[0];
// let dtype = raw_tensor
// .dtype();
// .map_err(|e| TensorError::Engine(format!("无法读取输出数据类型: {e}")))?;
// 在引擎内部消化掉 DatumType 强耦合
match raw_value.dtype().tensor_type().unwrap() {
TensorElementType::Int64 => {
let (array_d, slice) = raw_value
@@ -108,7 +92,7 @@ impl InferenceEngine for OcrSession {
.map_err(|_| TensorError::Engine("Tract 无法获取 i64 内存视图".to_string()))?;
// .context("Tract 无法获取 i64 内存视图")?;
// 🌟 提前提取真实维度
// 提前提取真实维度
let actual_shape = array_d
.to_vec()
.iter()
@@ -151,75 +135,16 @@ impl InferenceEngine for OcrSession {
}
}
}
// impl OcrSession {
// /// 获取模型输入的节点信息列表
// pub fn input_info(&self) -> Result<Vec<TensorInfo>> {
// let model = self.session.model();
// let outlets = model.input_outlets().map_err(DdddError::new)?;
// self.resolve_tensors(model, outlets)
// }
//
// /// 获取模型输出的节点信息列表
// pub fn output_info(&self) -> Result<Vec<TensorInfo>> {
// let model = self.session.model();
// let outlets = model.output_outlets().map_err(DdddError::new)?;
// self.resolve_tensors(model, outlets)
// }
//
// /// 获取模型详细元数据信息(对标 Python ddddocr 的 get_model_info
// /// 完美包容 [1, 1, 64, image_width] 这样的变长图像模型
// /// 获取模型详细元数据信息(代码更紧凑、优雅)
// pub fn model_info(&self) -> Result<ModelInfo> {
// Ok(ModelInfo {
// inputs: self.input_info()?,
// outputs: self.output_info()?,
// providers: None,
// })
// }
//
// /// 提取出来的公共转换逻辑:将一组 OutletId 解析为 TensorInfo 列表
// fn resolve_tensors(&self, model: &TypedModel, outlets: &[OutletId]) -> Result<Vec<TensorInfo>> {
// outlets
// .iter()
// .map(|&outlet_id| {
// 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();
//
// Ok(TensorInfo {
// name: node_name,
// shape,
// data_type: fact.datum_type,
// })
// })
// .collect() // 函数式声明:自动传播第一处发生的错误
// }
//
// /// 安全还原 Tract 维度至 Vec<AxisDim>
// fn resolve_shape(&self, shape_fact: &ShapeFact) -> Vec<AxisDim> {
// let tract_shape = shape_fact.to_tvec();
//
// let resolved = tract_shape
// .iter()
// .map(|dim| {
// // 防御性编程:必须同时满足能够转换为 i64 且 大于等于 0
// if let Ok(size) = dim.to_i64() {
// if size >= 0 {
// AxisDim::Static(size as usize)
// } else {
// // 如果 ONNX 导出时某些动态维度被标记为了 -1安全地作为动态符号捕获
// AxisDim::Dynamic(dim.to_string())
// }
// } else {
// AxisDim::Dynamic(dim.to_string())
// }
// })
// .collect();
//
// resolved
// }
// }
impl Info for OcrRuntime {
fn input_info(&self) -> Result<Vec<TensorInfo>> {
todo!()
}
fn output_info(&self) -> Result<Vec<TensorInfo>> {
todo!()
}
fn model_info(&self) -> Result<ModelInfo> {
todo!()
}
}

View File

@@ -1,8 +1,9 @@
use anyhow::Context;
use ddddocr_core::{DetectionResult, ModelBuilder};
use ddddocr_core::{DetectionResult, Ocr};
use ddddocr_core::traits::Loader;
use ddddocr_core::{Detector, ModelMetadata, Normalization, Slider};
// 假设你的包名是这个
use ddddocr_ort::{DetSession, OcrBuilder, OcrSession};
use ddddocr_ort::{DetRuntime, OcrBuilder, OcrRuntime};
use image::{DynamicImage, ImageBuffer, Luma, Rgb};
use std::fs;
use std::path::Path;
@@ -11,7 +12,7 @@ mod char_slice;
use char_slice::CHARSET_BETA;
use ddddocr_core::Resize;
use ddddocr_ort::loader::OrtModelLoader;
use ddddocr_ort::loader::ModelLoader as OrtModelLoader;
fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
// 1. 先将泛型转为具体的 &Path 引用
@@ -104,8 +105,9 @@ fn save_rust_result(result: &ImageBuffer<Luma<f32>, Vec<f32>>, filename: &str) {
}
#[test]
fn test_full_classification() {
let model = OrtModelLoader::builder().use_gpu(true)
.model_for_path("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx")
let model = OrtModelLoader::default().use_gpu(false)
.build_for_path("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx")
// .build_for_path("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_old.onnx")
.expect("模型加载失败");
let metadata = ModelMetadata::from_static_slice(
CHARSET_BETA,
@@ -115,7 +117,7 @@ fn test_full_classification() {
Normalization::MinusOneToOne,
);
// 1. 初始化模型
let ocr = OcrSession::new(model, metadata);
let ocr = OcrRuntime::new(model, metadata);
// 2. 加载测试图片
let img =
image::open("D:/CNWei/CNW/Rust/ddddocr-rs/samples/code2.png").expect("测试图片不存在");
@@ -125,21 +127,24 @@ fn test_full_classification() {
// .predict(&img)
// .expect("识别过程出错")
// .into_text();
let result = OcrBuilder::new()
.build(&ocr)
.predict(&img)
.expect("识别过程出错")
.into_text();
// let result = OcrBuilder::new()
// .build(&ocr)
// .predict(&img)
// .expect("识别过程出错")
// .into_text();
let res=Ocr::builder().runner(&ocr).predict(&img).expect("s").into_text();
println!("识别结果: {}", result);
assert!(!result.is_empty());
// println!("识别结果: {}", result);
println!("识别结果: {}", res);
// assert!(!result.is_empty());
assert!(!res.is_empty());
}
#[test]
fn test_det_load() -> anyhow::Result<()> {
let det_model = OrtModelLoader::builder()
.model_for_path("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_det.onnx")
let det_model = OrtModelLoader::default()
.build_for_path("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_det.onnx")
.expect("模型加载失败");
let det = DetSession::new(det_model);
let det = DetRuntime::new(det_model);
let image_path = "D:/CNWei/CNW/Rust/ddddocr-rs/samples/det1.png";
let image_bytes =
fs::read(image_path).map_err(|e| anyhow::anyhow!("无法读取图片 {}: {}", image_path, e))?;
@@ -239,8 +244,8 @@ fn test_real_slide_comparison() {
#[test]
fn test_resolve_shape_logic_direct() {
// 创建一个哑 ModelLoader 实例session 用不上,因为我们直接测私有方法)
let loader = OrtModelLoader::builder()
.model_for_path(
let loader = OrtModelLoader::default()
.build_for_path(
// "D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",
"D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_huashi666_i64.onnx",
)