1 Commits

Author SHA1 Message Date
ea7fb43a14 refactor: 抽象解耦推理引擎并重构为多Crate工作空间架构
- 移除 核心层与 tract/Tensor 的强耦合,前/后处理全线转用标准 ndarray
- 针对 OCR 与目标检测(Det)分别设计独立的强类型输出小枚举(OcrOutput/DetOutput)
- 利用 Trait 关联类型(Associated Type)InferenceEngine,OcrEngine,DetEngine 统一接口,实现多后端解耦
- 引入 thiserror 库,建立完备的强类型错误处理机制(DdddError/Result)
- 完成项目结构初拆,剥离为 ddddocr-core 和 ddddocr-tract
2026-07-10 20:23:49 +08:00
34 changed files with 624 additions and 289 deletions

View File

@@ -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/ 目录的人的后门
thiserror = "1.0" # 刚好可以开始接入你需要的标准库错误处理

17
ddddocr-core/Cargo.toml Normal file
View File

@@ -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"] }

View File

@@ -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);

View File

@@ -16,3 +16,33 @@ pub(crate) const MODEL_DOWNLOAD_HELP: &str = "\
B.
================================================================================";
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<usize>,
},
#[error("内存不连续,无法执行零拷贝操作")]
NonContiguousMemory,
#[error("未知的模型输出格式")]
UnknownOutputFormat,
}
/// 统一用我们自己的 DdddError 包装 Result
pub type Result<T> = std::result::Result<T, DdddError>;

37
ddddocr-core/src/lib.rs Normal file
View File

@@ -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<i64>), // 拥有完整所有权的 1维数组可任意传递和返回
Logits(ndarray::Array2<f32>),
}
/// 2. 目标检测专属的、编译期安全的输出枚举
pub enum DetOutput {
Detection(ndarray::Array3<f32>), // 拥有完整所有权的 2维矩阵可任意传递和返回
}
/// 核心层定义的统一推理引擎接口。
/// 未来的 ddddocr-tract 和 ddddocr-ort 都必须实现这个 Trait
pub trait InferenceEngine {
/// 关联类型:具体的 Session 需要声明自己到底产出什么枚举
type Output;
fn inference(&self, input_array: ndarray::Array4<f32>) -> Result<Self::Output>;
}
pub trait OcrEngine: InferenceEngine<Output = OcrOutput> {
fn metadata(&self) -> &ModelMetadata;
}
pub trait DetEngine: InferenceEngine<Output = DetOutput> {}

View File

@@ -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,

View File

@@ -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>, 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::<f32>()?
.to_owned()
.into_dimensionality::<Ix3>()?;
// 2. 无缝、安全地解包出标准 3维 矩阵
let DetOutput::Detection(output_array) = outputs;
let predictions = self.demo_postprocess(output_array, (416, 416));
let pred = predictions.slice(s![0, .., ..]);

View File

@@ -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;

View File

@@ -1,3 +1,2 @@
pub mod loader;
pub mod ocr;
pub mod det;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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<i64>), // 拥有完整所有权的 1维数组可任意传递和返回
// Logits(ndarray::Array2<f32>), // 拥有完整所有权的 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<Tensor> {
fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result<ndarray::Array4<f32>> {
// 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(&current_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<OcrResult> {
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<f32>,且保证是 [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<i64> = 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::<f32>::zeros((steps, classes));
let mut prob_matrix = ndarray::Array2::<f32>::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<OcrResult> {
// 1. 拿到底层的动态维度只读视图
let view = raw_tensor.to_array_view::<i64>()?;
// 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<OcrResult> {
let shape = raw_tensor.shape();
println!("模型输出shape数据: {:?}", shape);
let view = raw_tensor.to_array_view::<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(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<f32> = 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<i64> = 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<OcrResult> {
// // 1. 拿到底层的动态维度只读视图
// let view = raw_tensor.to_array_view::<i64>()?;
//
// // 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<OcrResult> {
// let shape = raw_tensor.shape();
// println!("模型输出shape数据: {:?}", shape);
// let view = raw_tensor.to_array_view::<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(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<f32> = 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<i64> = 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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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<u8>, b: &ArrayView3<u8>) -> Array3<u8> {
// 利用 ndarray 的 map_collect生成差值的绝对值数组
@@ -72,6 +73,9 @@ pub fn find_contours_and_max(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>) -> Opt
Some(max_label)
}
}
/// 根据目标连通域标签,计算其在图像中的外接矩形边界框(对应 `cv2.boundingRect`
///
/// 返回格式: `(min_x, min_y, width, height)`
pub fn bounding_rect(
labelled: &ImageBuffer<Luma<u32>, Vec<u32>>,
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<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>> {
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;

View File

@@ -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模式)

View File

@@ -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;

View File

@@ -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<f32>, shape: &[usize]) -> Result<OcrOutput> {
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))
}

24
ddddocr-tract/Cargo.toml Normal file
View File

@@ -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/ 目录的人的后门

View File

@@ -0,0 +1 @@
pub mod session;

View File

@@ -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<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
}
impl ModelSession for DetSession {
fn get_model_type(&self) -> ModelType {
todo!()
}
fn desc(&self) -> String {
"Detection Model 加载成功".to_string()
}
}
impl DetSession {
pub fn new<P>(model_path: P) -> Result<Self>
where
P: AsRef<Path>,
{
let session = ModelLoader::model_for_path(&model_path)?.session;
Ok(Self { session })
}
pub fn model_from_bytes(model_bytes: &[u8]) -> Result<Self> {
let session = ModelLoader::model_from_bytes(model_bytes)?.session;
Ok(Self { session })
}
// pub fn inference(&self, tensor: Tensor) -> anyhow::Result<Tensor> {
// // tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
// // 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<f32>) -> Result<Self::Output> {
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
// 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::<f32>()
.context("Tract 实体张量无法转换为 ndarray::ArrayD")?;
// 提前利用克隆(Clone)备份好当前未转维度前的真实 shape (Vec<usize>)
let actual_shape = array_d.shape().to_vec();
let array3 =
array_d
.into_dimensionality::<Ix3>()
.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 {}

6
ddddocr-tract/src/lib.rs Normal file
View File

@@ -0,0 +1,6 @@
mod det;
pub mod loader;
mod ocr;
pub use det::session::DetSession;
pub use ocr::session::OcrSession;

View File

@@ -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<P>(model_path: P) -> anyhow::Result<Self>
pub fn model_for_path<P>(model_path: P) -> Result<Self>
where
P: AsRef<std::path::Path>,
{
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<Self> {
pub fn model_from_bytes(model_bytes: &[u8]) -> Result<Self> {
// 使用 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 })
}

View File

@@ -0,0 +1 @@
pub mod session;

View File

@@ -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<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
pub model_metadata: ModelMetadata,
}
impl OcrSession {
pub fn new<P>(model_path: P, model_metadata: ModelMetadata) -> Result<Self>
where
P: AsRef<Path>,
{
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<Self> {
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<f32>) -> Result<Self::Output> {
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
// 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::<i64>()
.context("Tract 无法获取 i64 内存视图")?;
// 🌟 提前提取真实维度
let actual_shape = array_d.shape().to_vec();
// 转成标准的 Array1 传给 core
let array1 = array_d
.to_owned()
.into_dimensionality::<ndarray::Ix1>()
.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::<f32>()
.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,
),
}
}
}

View File

@@ -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", "", "",

View File

@@ -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<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
// 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

View File

@@ -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;

View File

@@ -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<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
}
impl ModelSession for DetSession {
fn get_model_type(&self) -> ModelType {
todo!()
}
fn desc(&self) -> String {
"Detection Model 加载成功".to_string()
}
}
impl DetSession {
pub fn new<P>(model_path: P) -> Result<Self, anyhow::Error>
where
P: AsRef<Path>,
{
let session = ModelLoader::model_for_path(&model_path)?.session;
Ok(Self { session })
}
pub fn model_from_bytes(model_bytes: &[u8]) -> Result<Self, anyhow::Error> {
let session = ModelLoader::model_from_bytes(model_bytes)?.session;
Ok(Self { session })
}
pub fn inference(&self, tensor: Tensor) -> anyhow::Result<Tensor> {
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
// 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())
}
}

View File

@@ -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<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
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<P>(model_path: P, model_metadata: ModelMetadata) -> Result<Self, anyhow::Error>
where
P: AsRef<Path>,
{
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<Self, anyhow::Error> {
let session = ModelLoader::model_from_bytes(model_bytes)?.session;
Ok(Self {
session,
model_metadata,
})
}
/// 对应 Python 的 _inference
pub fn inference(&self, tensor: Tensor) -> anyhow::Result<Tensor> {
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
// 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())
}
}

View File

@@ -1,3 +0,0 @@
pub mod image_io;
pub mod image_processor;
pub mod cv_ops;