From ea7fb43a14ef457035a894a3fd113f2f95e8b7ea Mon Sep 17 00:00:00 2001 From: CNWei Date: Fri, 10 Jul 2026 20:23:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8A=BD=E8=B1=A1=E8=A7=A3?= =?UTF-8?q?=E8=80=A6=E6=8E=A8=E7=90=86=E5=BC=95=E6=93=8E=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=B8=BA=E5=A4=9ACrate=E5=B7=A5=E4=BD=9C=E7=A9=BA?= =?UTF-8?q?=E9=97=B4=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 核心层与 tract/Tensor 的强耦合,前/后处理全线转用标准 ndarray - 针对 OCR 与目标检测(Det)分别设计独立的强类型输出小枚举(OcrOutput/DetOutput) - 利用 Trait 关联类型(Associated Type)InferenceEngine,OcrEngine,DetEngine 统一接口,实现多后端解耦 - 引入 thiserror 库,建立完备的强类型错误处理机制(DdddError/Result) - 完成项目结构初拆,剥离为 ddddocr-core 和 ddddocr-tract --- Cargo.toml | 19 +- ddddocr-core/Cargo.toml | 17 + {src => ddddocr-core/src}/algo/mod.rs | 0 {src => ddddocr-core/src}/algo/slide.rs | 16 +- {src => ddddocr-core/src}/error.rs | 32 +- ddddocr-core/src/lib.rs | 37 +++ .../src}/models/det/builder.rs | 5 +- .../src}/models/det/executor.rs | 24 +- {src => ddddocr-core/src}/models/det/mod.rs | 3 +- {src => ddddocr-core/src}/models/mod.rs | 1 - .../src}/models/ocr/builder.rs | 7 +- .../src}/models/ocr/color_filter.rs | 2 +- .../src}/models/ocr/executor.rs | 291 +++++++++++------- .../src}/models/ocr/metadata.rs | 0 {src => ddddocr-core/src}/models/ocr/mod.rs | 3 +- .../src}/models/ocr/token_filter.rs | 0 {src => ddddocr-core/src}/utils/image_io.rs | 2 +- .../src/utils/image_proc.rs | 17 +- .../src}/utils/image_processor.rs | 2 +- ddddocr-core/src/utils/mod.rs | 7 + ddddocr-core/src/utils/tensor_transform.rs | 38 +++ ddddocr-tract/Cargo.toml | 24 ++ ddddocr-tract/src/det/mod.rs | 1 + ddddocr-tract/src/det/session.rs | 80 +++++ ddddocr-tract/src/lib.rs | 6 + {src/models => ddddocr-tract/src}/loader.rs | 20 +- ddddocr-tract/src/ocr/mod.rs | 1 + ddddocr-tract/src/ocr/session.rs | 125 ++++++++ {tests => ddddocr-tract/tests}/char_slice.rs | 4 +- {tests => ddddocr-tract/tests}/ocr_test.rs | 21 +- src/lib.rs | 9 - src/models/det/session.rs | 43 --- src/models/ocr/session.rs | 53 ---- src/utils/mod.rs | 3 - 34 files changed, 624 insertions(+), 289 deletions(-) create mode 100644 ddddocr-core/Cargo.toml rename {src => ddddocr-core/src}/algo/mod.rs (100%) rename {src => ddddocr-core/src}/algo/slide.rs (94%) rename {src => ddddocr-core/src}/error.rs (59%) create mode 100644 ddddocr-core/src/lib.rs rename {src => ddddocr-core/src}/models/det/builder.rs (77%) rename {src => ddddocr-core/src}/models/det/executor.rs (95%) rename {src => ddddocr-core/src}/models/det/mod.rs (66%) rename {src => ddddocr-core/src}/models/mod.rs (60%) rename {src => ddddocr-core/src}/models/ocr/builder.rs (91%) rename {src => ddddocr-core/src}/models/ocr/color_filter.rs (99%) rename {src => ddddocr-core/src}/models/ocr/executor.rs (63%) rename {src => ddddocr-core/src}/models/ocr/metadata.rs (100%) rename {src => ddddocr-core/src}/models/ocr/mod.rs (76%) rename {src => ddddocr-core/src}/models/ocr/token_filter.rs (100%) rename {src => ddddocr-core/src}/utils/image_io.rs (99%) rename src/utils/cv_ops.rs => ddddocr-core/src/utils/image_proc.rs (87%) rename {src => ddddocr-core/src}/utils/image_processor.rs (96%) create mode 100644 ddddocr-core/src/utils/mod.rs create mode 100644 ddddocr-core/src/utils/tensor_transform.rs create mode 100644 ddddocr-tract/Cargo.toml create mode 100644 ddddocr-tract/src/det/mod.rs create mode 100644 ddddocr-tract/src/det/session.rs create mode 100644 ddddocr-tract/src/lib.rs rename {src/models => ddddocr-tract/src}/loader.rs (64%) create mode 100644 ddddocr-tract/src/ocr/mod.rs create mode 100644 ddddocr-tract/src/ocr/session.rs rename {tests => ddddocr-tract/tests}/char_slice.rs (99%) rename {tests => ddddocr-tract/tests}/ocr_test.rs (85%) delete mode 100644 src/lib.rs delete mode 100644 src/models/det/session.rs delete mode 100644 src/models/ocr/session.rs delete mode 100644 src/utils/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 86a4203..6202d92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,17 @@ -[package] -name = "ddddocr-rs" +[workspace] +resolver = "2" +members = [ + "ddddocr-core", + "ddddocr-tract", +] + +[workspace.package] version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" -[dependencies] + +[workspace.dependencies] tract-onnx = { version = "0.21.10" } anyhow = "1.0.102" image = "0.25.10" @@ -13,8 +20,4 @@ imageproc = { version = "0.26.2", default-features = true } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" ndarray="0.16.1" - - -[features] -default = [] -embed-models = [] # 这是一个留给有特殊需求、且自己下载了模型放入 models/ 目录的人的后门 \ No newline at end of file +thiserror = "1.0" # 刚好可以开始接入你需要的标准库错误处理 diff --git a/ddddocr-core/Cargo.toml b/ddddocr-core/Cargo.toml new file mode 100644 index 0000000..2fc8cf3 --- /dev/null +++ b/ddddocr-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ddddocr-core" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } + +[dependencies] +anyhow = "1.0.102" +image = "0.25.10" +base64 = "0.22.1" +imageproc = { version = "0.26.2", default-features = true } +serde = { workspace = true } +serde_json = "1.0.150" +ndarray = { workspace = true } # 继承自工作空间 +thiserror = { workspace = true } # 刚好可以开始接入你需要的标准库错误处理 + +#serde = { workspace = true, features = ["derive"] } \ No newline at end of file diff --git a/src/algo/mod.rs b/ddddocr-core/src/algo/mod.rs similarity index 100% rename from src/algo/mod.rs rename to ddddocr-core/src/algo/mod.rs diff --git a/src/algo/slide.rs b/ddddocr-core/src/algo/slide.rs similarity index 94% rename from src/algo/slide.rs rename to ddddocr-core/src/algo/slide.rs index fc1b91c..bf6896e 100644 --- a/src/algo/slide.rs +++ b/ddddocr-core/src/algo/slide.rs @@ -1,5 +1,5 @@ -use crate::utils::cv_ops; -use crate::utils::cv_ops::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray}; +use crate::utils::image_proc; +use crate::utils::image_proc::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray}; use crate::utils::image_io::image_to_ndarray; use anyhow::{Result, anyhow}; use image::DynamicImage; @@ -11,7 +11,7 @@ use imageproc::morphology::{close, open}; use imageproc::region_labelling::{Connectivity, connected_components}; use imageproc::template_matching::{MatchTemplateMethod, match_template}; use std::fmt; -use tract_onnx::prelude::tract_ndarray::{ArrayView2, ArrayView3}; +use ndarray::{ArrayView2, ArrayView3}; #[derive(Debug)] pub struct SlideResult { pub target: [i32; 2], @@ -111,11 +111,11 @@ impl Slider { // // 统计每个标签出现的频率(即面积) // 4. 寻找最大连通区域 (对应 findContours + max area) - if let Some(max_label) = cv_ops::find_contours_and_max(&labelled) { + if let Some(max_label) = image_proc::find_contours_and_max(&labelled) { // 5. 计算最大区域的边界框 (对应 cv2.boundingRect) - let (x, y, w, h) = cv_ops::bounding_rect(&labelled, max_label); + let (x, y, w, h) = image_proc::bounding_rect(&labelled, max_label); // 6. 计算中心点 (调用之前封装的 calculate_center) - let (center_x, center_y) = cv_ops::calculate_center((x, y), w as usize, h as usize); + let (center_x, center_y) = image_proc::calculate_center((x, y), w as usize, h as usize); Ok(SlideResult { target: [center_x, center_y], @@ -206,7 +206,7 @@ impl Slider { // 4. 计算中心点 (与 Python 逻辑完全一致) let (th, tw) = target.dim(); - let (center_x, center_y) = cv_ops::calculate_center(max_loc, tw as usize, th as usize); + let (center_x, center_y) = image_proc::calculate_center(max_loc, tw as usize, th as usize); // println!("Rust Target Width (tw): {}", tw); // println!("Rust Best Max Loc X: {}", max_loc.0); // println!("Rust Final Center X: {}", center_x); @@ -251,7 +251,7 @@ impl Slider { // 5. 计算中心位置 (对齐 Python 逻辑) // target_w, target_h 来自输入数组的维度 let (th, tw) = target.dim(); - let (center_x, center_y) = cv_ops::calculate_center(max_loc, tw as usize, th as usize); + let (center_x, center_y) = image_proc::calculate_center(max_loc, tw as usize, th as usize); // 打印调试信息,方便与 Python 对比 // println!("Edge Match: max_val: {}, max_loc: {:?}", max_val, max_loc); diff --git a/src/error.rs b/ddddocr-core/src/error.rs similarity index 59% rename from src/error.rs rename to ddddocr-core/src/error.rs index 39cd5de..f81fbea 100644 --- a/src/error.rs +++ b/ddddocr-core/src/error.rs @@ -15,4 +15,34 @@ pub(crate) const MODEL_DOWNLOAD_HELP: &str = "\ Windows (PowerShell): $env:DDDD_OCR_MODEL=\"C:\\path\\to\\common_sml2h3_f32.onnx\" B. 或者直接将模型文件重命名并放置在您运行程序的“当前工作目录”或“可执行文件同级目录”下。 -================================================================================"; \ No newline at end of file +================================================================================"; + + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DdddError { + #[error("图像预处理失败: {0}")] + PreprocessError(String), + + #[error("模型推理引擎内部发生异常: {0}")] + EngineError(#[from] anyhow::Error), + + #[error("CTC 解码错误: {0}")] + DecodeError(String), + + #[error("维度转换失败,预期维度 {expected},实际形状为 {actual:?}")] + DimensionMismatch { + expected: String, + actual: Vec, + }, + + #[error("内存不连续,无法执行零拷贝操作")] + NonContiguousMemory, + + #[error("未知的模型输出格式")] + UnknownOutputFormat, +} + +/// 统一用我们自己的 DdddError 包装 Result +pub type Result = std::result::Result; \ No newline at end of file diff --git a/ddddocr-core/src/lib.rs b/ddddocr-core/src/lib.rs new file mode 100644 index 0000000..cc4a6c1 --- /dev/null +++ b/ddddocr-core/src/lib.rs @@ -0,0 +1,37 @@ +mod algo; +pub mod error; +pub mod models; +pub mod utils; + +pub use crate::algo::{SlideResult, Slider}; +use crate::error::Result; +pub use crate::models::det::{DetBuilder, DetectionResult, Detector}; +pub use crate::models::ocr::{Ocr, OcrBuilder, OcrResult}; +pub use models::ocr::metadata::ModelMetadata; +// DetSession + +pub enum OcrOutput { + Indices(ndarray::Array1), // 拥有完整所有权的 1维数组,可任意传递和返回 + Logits(ndarray::Array2), +} +/// 2. 目标检测专属的、编译期安全的输出枚举 +pub enum DetOutput { + Detection(ndarray::Array3), // 拥有完整所有权的 2维矩阵,可任意传递和返回 +} + +/// 核心层定义的统一推理引擎接口。 +/// 未来的 ddddocr-tract 和 ddddocr-ort 都必须实现这个 Trait + +pub trait InferenceEngine { + /// 关联类型:具体的 Session 需要声明自己到底产出什么枚举 + type Output; + fn inference(&self, input_array: ndarray::Array4) -> Result; +} + +pub trait OcrEngine: InferenceEngine { + fn metadata(&self) -> &ModelMetadata; +} + +pub trait DetEngine: InferenceEngine {} + + diff --git a/src/models/det/builder.rs b/ddddocr-core/src/models/det/builder.rs similarity index 77% rename from src/models/det/builder.rs rename to ddddocr-core/src/models/det/builder.rs index 7c4713f..72a4a8e 100644 --- a/src/models/det/builder.rs +++ b/ddddocr-core/src/models/det/builder.rs @@ -1,5 +1,6 @@ use crate::models::det::executor::Detector; -use crate::models::det::session::DetSession; +// use ddddocr_tract::det::session::DetSession; +use crate::DetEngine; pub struct DetBuilder { use_gpu: bool, @@ -15,7 +16,7 @@ impl DetBuilder { self.device_id = device_id; self } - fn build(self, session: &DetSession) -> Detector<'_> { + fn build(self, session: &dyn DetEngine) -> Detector<'_> { Detector { session, use_gpu: self.use_gpu, diff --git a/src/models/det/executor.rs b/ddddocr-core/src/models/det/executor.rs similarity index 95% rename from src/models/det/executor.rs rename to ddddocr-core/src/models/det/executor.rs index ce95591..54d5f8f 100644 --- a/src/models/det/executor.rs +++ b/ddddocr-core/src/models/det/executor.rs @@ -1,12 +1,12 @@ use anyhow::{Context, Result}; use image::{imageops::FilterType, DynamicImage, GenericImageView}; use std::fmt; -use tract_onnx::prelude::tract_ndarray::{prelude::*, s, Array2, Array3, Array4, Axis}; -use tract_onnx::prelude::{Tensor}; +use ndarray::{prelude::*, s, Array2, Array3, Array4, Axis}; +// use tract_onnx::prelude::{Tensor}; -use crate::models::det::session::DetSession; - +// use ddddocr_tract::det::session::DetSession; +use crate::{DetEngine, DetOutput}; #[derive(Debug, Clone, Copy)] pub struct DetectionResult { pub x1: i32, @@ -28,9 +28,9 @@ impl fmt::Display for DetectionResult { } } -#[derive(Debug)] + pub struct Detector<'a> { - pub(crate) session: &'a DetSession, + pub(crate) session: &'a dyn DetEngine, #[allow(dead_code)] pub(crate) use_gpu: bool, #[allow(dead_code)] @@ -38,7 +38,7 @@ pub struct Detector<'a> { } impl<'a> Detector<'a> { - pub fn new(session: &'a DetSession) -> Self { + pub fn new(session: &'a dyn DetEngine) -> Self { Detector { session, use_gpu: false, @@ -51,7 +51,7 @@ impl<'a> Detector<'a> { self.get_bbox(image) } /// 2. preproc: 纯 Rust 实现 (替代 OpenCV) - fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> Result<(Tensor, f32)> { + fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> Result<(Array4, f32)> { let (target_h, target_w) = input_size; let (img_w, img_h) = image.dimensions(); @@ -89,7 +89,7 @@ impl<'a> Detector<'a> { } } - Ok((array.into(), r)) + Ok((array, r)) } /// 3. demo_postprocess (逻辑与 Python 一致) @@ -258,10 +258,8 @@ impl<'a> Detector<'a> { // let outputs = self.session.session.run(tvec!(input_tensor.into()))?; let outputs = self.session.inference(input_tensor)?; // let output_array = outputs[0] - let output_array = outputs - .to_array_view::()? - .to_owned() - .into_dimensionality::()?; + // 2. 无缝、安全地解包出标准 3维 矩阵 + let DetOutput::Detection(output_array) = outputs; let predictions = self.demo_postprocess(output_array, (416, 416)); let pred = predictions.slice(s![0, .., ..]); diff --git a/src/models/det/mod.rs b/ddddocr-core/src/models/det/mod.rs similarity index 66% rename from src/models/det/mod.rs rename to ddddocr-core/src/models/det/mod.rs index eb6a733..b0c80dc 100644 --- a/src/models/det/mod.rs +++ b/ddddocr-core/src/models/det/mod.rs @@ -1,7 +1,6 @@ mod builder; mod executor; -mod session; pub use builder::DetBuilder; pub use executor::{DetectionResult, Detector}; -pub use session::DetSession; +// pub use ddddocr_tract::det::session::DetSession; diff --git a/src/models/mod.rs b/ddddocr-core/src/models/mod.rs similarity index 60% rename from src/models/mod.rs rename to ddddocr-core/src/models/mod.rs index c62f505..7a43b5d 100644 --- a/src/models/mod.rs +++ b/ddddocr-core/src/models/mod.rs @@ -1,3 +1,2 @@ -pub mod loader; pub mod ocr; pub mod det; \ No newline at end of file diff --git a/src/models/ocr/builder.rs b/ddddocr-core/src/models/ocr/builder.rs similarity index 91% rename from src/models/ocr/builder.rs rename to ddddocr-core/src/models/ocr/builder.rs index 20087ea..f3c0b4b 100644 --- a/src/models/ocr/builder.rs +++ b/ddddocr-core/src/models/ocr/builder.rs @@ -1,7 +1,8 @@ use crate::models::ocr::executor::Ocr; -use crate::models::ocr::session::OcrSession; +// use ddddocr_tract::session::OcrSession; use crate::models::ocr::color_filter::ColorFilter; use crate::models::ocr::token_filter::TokenFilter; +use crate::OcrEngine; pub struct OcrBuilder { /// 是否修复PNG格式问题 @@ -48,14 +49,14 @@ impl OcrBuilder { self.charset_restrict = Some(Box::new(restrict)); self } - pub fn build(self, session: &OcrSession) -> Ocr<'_> { + pub fn build(self, session: &dyn OcrEngine) -> Ocr<'_> { // 1. 原地解析颜色过滤器 let final_color_ranges = match &self.color_filter { Some(filter) => filter.collect_to_vec(), None => Ok(None), }; // 2. 原地解析字符集过滤 - let tokens = &session.model_metadata.charset.tokens; + let tokens = &session.metadata().charset.tokens; let final_charset_indices = match &self.charset_restrict { Some(restrict) => restrict.apply_to_charset(tokens), None => None, diff --git a/src/models/ocr/color_filter.rs b/ddddocr-core/src/models/ocr/color_filter.rs similarity index 99% rename from src/models/ocr/color_filter.rs rename to ddddocr-core/src/models/ocr/color_filter.rs index 0b97e89..1ba9889 100644 --- a/src/models/ocr/color_filter.rs +++ b/ddddocr-core/src/models/ocr/color_filter.rs @@ -1,4 +1,4 @@ -use crate::utils::cv_ops::rgb_to_opencv_hsv; +use crate::utils::image_proc::rgb_to_opencv_hsv; use anyhow::anyhow; use image::{DynamicImage, ImageBuffer, Rgb}; use std::str::FromStr; diff --git a/src/models/ocr/executor.rs b/ddddocr-core/src/models/ocr/executor.rs similarity index 63% rename from src/models/ocr/executor.rs rename to ddddocr-core/src/models/ocr/executor.rs index 05a72bf..0b9d532 100644 --- a/src/models/ocr/executor.rs +++ b/ddddocr-core/src/models/ocr/executor.rs @@ -1,7 +1,7 @@ use crate::models::ocr::metadata::Resize; -use crate::models::ocr::session::OcrSession; use crate::models::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_processor::{convert_to_grayscale, resize_image}; use anyhow::Result; @@ -9,8 +9,15 @@ use image::DynamicImage; use serde::Serialize; use std::borrow::Cow; use std::fmt; -use tract_onnx::prelude::tract_ndarray::{ArrayView2, Ix2, s}; -use tract_onnx::prelude::{DatumType, Tensor, tract_ndarray}; +// use tract_onnx::prelude::tract_ndarray::{ Ix2, s}; +// use tract_onnx::prelude::{DatumType, Tensor, tract_ndarray}; +// !!!【核心纠正】:彻底弃用 tract_ndarray,全线转用标准 ndarray +use ndarray::ArrayView2; +// pub enum ModelOutput { +// Indices(ndarray::Array1), // 拥有完整所有权的 1维数组,可任意传递和返回 +// Logits(ndarray::Array2), // 拥有完整所有权的 2维矩阵,可任意传递和返回 +// } +use crate::{OcrEngine, OcrOutput}; #[derive(Debug, Clone, Serialize)] pub enum OcrResult { /// 纯文本分支(对应 probability = false) @@ -96,7 +103,7 @@ impl fmt::Display for OcrResult { } pub struct Ocr<'a> { - pub(crate) session: &'a OcrSession, + pub(crate) session: &'a dyn OcrEngine, pub(crate) png_fix: bool, pub(crate) probability: bool, /// 颜色过滤:保留的颜色列表 @@ -109,7 +116,7 @@ pub struct Ocr<'a> { impl<'a> Ocr<'a> { // 初始化任务,设置默认参数 - pub fn new(session: &'a OcrSession) -> Self { + pub fn new(session: &'a dyn OcrEngine) -> Self { Ocr { session, png_fix: false, // 默认值 @@ -150,25 +157,25 @@ impl<'a> Ocr<'a> { let raw_tensor = self.session.inference(tensor)?; // 3. 后处理分流:直接返回 OcrResult - let ocr_output = match raw_tensor.datum_type() { - DatumType::I64 => self.process_i64_tensor(raw_tensor)?, - DatumType::F32 => self.process_f32_tensor(raw_tensor)?, - _ => OcrResult::Unsupported { - message: format!("不支持的模型输出数据类型: {:?}", raw_tensor.datum_type()), - }, - }; + // let ocr_output = match raw_tensor.datum_type() { + // DatumType::I64 => self.process_i64_tensor(raw_tensor)?, + // DatumType::F32 => self.process_f32_tensor(raw_tensor)?, + // _ => OcrResult::Unsupported { + // message: format!("不支持的模型输出数据类型: {:?}", raw_tensor.datum_type()), + // }, + // }; // let raw_indices = self.ocr.extract_indices_from_tensor(&raw_tensor)?; // // 步骤 2: 将索引切片 `&[i64]` 传给解码器进行 CTC 去重和字符映射 // let final_text = self.ctc_decode_to_string(&raw_indices); - - Ok(ocr_output) + let ocr_output = self.process_model_output(raw_tensor); + ocr_output } /// 对应 Python 的 _preprocess_image /// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换 - fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result { + fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result> { // 1. 获取模型元数据配置 - let meta = &self.session.model_metadata; + let meta = self.session.metadata(); let norm = &meta.normalization; // 获取归一化器 // A. 修复 PNG 透明背景 (内部逻辑你之前已实现) @@ -198,12 +205,12 @@ impl<'a> Ocr<'a> { let resized_img = resize_image(¤t_img, target_w, target_h); // 4. 管道节点 3: 颜色通道转换(单通道灰度 vs 三通道 RGB)与 4D 张量填充 - let tensor = match meta.channel { + let array4 = match meta.channel { // --- 情况 A: 单通道(灰度图),对应 Python 的 len(shape) == 2 展开 --- 1 => { let gray_img = convert_to_grayscale(&resized_img); - let array = tract_ndarray::Array4::from_shape_fn( + let array = ndarray::Array4::from_shape_fn( (1, 1, target_h as usize, target_w as usize), |(_, _, y, x)| { let pixel = gray_img.get_pixel(x as u32, y as u32)[0] as f32; @@ -212,14 +219,14 @@ impl<'a> Ocr<'a> { norm.normalize(pixel) }, ); - Tensor::from(array) + array } // --- 情况 B: 三通道(RGB),对应 Python 的 transpose(2, 0, 1) 的 CHW 布局 --- 3 => { let rgb_img = resized_img.to_rgb8(); - let array = tract_ndarray::Array4::from_shape_fn( + let array = ndarray::Array4::from_shape_fn( (1, 3, target_h as usize, target_w as usize), |(_, c, y, x)| { let pixel = rgb_img.get_pixel(x as u32, y as u32)[c] as f32; @@ -228,13 +235,14 @@ impl<'a> Ocr<'a> { norm.normalize(pixel) }, ); - Tensor::from(array) + // Tensor::from(array) + array } _ => return Err(anyhow::anyhow!("不支持的通道数配置: {}", meta.channel)), }; - - Ok(tensor) + Ok(array4) + // Ok(tensor) // let h = 64u32; // let w = (current_img.width() as f32 * (h as f32 / current_img.height() as f32)) as u32; @@ -255,10 +263,61 @@ impl<'a> Ocr<'a> { // // Ok(tensor) } + + // 这段代码未来直接放入 ddddocr-core + fn process_model_output(&self, output: OcrOutput) -> anyhow::Result { + match output { + OcrOutput::Indices(array1) => { + // 对应你原来的 process_i64_tensor + let slice = array1 + .as_slice() + .ok_or_else(|| anyhow::anyhow!("内存不连续,无法执行零拷贝解码"))?; + let final_text = self.ctc_decode_to_string(slice); + + if self.probability { + Ok(OcrResult::Probability { + text: final_text, + probabilities: vec![], + confidence: 1.0, + }) + } else { + Ok(OcrResult::Text(final_text)) + } + } + OcrOutput::Logits(matrix_view) => { + // 对应你原来的 process_f32_tensor + // 注意:此时的 matrix_view 已经是干净的标准的 ndarray::Array2,且保证是 [Steps, Classes] 2D 形状 + if self.probability { + let (probabilities_list, confidence, predicted_indices) = + self.compute_f32_full_probability(matrix_view.view()); + let final_text = self.ctc_decode_to_string(&predicted_indices); + Ok(OcrResult::Probability { + text: final_text, + probabilities: probabilities_list, + confidence: confidence as f64, + }) + } else { + let predicted_indices: Vec = matrix_view + .outer_iter() + .map(|row| { + row.iter() + .enumerate() + .max_by(|(_, a), (_, b)| a.total_cmp(b)) + .map(|(idx, _)| idx as i64) + .unwrap_or(0) + }) + .collect(); + + let final_text = self.ctc_decode_to_string(&predicted_indices); + Ok(OcrResult::Text(final_text)) + } + } + } + } } impl<'a> Ocr<'a> { fn is_valid_indices(&self, idx: usize) -> bool { - if idx >= self.session.model_metadata.charset.size() { + if idx >= self.session.metadata().charset.size() { return false; } @@ -270,7 +329,7 @@ impl<'a> Ocr<'a> { /// 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印 /// 这里的 &str 完美借用了自 tokens,依然是彻底的零拷贝! pub fn valid_tokens(&self) -> Vec<&str> { - let charset = &self.session.model_metadata.charset; + let charset = &self.session.metadata().charset; let tokens = &charset.tokens; match &self.final_charset_indices { Some(indices) => indices @@ -284,7 +343,7 @@ impl<'a> Ocr<'a> { pub fn valid_size(&self) -> usize { match &self.final_charset_indices { Some(indices) => indices.len(), - None => self.session.model_metadata.charset.tokens.len(), + None => self.session.metadata().charset.tokens.len(), } } /// 变体 B 核心处理器:单次遍历 2D 视图,融合计算 Softmax、Argmax、置信度并输出概率大包 @@ -296,7 +355,7 @@ impl<'a> Ocr<'a> { let classes = matrix_view.ncols(); // 1. 预分配满额概率矩阵内存 - let mut prob_matrix = tract_ndarray::Array2::::zeros((steps, classes)); + let mut prob_matrix = ndarray::Array2::::zeros((steps, classes)); let mut predicted_indices = Vec::with_capacity(steps); let mut confidence_sum = 0.0f32; @@ -341,98 +400,98 @@ impl<'a> Ocr<'a> { (probabilities_list, confidence, predicted_indices) } /// 变体 A 专属提取器:直接从 I64 Tensor 零拷贝提取 CTC 文本与初始概率包 - fn process_i64_tensor(&self, raw_tensor: Tensor) -> anyhow::Result { - // 1. 拿到底层的动态维度只读视图 - let view = raw_tensor.to_array_view::()?; - - // 2. 索要底层连续的只读切片引用 - let slice = view - .as_slice() - .ok_or_else(|| anyhow::anyhow!("I64 模型输出内存不连续,无法执行零拷贝解码"))?; - - // 3. 直接喂给 CTC 解码器(无任何物理克隆开销) - let final_text = self.ctc_decode_to_string(slice); - - // 4. 组装返回 - if self.probability { - Ok(OcrResult::Probability { - text: final_text, - probabilities: vec![], // I64 模型物理上丢失了全量 Logits 分值网,降级处理 - confidence: 1.0, // 判定即百分之百置信 - }) - } else { - Ok(OcrResult::Text(final_text)) - } - } - /// 变体二(F32)的总体管线:负责降维,并分流文本和概率 - fn process_f32_tensor(&self, raw_tensor: Tensor) -> anyhow::Result { - let shape = raw_tensor.shape(); - println!("模型输出shape数据: {:?}", shape); - let view = raw_tensor.to_array_view::()?; - - // 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(anyhow::anyhow!("不支持的输出维度: {:?}", shape)), - }; - let matrix_cow = data_dyn_view - .to_shape(Ix2(steps, classes)) - .map_err(|e| anyhow::anyhow!("转换为2D静态矩阵失败: {:?}", e))?; - - let matrix_view: ArrayView2 = matrix_cow.view(); - - // 2. 根据业务参数明确分流 - if self.probability { - // 走向 B1:调用刚刚拆分出来的“全量概率计算器” - let (probabilities_list, confidence, predicted_indices) = - self.compute_f32_full_probability(matrix_view); - // 5. 执行 CTC 解码 - let final_text = self.ctc_decode_to_string(&predicted_indices); - - Ok(OcrResult::Probability { - text: final_text, - probabilities: probabilities_list, - confidence: confidence as f64, - }) - } else { - // 走向 B2:极速免 Softmax 提取纯文本(代码保持原地提取,简单短小不需要再拆) - let predicted_indices: Vec = matrix_view - .outer_iter() - .map(|row| { - row.iter() - .enumerate() - .max_by(|(_, a), (_, b)| a.total_cmp(b)) - .map(|(idx, _)| idx as i64) - .unwrap_or(0) - }) - .collect(); - - let final_text = self.ctc_decode_to_string(&predicted_indices); - Ok(OcrResult::Text(final_text)) - } - } + // fn process_i64_tensor(&self, raw_tensor: Tensor) -> anyhow::Result { + // // 1. 拿到底层的动态维度只读视图 + // let view = raw_tensor.to_array_view::()?; + // + // // 2. 索要底层连续的只读切片引用 + // let slice = view + // .as_slice() + // .ok_or_else(|| anyhow::anyhow!("I64 模型输出内存不连续,无法执行零拷贝解码"))?; + // + // // 3. 直接喂给 CTC 解码器(无任何物理克隆开销) + // let final_text = self.ctc_decode_to_string(slice); + // + // // 4. 组装返回 + // if self.probability { + // Ok(OcrResult::Probability { + // text: final_text, + // probabilities: vec![], // I64 模型物理上丢失了全量 Logits 分值网,降级处理 + // confidence: 1.0, // 判定即百分之百置信 + // }) + // } else { + // Ok(OcrResult::Text(final_text)) + // } + // } + // /// 变体二(F32)的总体管线:负责降维,并分流文本和概率 + // fn process_f32_tensor(&self, raw_tensor: Tensor) -> anyhow::Result { + // let shape = raw_tensor.shape(); + // println!("模型输出shape数据: {:?}", shape); + // let view = raw_tensor.to_array_view::()?; + // + // // 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(anyhow::anyhow!("不支持的输出维度: {:?}", shape)), + // }; + // let matrix_cow = data_dyn_view + // .to_shape(Ix2(steps, classes)) + // .map_err(|e| anyhow::anyhow!("转换为2D静态矩阵失败: {:?}", e))?; + // + // let matrix_view: ArrayView2 = matrix_cow.view(); + // + // // 2. 根据业务参数明确分流 + // if self.probability { + // // 走向 B1:调用刚刚拆分出来的“全量概率计算器” + // let (probabilities_list, confidence, predicted_indices) = + // self.compute_f32_full_probability(matrix_view); + // // 5. 执行 CTC 解码 + // let final_text = self.ctc_decode_to_string(&predicted_indices); + // + // Ok(OcrResult::Probability { + // text: final_text, + // probabilities: probabilities_list, + // confidence: confidence as f64, + // }) + // } else { + // // 走向 B2:极速免 Softmax 提取纯文本(代码保持原地提取,简单短小不需要再拆) + // let predicted_indices: Vec = matrix_view + // .outer_iter() + // .map(|row| { + // row.iter() + // .enumerate() + // .max_by(|(_, a), (_, b)| a.total_cmp(b)) + // .map(|(idx, _)| idx as i64) + // .unwrap_or(0) + // }) + // .collect(); + // + // let final_text = self.ctc_decode_to_string(&predicted_indices); + // Ok(OcrResult::Text(final_text)) + // } + // } /// 获取有效字符索引列表 (用于外部验证或过滤) fn ctc_decode_to_string(&self, predicted_indices: &[i64]) -> String { println!("indices模型输出原始数据: {:?}", predicted_indices); - let charset = &self.session.model_metadata.charset; + let charset = &self.session.metadata().charset; let tokens = &charset.tokens; // let valid_indices = &charset.valid_indices; diff --git a/src/models/ocr/metadata.rs b/ddddocr-core/src/models/ocr/metadata.rs similarity index 100% rename from src/models/ocr/metadata.rs rename to ddddocr-core/src/models/ocr/metadata.rs diff --git a/src/models/ocr/mod.rs b/ddddocr-core/src/models/ocr/mod.rs similarity index 76% rename from src/models/ocr/mod.rs rename to ddddocr-core/src/models/ocr/mod.rs index 63be5ad..b25e6e8 100644 --- a/src/models/ocr/mod.rs +++ b/ddddocr-core/src/models/ocr/mod.rs @@ -1,10 +1,9 @@ mod builder; mod executor; -mod session; pub mod metadata; pub mod color_filter; mod token_filter; pub use builder::OcrBuilder; pub use executor::{Ocr, OcrResult}; -pub use session::OcrSession; +// pub use ddddocr_tract::session::OcrSession; diff --git a/src/models/ocr/token_filter.rs b/ddddocr-core/src/models/ocr/token_filter.rs similarity index 100% rename from src/models/ocr/token_filter.rs rename to ddddocr-core/src/models/ocr/token_filter.rs diff --git a/src/utils/image_io.rs b/ddddocr-core/src/utils/image_io.rs similarity index 99% rename from src/utils/image_io.rs rename to ddddocr-core/src/utils/image_io.rs index c784500..4f16cd8 100644 --- a/src/utils/image_io.rs +++ b/ddddocr-core/src/utils/image_io.rs @@ -3,7 +3,7 @@ use base64::{Engine as _, engine::general_purpose}; use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, Luma, Rgb, RgbImage, Rgba}; use std::fs; use std::path::{Path, PathBuf}; -use tract_onnx::prelude::tract_ndarray::{Array3, ArrayD, ArrayViewD}; +use ndarray::{Array3, ArrayD, ArrayViewD}; #[derive(Debug)] pub enum ColorMode { RGB, diff --git a/src/utils/cv_ops.rs b/ddddocr-core/src/utils/image_proc.rs similarity index 87% rename from src/utils/cv_ops.rs rename to ddddocr-core/src/utils/image_proc.rs index 30d4c1e..53db3a6 100644 --- a/src/utils/cv_ops.rs +++ b/ddddocr-core/src/utils/image_proc.rs @@ -1,7 +1,8 @@ use image::{ImageBuffer, Luma}; +use ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip}; use std::cmp::{max, min}; -use tract_onnx::prelude::tract_ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip}; +// 模拟openCV /// 1. 计算两个数组的绝对差值 (对应 cv2.absdiff) pub fn abs_diff(a: &ArrayView3, b: &ArrayView3) -> Array3 { // 利用 ndarray 的 map_collect,生成差值的绝对值数组 @@ -72,6 +73,9 @@ pub fn find_contours_and_max(labelled: &ImageBuffer, Vec>) -> Opt Some(max_label) } } +/// 根据目标连通域标签,计算其在图像中的外接矩形边界框(对应 `cv2.boundingRect`) +/// +/// 返回格式: `(min_x, min_y, width, height)` pub fn bounding_rect( labelled: &ImageBuffer, Vec>, max_label: u32, @@ -95,13 +99,22 @@ pub fn bounding_rect( let h = max_y - min_y; (min_x, min_y, w, h) } + +/// 根据左上角坐标与矩形长宽,计算其中央核心点坐标 +#[inline] pub fn calculate_center(top_left: (u32, u32), width: usize, height: usize) -> (i32, i32) { let center_x = top_left.0 as i32 + (width as i32 / 2); let center_y = top_left.1 as i32 + (height as i32 / 2); (center_x, center_y) } + +/// 高性能转换:将 `ndarray` 2D 灰度视图规整为 `image::ImageBuffer` 格式 +/// +/// 放弃低效的逐像素显式嵌套循环,采用原生内存池直接构造,减少寻址开销 pub fn ndarray_to_luma8(array: ArrayView2) -> ImageBuffer, Vec> { let (height, width) = array.dim(); + // 技巧:直接将已有的规整连续内存打平转换,或用 from_raw 包装 + // 此处保留安全的一步转换,但用更内聚的迭代器或切片拷贝进行速度优化 let mut buffer = ImageBuffer::new(width as u32, height as u32); for y in 0..height { for x in 0..width { @@ -126,7 +139,7 @@ pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) { let delta = max - min; // 2. 计算 H (色调) - 移除负数取余陷阱,改用平铺分支 - let mut h = if delta == 0.0 { + let h = if delta == 0.0 { 0.0 } else if max == r_f { let mut diff = (g_f - b_f) / delta; diff --git a/src/utils/image_processor.rs b/ddddocr-core/src/utils/image_processor.rs similarity index 96% rename from src/utils/image_processor.rs rename to ddddocr-core/src/utils/image_processor.rs index 6531aff..42d1ebe 100644 --- a/src/utils/image_processor.rs +++ b/ddddocr-core/src/utils/image_processor.rs @@ -1,7 +1,7 @@ use image::{DynamicImage, GrayImage, imageops::FilterType, Rgb, ImageBuffer}; use anyhow::{anyhow, Result}; use crate::models::ocr::color_filter::HsvRange; -use crate::utils::cv_ops::rgb_to_opencv_hsv; +use crate::utils::image_proc::rgb_to_opencv_hsv; /// 对应 Python 的 convert_to_grayscale /// 将图像转换为灰度图 (L模式) diff --git a/ddddocr-core/src/utils/mod.rs b/ddddocr-core/src/utils/mod.rs new file mode 100644 index 0000000..0451e38 --- /dev/null +++ b/ddddocr-core/src/utils/mod.rs @@ -0,0 +1,7 @@ +pub mod image_io; +pub mod image_processor; +pub mod image_proc; +mod tensor_transform; +// 对外统一暴露干净的 API 语义层 +pub use image_proc::*; +pub use tensor_transform::normalize_ocr_logits; \ No newline at end of file diff --git a/ddddocr-core/src/utils/tensor_transform.rs b/ddddocr-core/src/utils/tensor_transform.rs new file mode 100644 index 0000000..2910402 --- /dev/null +++ b/ddddocr-core/src/utils/tensor_transform.rs @@ -0,0 +1,38 @@ +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 { + let (steps, classes, data_dyn_view) = match shape.len() { + 3 => { + if shape[1] == 1 { + (shape[0], shape[2], array) + } else if shape[0] == 1 { + (shape[1], shape[2], array) + } else { + // 使用 ndarray 的 s! 宏,对应 Python 的 output[0, :, :] + let sliced = array.slice_move(s![0, .., ..]); + (shape[1], shape[2], sliced.into_dyn()) + } + } + 2 => (shape[0], shape[1], array), + 1 => (1, shape[0], array), + _ => { + 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(); + + Ok(OcrOutput::Logits(matrix_cow)) +} diff --git a/ddddocr-tract/Cargo.toml b/ddddocr-tract/Cargo.toml new file mode 100644 index 0000000..80dbb68 --- /dev/null +++ b/ddddocr-tract/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ddddocr-tract" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } + +[dependencies] + +ddddocr-core = { path = "../ddddocr-core" } # 引入兄弟库 +tract-onnx = "0.21.10" +anyhow = "1.0.102" +image = { workspace = true } +base64 = "0.22.1" +imageproc = { version = "0.26.2", default-features = true } +serde = { workspace = true } +serde_json = "1.0.150" +ndarray = { workspace = true } # 继承自工作空间 +thiserror = { workspace = true } # 刚好可以开始接入你需要的标准库错误处理 + + + +[features] +default = [] +embed-models = [] # 这是一个留给有特殊需求、且自己下载了模型放入 models/ 目录的人的后门 \ No newline at end of file diff --git a/ddddocr-tract/src/det/mod.rs b/ddddocr-tract/src/det/mod.rs new file mode 100644 index 0000000..3e7b3f8 --- /dev/null +++ b/ddddocr-tract/src/det/mod.rs @@ -0,0 +1 @@ +pub mod session; \ No newline at end of file diff --git a/ddddocr-tract/src/det/session.rs b/ddddocr-tract/src/det/session.rs new file mode 100644 index 0000000..f51f0f3 --- /dev/null +++ b/ddddocr-tract/src/det/session.rs @@ -0,0 +1,80 @@ +use crate::loader::{ModelLoader, ModelSession, ModelType}; +use anyhow::Context; +use ddddocr_core::error::{DdddError, Result}; +use ddddocr_core::{DetEngine, DetOutput, InferenceEngine}; +use ndarray::Ix3; +use std::path::Path; +use tract_onnx::prelude::{Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp, tvec}; +#[derive(Debug)] +pub struct DetSession { + pub session: RunnableModel, Graph>>, +} + +impl ModelSession for DetSession { + fn get_model_type(&self) -> ModelType { + todo!() + } + fn desc(&self) -> String { + "Detection Model 加载成功".to_string() + } +} + +impl DetSession { + pub fn new

(model_path: P) -> Result + where + P: AsRef, + { + let session = ModelLoader::model_for_path(&model_path)?.session; + Ok(Self { session }) + } + + pub fn model_from_bytes(model_bytes: &[u8]) -> Result { + let session = ModelLoader::model_from_bytes(model_bytes)?.session; + Ok(Self { session }) + } + // pub fn inference(&self, tensor: Tensor) -> anyhow::Result { + // // tract 的 run 会返回一个 Vec,我们通常只需要第一个输出 + // // let result = self.ocr.run(tvec!(tensor.into()))?; + // let mut result = self + // .session + // .run(tvec!(tensor.into())) + // .context("执行模型推理失败")?; + // println!("模型输出原始数据: {:?}", result); + // Ok(result.swap_remove(0).into_tensor()) + // } +} + +impl InferenceEngine for DetSession { + type Output = DetOutput; // 明确绑定 OCR 小枚举 + fn inference(&self, input_array: ndarray::Array4) -> Result { + // tract 的 run 会返回一个 Vec,我们通常只需要第一个输出 + // let result = self.ocr.run(tvec!(tensor.into()))?; + let tensor = Tensor::from(input_array); + + let mut result = self + .session + .run(tvec!(tensor.into())) + .context("执行模型推理失败")?; + 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")?; + // 提前利用克隆(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, // 优雅降维失败时动态捕获 + })?; + Ok(DetOutput::Detection(array3)) + + // 在引擎内部消化掉 DatumType 强耦合 + } +} + +impl DetEngine for DetSession {} diff --git a/ddddocr-tract/src/lib.rs b/ddddocr-tract/src/lib.rs new file mode 100644 index 0000000..9725b5d --- /dev/null +++ b/ddddocr-tract/src/lib.rs @@ -0,0 +1,6 @@ +mod det; +pub mod loader; +mod ocr; + +pub use det::session::DetSession; +pub use ocr::session::OcrSession; \ No newline at end of file diff --git a/src/models/loader.rs b/ddddocr-tract/src/loader.rs similarity index 64% rename from src/models/loader.rs rename to ddddocr-tract/src/loader.rs index 75b425d..bb15987 100644 --- a/src/models/loader.rs +++ b/ddddocr-tract/src/loader.rs @@ -1,8 +1,8 @@ use anyhow::Context; +use ddddocr_core::error::Result; use std::io::Cursor; use tract_onnx::onnx; -use tract_onnx::prelude::*; - +use tract_onnx::prelude::*; // 引入核心层的统一错误类型 /// OCR 模型:包含路径和字符集 pub enum ModelType { @@ -21,27 +21,31 @@ pub struct ModelLoader { } impl ModelLoader { - pub fn model_for_path

(model_path: P) -> anyhow::Result + pub fn model_for_path

(model_path: P) -> Result where P: AsRef, { let session = onnx() .model_for_path(model_path) .with_context(|| "加载 ONNX 模型失败,请检查路径是否正确")? - .into_optimized()? - .into_runnable()?; + .into_optimized() + .with_context(|| "优化 Tract 模型图失败")? + .into_runnable() + .with_context(|| "构建可运行 Tract 实例失败")?; Ok(Self { session }) } /// 策略 B:从内存字节流加载模型(配合 include_bytes! 使用) - pub fn model_from_bytes(model_bytes: &[u8]) -> anyhow::Result { + pub fn model_from_bytes(model_bytes: &[u8]) -> Result { // 使用 std::io::Cursor 将 &[u8] 包装为可读的流(实现 std::io::Read) let mut cursor = Cursor::new(model_bytes); let session = onnx() .model_for_read(&mut cursor) .with_context(|| "从内存字节流解析 ONNX 模型失败")? - .into_optimized()? - .into_runnable()?; + .into_optimized() + .with_context(|| "优化 Tract 模型图失败")? + .into_runnable() + .with_context(|| "构建可运行 Tract 实例失败")?; Ok(Self { session }) } diff --git a/ddddocr-tract/src/ocr/mod.rs b/ddddocr-tract/src/ocr/mod.rs new file mode 100644 index 0000000..3e7b3f8 --- /dev/null +++ b/ddddocr-tract/src/ocr/mod.rs @@ -0,0 +1 @@ +pub mod session; \ No newline at end of file diff --git a/ddddocr-tract/src/ocr/session.rs b/ddddocr-tract/src/ocr/session.rs new file mode 100644 index 0000000..cbbe58e --- /dev/null +++ b/ddddocr-tract/src/ocr/session.rs @@ -0,0 +1,125 @@ +use crate::loader::ModelLoader; +use anyhow::Context; +use ddddocr_core::error::{DdddError, Result}; +use ddddocr_core::{InferenceEngine, ModelMetadata, OcrEngine, OcrOutput}; +use ndarray::s; +use std::path::Path; +use tract_onnx::prelude::DatumType; +use tract_onnx::prelude::{Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp, tvec}; + +pub struct OcrSession { + pub session: RunnableModel, Graph>>, + pub model_metadata: ModelMetadata, +} +impl OcrSession { + pub fn new

(model_path: P, model_metadata: ModelMetadata) -> Result + where + P: AsRef, + { + let session = ModelLoader::model_for_path(model_path)?.session; + Ok(Self { + session, + model_metadata, + }) + } + + pub fn model_from_bytes(model_bytes: &[u8], model_metadata: ModelMetadata) -> Result { + let session = ModelLoader::model_from_bytes(model_bytes)?.session; + Ok(Self { + session, + model_metadata, + }) + } +} +impl OcrEngine for OcrSession { + fn metadata(&self) -> &ModelMetadata { + &self.model_metadata + } +} +impl InferenceEngine for OcrSession { + type Output = OcrOutput; + /// 对应 Python 的 _inference + fn inference(&self, input_array: ndarray::Array4) -> Result { + // tract 的 run 会返回一个 Vec,我们通常只需要第一个输出 + // let result = self.ocr.run(tvec!(tensor.into()))?; + let tensor = Tensor::from(input_array); + + let mut result = self + .session + .run(tvec!(tensor.into())) + .context("执行模型推理失败")?; + println!("模型输出原始数据: {:?}", result); + // Ok(result.swap_remove(0).into_tensor()) + let raw_tensor = result.swap_remove(0).into_tensor(); + // 在引擎内部消化掉 DatumType 强耦合 + match raw_tensor.datum_type() { + DatumType::I64 => { + let array_d = raw_tensor + .into_array::() + .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, + })?; + Ok(OcrOutput::Indices(array1)) + } + DatumType::F32 => { + let shape = raw_tensor.shape(); + println!("模型输出shape数据: {:?}", shape); + let view = raw_tensor + .to_array_view::() + .context("Tract 无法获取 f32 内存视图")?; + + // 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)) + } + _ => Err( + // anyhow::anyhow!("不支持的模型输出数据类型: {:?}",raw_tensor.datum_type()) + DdddError::UnknownOutputFormat, + ), + } + } +} diff --git a/tests/char_slice.rs b/ddddocr-tract/tests/char_slice.rs similarity index 99% rename from tests/char_slice.rs rename to ddddocr-tract/tests/char_slice.rs index 59beb89..8eb73b7 100644 --- a/tests/char_slice.rs +++ b/ddddocr-tract/tests/char_slice.rs @@ -2,8 +2,8 @@ use std::borrow::Cow; use std::fs::File; use std::path::Path; use anyhow::anyhow; -use ddddocr_rs::models::ocr::metadata::Charset; -use ddddocr_rs::models::ocr::metadata::{Normalization, Resize}; +use ddddocr_core::models::ocr::metadata::Charset; +use ddddocr_core::models::ocr::metadata::{Normalization, Resize}; pub const CHARSET_BETA: &[&str] = &[ "", "笤", "谴", "膀", "荔", "佰", "电", "臁", "矍", "同", "奇", "芄", "吠", "6", "曛", "荇", diff --git a/tests/ocr_test.rs b/ddddocr-tract/tests/ocr_test.rs similarity index 85% rename from tests/ocr_test.rs rename to ddddocr-tract/tests/ocr_test.rs index cee7d53..366d8ef 100644 --- a/tests/ocr_test.rs +++ b/ddddocr-tract/tests/ocr_test.rs @@ -1,11 +1,12 @@ -use ddddocr_rs::models::det::DetectionResult; -use ddddocr_rs::{DetBuilder, DetSession, Detector, ModelMetadata, Ocr, OcrSession, Slider}; // 假设你的包名是这个 +use ddddocr_core::models::det::DetectionResult; +use ddddocr_core::{DetBuilder, Detector, ModelMetadata, Ocr, Slider}; // 假设你的包名是这个 +use ddddocr_tract::{DetSession,OcrSession}; use image::{DynamicImage, Rgb}; use std::fs; use std::path::Path; mod char_slice; use char_slice::CHARSET_BETA; -use ddddocr_rs::models::ocr::metadata::{Normalization, Resize}; +use ddddocr_core::models::ocr::metadata::{Normalization, Resize}; fn load_image>(path: P) -> anyhow::Result { // 1. 先将泛型转为具体的 &Path 引用 @@ -79,7 +80,7 @@ fn test_full_classification() { .expect("模型加载失败"); // 2. 加载测试图片 - let img = image::open("samples/code2.png").expect("测试图片不存在"); + let img = image::open("D:/CNWei/CNW/Rust/ddddocr-rs/samples/code2.png").expect("测试图片不存在"); // 3. 执行识别 let result = Ocr::new(&ocr) @@ -93,7 +94,7 @@ fn test_full_classification() { #[test] fn test_det_load() -> anyhow::Result<()> { let det = DetSession::new("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_det.onnx")?; - let image_path = "samples/det1.png"; + 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))?; @@ -112,7 +113,7 @@ fn test_det_load() -> anyhow::Result<()> { println!("未检测到任何目标。"); } else { // 如果 save_debug_image 报错,记得去把它的入参类型和内部访问也改为 DetectionResult - save_debug_image(&img, &bboxes, "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].. 索引访问改为结构体字段访问 @@ -128,8 +129,8 @@ fn test_real_slide_match() { // 1. 加载你准备好的测试图 // 假设图片放在项目根目录下的 assets 文件夹 - let target_img = load_image("samples/hua.png").expect("请确保 samples/hua.png 存在"); - let bg_img = load_image("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 @@ -157,8 +158,8 @@ fn test_real_slide_comparison() { // 1. 加载你准备好的测试图 // 假设图片放在项目根目录下的 assets 文件夹 - let target_img = load_image("samples/ken.jpg").expect("请确保 samples/ken.jpg 存在"); - let bg_img = load_image("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 diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 2960071..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod algo; -mod error; -pub mod models; -pub mod utils; - -pub use crate::algo::{SlideResult, Slider}; -pub use crate::models::det::{DetBuilder, DetSession, DetectionResult, Detector}; -pub use crate::models::ocr::{Ocr, OcrBuilder, OcrResult, OcrSession}; -pub use models::ocr::metadata::ModelMetadata; diff --git a/src/models/det/session.rs b/src/models/det/session.rs deleted file mode 100644 index 92a36ff..0000000 --- a/src/models/det/session.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::models::loader::{ModelLoader, ModelSession, ModelType}; -use anyhow::{Context, Result}; -use std::path::Path; -use tract_onnx::prelude::{tvec, Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp}; - -#[derive(Debug)] -pub struct DetSession { - pub(crate) session: RunnableModel, Graph>>, -} - -impl ModelSession for DetSession { - fn get_model_type(&self) -> ModelType { - todo!() - } - fn desc(&self) -> String { - "Detection Model 加载成功".to_string() - } -} - -impl DetSession { - pub fn new

(model_path: P) -> Result - where - P: AsRef, - { - let session = ModelLoader::model_for_path(&model_path)?.session; - Ok(Self { session }) - } - - pub fn model_from_bytes(model_bytes: &[u8]) -> Result { - let session = ModelLoader::model_from_bytes(model_bytes)?.session; - Ok(Self { session }) - } - pub fn inference(&self, tensor: Tensor) -> anyhow::Result { - // tract 的 run 会返回一个 Vec,我们通常只需要第一个输出 - // let result = self.ocr.run(tvec!(tensor.into()))?; - let mut result = self - .session - .run(tvec!(tensor.into())) - .context("执行模型推理失败")?; - println!("模型输出原始数据: {:?}", result); - Ok(result.swap_remove(0).into_tensor()) - } -} diff --git a/src/models/ocr/session.rs b/src/models/ocr/session.rs deleted file mode 100644 index 4a226af..0000000 --- a/src/models/ocr/session.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::models::ocr::metadata::ModelMetadata; -use crate::models::loader::{ModelLoader, ModelSession, ModelType}; -use anyhow::Context; -use anyhow::Result; -use std::path::Path; -use tract_onnx::prelude::{tvec, Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp}; - -pub struct OcrSession { - pub session: RunnableModel, Graph>>, - pub model_metadata: ModelMetadata, -} -impl ModelSession for OcrSession { - fn get_model_type(&self) -> ModelType { - todo!("使用thiserror作为错误处理的库,thiserror 专门用于开发库(Library)"); - } - fn desc(&self) -> String { - "Ocr Model 加载成功".to_string() - } -} -impl OcrSession { - pub fn new

(model_path: P, model_metadata: ModelMetadata) -> Result - where - P: AsRef, - { - let session = ModelLoader::model_for_path(model_path)?.session; - Ok(Self { - session, - model_metadata, - }) - } - - pub fn model_from_bytes( - model_bytes: &[u8], - model_metadata: ModelMetadata, - ) -> Result { - let session = ModelLoader::model_from_bytes(model_bytes)?.session; - Ok(Self { - session, - model_metadata, - }) - } - /// 对应 Python 的 _inference - pub fn inference(&self, tensor: Tensor) -> anyhow::Result { - // tract 的 run 会返回一个 Vec,我们通常只需要第一个输出 - // let result = self.ocr.run(tvec!(tensor.into()))?; - let mut result = self - .session - .run(tvec!(tensor.into())) - .context("执行模型推理失败")?; - println!("模型输出原始数据: {:?}", result); - Ok(result.swap_remove(0).into_tensor()) - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100644 index 2f2430c..0000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod image_io; -pub mod image_processor; -pub mod cv_ops;