Compare commits
4 Commits
feature-v0
...
feature-v0
| Author | SHA1 | Date | |
|---|---|---|---|
| ea7fb43a14 | |||
| 2d9cb35590 | |||
| 0cf3d5fefb | |||
| 31271e80db |
15
Cargo.toml
15
Cargo.toml
@@ -1,10 +1,17 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "ddddocr-rs"
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"ddddocr-core",
|
||||||
|
"ddddocr-tract",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
|
[workspace.dependencies]
|
||||||
tract-onnx = { version = "0.21.10" }
|
tract-onnx = { version = "0.21.10" }
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
image = "0.25.10"
|
image = "0.25.10"
|
||||||
@@ -12,3 +19,5 @@ base64 = "0.22.1"
|
|||||||
imageproc = { version = "0.26.2", default-features = true }
|
imageproc = { version = "0.26.2", default-features = true }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.150"
|
serde_json = "1.0.150"
|
||||||
|
ndarray="0.16.1"
|
||||||
|
thiserror = "1.0" # 刚好可以开始接入你需要的标准库错误处理
|
||||||
|
|||||||
17
ddddocr-core/Cargo.toml
Normal file
17
ddddocr-core/Cargo.toml
Normal 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"] }
|
||||||
3
ddddocr-core/src/algo/mod.rs
Normal file
3
ddddocr-core/src/algo/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod slide;
|
||||||
|
|
||||||
|
pub use slide::{SlideResult, Slider};
|
||||||
@@ -1,32 +1,40 @@
|
|||||||
use crate::utils::cv_ops;
|
use crate::utils::image_proc;
|
||||||
use crate::utils::cv_ops::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray};
|
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 crate::utils::image_io::image_to_ndarray;
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use image::{DynamicImage, GenericImageView};
|
use image::DynamicImage;
|
||||||
use image::{ImageBuffer, Luma};
|
use image::Luma;
|
||||||
use imageproc::contrast::{ThresholdType, threshold};
|
use imageproc::contrast::{ThresholdType, threshold};
|
||||||
use imageproc::distance_transform::Norm;
|
use imageproc::distance_transform::Norm;
|
||||||
use imageproc::edges::canny;
|
use imageproc::edges::canny;
|
||||||
use imageproc::morphology::{close, open};
|
use imageproc::morphology::{close, open};
|
||||||
use imageproc::region_labelling::{Connectivity, connected_components};
|
use imageproc::region_labelling::{Connectivity, connected_components};
|
||||||
use imageproc::template_matching::{MatchTemplateMethod, match_template};
|
use imageproc::template_matching::{MatchTemplateMethod, match_template};
|
||||||
use std::cmp::{max, min};
|
use std::fmt;
|
||||||
use tract_onnx::prelude::tract_ndarray::{Array2, Array3, ArrayView2, ArrayView3, Axis, s};
|
use ndarray::{ArrayView2, ArrayView3};
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct SlideResult {
|
pub struct SlideResult {
|
||||||
pub target: [i32; 2],
|
pub target: [i32; 2],
|
||||||
pub target_x: i32,
|
pub target_x: i32,
|
||||||
pub target_y: i32,
|
pub target_y: i32,
|
||||||
pub confidence: f64,
|
pub confidence: f64,
|
||||||
}
|
}
|
||||||
|
impl fmt::Display for SlideResult {
|
||||||
pub struct Slide;
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "滑块匹配测试结果:")?;
|
||||||
impl Slide {
|
writeln!(f, "检测坐标: [x: {}, y: {}]", self.target_x, self.target_y)?;
|
||||||
pub fn new() -> Self {
|
// 注意:这里保留 4 位小数,如果想让外部控制,也可以直接写 {:.4}
|
||||||
Self
|
write!(f, "置信度: {:.4}", self.confidence)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Slider;
|
||||||
|
|
||||||
|
impl Slider {
|
||||||
|
pub fn new() -> Result<Self, anyhow::Error> {
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
/// 对应 Python: slide_match 滑块匹配接口
|
/// 对应 Python: slide_match 滑块匹配接口
|
||||||
pub fn slide_match(
|
pub fn slide_match(
|
||||||
&self,
|
&self,
|
||||||
@@ -59,23 +67,8 @@ impl Slide {
|
|||||||
target: ArrayView3<u8>,
|
target: ArrayView3<u8>,
|
||||||
background: ArrayView3<u8>,
|
background: ArrayView3<u8>,
|
||||||
) -> Result<SlideResult> {
|
) -> Result<SlideResult> {
|
||||||
// let (h, w, _) = target.dim();
|
|
||||||
|
|
||||||
// 1. 计算图像差异并灰度化 (对应 cv2.absdiff + cv2.cvtColor)
|
|
||||||
// 使用 OpenCV 标准权重公式:0.299R + 0.587G + 0.114B
|
|
||||||
// let mut diff_buffer = ImageBuffer::new(w as u32, h as u32);
|
|
||||||
// for y in 0..h {
|
|
||||||
// for x in 0..w {
|
|
||||||
// let r_diff = (target[[y, x, 0]] as i16 - background[[y, x, 0]] as i16).abs() as f32;
|
|
||||||
// let g_diff = (target[[y, x, 1]] as i16 - background[[y, x, 1]] as i16).abs() as f32;
|
|
||||||
// let b_diff = (target[[y, x, 2]] as i16 - background[[y, x, 2]] as i16).abs() as f32;
|
|
||||||
//
|
|
||||||
// let gray_diff = (0.299 * r_diff + 0.587 * g_diff + 0.114 * b_diff) as u8;
|
|
||||||
// diff_buffer.put_pixel(x as u32, y as u32, Luma([gray_diff]));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// 1. 计算差异数组 (复用 cv2::absdiff)
|
// 1. 计算差异数组 (复用 cv2::absdiff)
|
||||||
|
|
||||||
let (th, tw, tc) = target.dim();
|
let (th, tw, tc) = target.dim();
|
||||||
let (bh, bw, bc) = background.dim();
|
let (bh, bw, bc) = background.dim();
|
||||||
|
|
||||||
@@ -118,11 +111,11 @@ impl Slide {
|
|||||||
|
|
||||||
// // 统计每个标签出现的频率(即面积)
|
// // 统计每个标签出现的频率(即面积)
|
||||||
// 4. 寻找最大连通区域 (对应 findContours + max area)
|
// 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)
|
// 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)
|
// 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 {
|
Ok(SlideResult {
|
||||||
target: [center_x, center_y],
|
target: [center_x, center_y],
|
||||||
@@ -194,9 +187,6 @@ impl Slide {
|
|||||||
background: ArrayView2<u8>,
|
background: ArrayView2<u8>,
|
||||||
) -> Result<SlideResult> {
|
) -> Result<SlideResult> {
|
||||||
// 1. 将 ndarray 转换为 imageproc 需要的 ImageBuffer (无拷贝或轻量转换)
|
// 1. 将 ndarray 转换为 imageproc 需要的 ImageBuffer (无拷贝或轻量转换)
|
||||||
|
|
||||||
// let (bh, bw) = background.dim();
|
|
||||||
|
|
||||||
// 转换逻辑 (假设你已经有方法转回 ImageBuffer)
|
// 转换逻辑 (假设你已经有方法转回 ImageBuffer)
|
||||||
let t_buf = ndarray_to_luma8(target);
|
let t_buf = ndarray_to_luma8(target);
|
||||||
let b_buf = ndarray_to_luma8(background);
|
let b_buf = ndarray_to_luma8(background);
|
||||||
@@ -216,7 +206,7 @@ impl Slide {
|
|||||||
// 4. 计算中心点 (与 Python 逻辑完全一致)
|
// 4. 计算中心点 (与 Python 逻辑完全一致)
|
||||||
let (th, tw) = target.dim();
|
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 Target Width (tw): {}", tw);
|
||||||
// println!("Rust Best Max Loc X: {}", max_loc.0);
|
// println!("Rust Best Max Loc X: {}", max_loc.0);
|
||||||
// println!("Rust Final Center X: {}", center_x);
|
// println!("Rust Final Center X: {}", center_x);
|
||||||
@@ -261,7 +251,7 @@ impl Slide {
|
|||||||
// 5. 计算中心位置 (对齐 Python 逻辑)
|
// 5. 计算中心位置 (对齐 Python 逻辑)
|
||||||
// target_w, target_h 来自输入数组的维度
|
// target_w, target_h 来自输入数组的维度
|
||||||
let (th, tw) = target.dim();
|
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 对比
|
// 打印调试信息,方便与 Python 对比
|
||||||
// println!("Edge Match: max_val: {}, max_loc: {:?}", max_val, max_loc);
|
// println!("Edge Match: max_val: {}, max_loc: {:?}", max_val, max_loc);
|
||||||
48
ddddocr-core/src/error.rs
Normal file
48
ddddocr-core/src/error.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
pub(crate) const MODEL_DOWNLOAD_HELP: &str = "\
|
||||||
|
================================================================================
|
||||||
|
[ddddocr-rust] 错误:未找到默认的模型文件!
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
由于打包体积限制,本库未内置 ONNX 模型。请按照以下步骤操作:
|
||||||
|
|
||||||
|
1. 前往官方 GitHub 下载对应的模型权重:
|
||||||
|
- OCR 模型: https://github.com/sml2h3/ddddocr/raw/master/ddddocr/common_sml2h3_f32.onnx
|
||||||
|
- DET 模型: https://github.com/sml2h3/ddddocr/raw/master/ddddocr/common_det.onnx
|
||||||
|
|
||||||
|
2. 配置加载方式(二选一):
|
||||||
|
A. 【推荐】设置环境变量指向您下载的文件:
|
||||||
|
Linux/macOS: export DDDD_OCR_MODEL=\"/path/to/common_sml2h3_f32.onnx\"
|
||||||
|
Windows (CMD): set DDDD_OCR_MODEL=C:\\path\\to\\common_sml2h3_f32.onnx
|
||||||
|
Windows (PowerShell): $env:DDDD_OCR_MODEL=\"C:\\path\\to\\common_sml2h3_f32.onnx\"
|
||||||
|
|
||||||
|
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
37
ddddocr-core/src/lib.rs
Normal 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> {}
|
||||||
|
|
||||||
|
|
||||||
26
ddddocr-core/src/models/det/builder.rs
Normal file
26
ddddocr-core/src/models/det/builder.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use crate::models::det::executor::Detector;
|
||||||
|
// use ddddocr_tract::det::session::DetSession;
|
||||||
|
use crate::DetEngine;
|
||||||
|
|
||||||
|
pub struct DetBuilder {
|
||||||
|
use_gpu: bool,
|
||||||
|
device_id: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DetBuilder {
|
||||||
|
fn use_gpu(mut self) -> Self {
|
||||||
|
self.use_gpu = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn device_id(mut self, device_id: u8) -> Self {
|
||||||
|
self.device_id = device_id;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn build(self, session: &dyn DetEngine) -> Detector<'_> {
|
||||||
|
Detector {
|
||||||
|
session,
|
||||||
|
use_gpu: self.use_gpu,
|
||||||
|
device_id: self.device_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
use crate::models::loader::{ModelLoader, ModelSession, ModelType};
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use image::{DynamicImage, GenericImageView, imageops::FilterType};
|
use image::{imageops::FilterType, DynamicImage, GenericImageView};
|
||||||
use tract_onnx::prelude::tract_ndarray::{Array2, Array3, Array4, Axis, prelude::*, s};
|
use std::fmt;
|
||||||
use tract_onnx::prelude::{Graph, RunnableModel, Tensor, TypedFact, TypedOp, tvec};
|
use ndarray::{prelude::*, s, Array2, Array3, Array4, Axis};
|
||||||
|
// use tract_onnx::prelude::{Tensor};
|
||||||
|
|
||||||
|
|
||||||
|
// use ddddocr_tract::det::session::DetSession;
|
||||||
|
use crate::{DetEngine, DetOutput};
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct DetectionResult {
|
pub struct DetectionResult {
|
||||||
pub x1: i32,
|
pub x1: i32,
|
||||||
@@ -16,30 +17,41 @@ pub struct DetectionResult {
|
|||||||
pub class_id: u32,
|
pub class_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DetectionResult {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
// 结构体只管自己这一行怎么显示,不用管外部的索引 [i]
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"x1={}, y1={}, x2={}, y2={}, 分数={:.4}, 类别ID={}",
|
||||||
|
self.x1, self.y1, self.x2, self.y2, self.score, self.class_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct Det {
|
pub struct Detector<'a> {
|
||||||
session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
pub(crate) session: &'a dyn DetEngine,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) use_gpu: bool,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) device_id: u8,
|
||||||
}
|
}
|
||||||
impl ModelSession for Det {
|
|
||||||
fn get_model_type(&self) -> ModelType {
|
impl<'a> Detector<'a> {
|
||||||
todo!()
|
pub fn new(session: &'a dyn DetEngine) -> Self {
|
||||||
}
|
Detector {
|
||||||
fn desc(&self) -> String {
|
session,
|
||||||
"Detection Model 加载成功".to_string()
|
use_gpu: false,
|
||||||
|
device_id: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Det {
|
|
||||||
pub fn new(model_path: String) -> Result<Self, anyhow::Error> {
|
|
||||||
let session = ModelLoader::load_model(&model_path)?.session;
|
|
||||||
Ok(Self { session })
|
|
||||||
}
|
|
||||||
pub fn predict(&self, image: &DynamicImage) -> Result<Vec<DetectionResult>> {
|
pub fn predict(&self, image: &DynamicImage) -> Result<Vec<DetectionResult>> {
|
||||||
// Rust 中通常在调用层处理文件/PIL转换,这里直接进入核心逻辑
|
// Rust 中通常在调用层处理文件/PIL转换,这里直接进入核心逻辑
|
||||||
self.get_bbox(image)
|
self.get_bbox(image)
|
||||||
}
|
}
|
||||||
/// 2. preproc: 纯 Rust 实现 (替代 OpenCV)
|
/// 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 (target_h, target_w) = input_size;
|
||||||
let (img_w, img_h) = image.dimensions();
|
let (img_w, img_h) = image.dimensions();
|
||||||
|
|
||||||
@@ -77,8 +89,7 @@ impl Det {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok((array, r))
|
||||||
Ok((array.into(), r))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 3. demo_postprocess (逻辑与 Python 一致)
|
/// 3. demo_postprocess (逻辑与 Python 一致)
|
||||||
@@ -244,11 +255,11 @@ impl Det {
|
|||||||
let (input_tensor, ratio) = self.preproc(dynamic_img, (416, 416))?;
|
let (input_tensor, ratio) = self.preproc(dynamic_img, (416, 416))?;
|
||||||
|
|
||||||
// tract 推理
|
// tract 推理
|
||||||
let outputs = self.session.run(tvec!(input_tensor.into()))?;
|
// let outputs = self.session.session.run(tvec!(input_tensor.into()))?;
|
||||||
let output_array = outputs[0]
|
let outputs = self.session.inference(input_tensor)?;
|
||||||
.to_array_view::<f32>()?
|
// let output_array = outputs[0]
|
||||||
.to_owned()
|
// 2. 无缝、安全地解包出标准 3维 矩阵
|
||||||
.into_dimensionality::<Ix3>()?;
|
let DetOutput::Detection(output_array) = outputs;
|
||||||
|
|
||||||
let predictions = self.demo_postprocess(output_array, (416, 416));
|
let predictions = self.demo_postprocess(output_array, (416, 416));
|
||||||
let pred = predictions.slice(s![0, .., ..]);
|
let pred = predictions.slice(s![0, .., ..]);
|
||||||
@@ -273,15 +284,13 @@ impl Det {
|
|||||||
let detections = self.multiclass_nms(&boxes_xyxy, &scores, 0.45, 0.1);
|
let detections = self.multiclass_nms(&boxes_xyxy, &scores, 0.45, 0.1);
|
||||||
let final_results = detections
|
let final_results = detections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|d| {
|
.map(|d| DetectionResult {
|
||||||
DetectionResult{
|
|
||||||
x1: (d[0] as i32).max(0).min(orig_w as i32),
|
x1: (d[0] as i32).max(0).min(orig_w as i32),
|
||||||
y1: (d[1] as i32).max(0).min(orig_h as i32),
|
y1: (d[1] as i32).max(0).min(orig_h as i32),
|
||||||
x2: (d[2] as i32).max(0).min(orig_w as i32),
|
x2: (d[2] as i32).max(0).min(orig_w as i32),
|
||||||
y2: (d[3] as i32).max(0).min(orig_h as i32),
|
y2: (d[3] as i32).max(0).min(orig_h as i32),
|
||||||
score: d[4],
|
score: d[4],
|
||||||
class_id: d[5] as u32,
|
class_id: d[5] as u32,
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(final_results)
|
Ok(final_results)
|
||||||
6
ddddocr-core/src/models/det/mod.rs
Normal file
6
ddddocr-core/src/models/det/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
mod builder;
|
||||||
|
mod executor;
|
||||||
|
|
||||||
|
pub use builder::DetBuilder;
|
||||||
|
pub use executor::{DetectionResult, Detector};
|
||||||
|
// pub use ddddocr_tract::det::session::DetSession;
|
||||||
2
ddddocr-core/src/models/mod.rs
Normal file
2
ddddocr-core/src/models/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod ocr;
|
||||||
|
pub mod det;
|
||||||
74
ddddocr-core/src/models/ocr/builder.rs
Normal file
74
ddddocr-core/src/models/ocr/builder.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use crate::models::ocr::executor::Ocr;
|
||||||
|
// 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格式问题
|
||||||
|
png_fix: bool,
|
||||||
|
/// 是否返回概率信息
|
||||||
|
probability: bool,
|
||||||
|
/// 颜色过滤:保留的颜色列表
|
||||||
|
color_filter: Option<Box<dyn ColorFilter + Send + Sync>>,
|
||||||
|
/// 字符集范围
|
||||||
|
charset_restrict: Option<Box<dyn TokenFilter + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OcrBuilder {
|
||||||
|
// 初始化任务,设置默认参数
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
png_fix: false, // 默认值
|
||||||
|
probability: false,
|
||||||
|
color_filter: None,
|
||||||
|
charset_restrict: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn png_fix(mut self, value: bool) -> Self {
|
||||||
|
self.png_fix = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn probability(mut self, value: bool) -> Self {
|
||||||
|
self.probability = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_filter<T>(mut self, filter: T) -> Self
|
||||||
|
where
|
||||||
|
T: ColorFilter + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.color_filter = Some(Box::new(filter));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn charset_restrict<T>(mut self, restrict: T) -> Self
|
||||||
|
where
|
||||||
|
T: TokenFilter + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.charset_restrict = Some(Box::new(restrict));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
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.metadata().charset.tokens;
|
||||||
|
let final_charset_indices = match &self.charset_restrict {
|
||||||
|
Some(restrict) => restrict.apply_to_charset(tokens),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ocr::new(session, self)
|
||||||
|
Ocr {
|
||||||
|
session,
|
||||||
|
png_fix: self.png_fix, // 原地解构出来
|
||||||
|
probability: self.probability,
|
||||||
|
final_color_ranges,
|
||||||
|
final_charset_indices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,24 @@
|
|||||||
use std::str::FromStr;
|
use crate::utils::image_proc::rgb_to_opencv_hsv;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use image::{DynamicImage, ImageBuffer, Rgb};
|
use image::{DynamicImage, ImageBuffer, Rgb};
|
||||||
use crate::utils::cv_ops::rgb_to_opencv_hsv;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct HsvRange {
|
|
||||||
pub lower: (u8, u8, u8), // (H, S, V)
|
|
||||||
pub upper: (u8, u8, u8), // (H, S, V)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 核心区间判定辅助函数
|
/// 核心区间判定辅助函数
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn is_pixel_matched(ranges: &[HsvRange], h: u8, s: u8, v: u8) -> bool {
|
fn is_pixel_matched(ranges: &[HsvRange], h: u8, s: u8, v: u8) -> bool {
|
||||||
ranges.iter().any(|range| {
|
ranges.iter().any(|range| {
|
||||||
h >= range.lower.0 && h <= range.upper.0 &&
|
h >= range.lower.0
|
||||||
s >= range.lower.1 && s <= range.upper.1 &&
|
&& h <= range.upper.0
|
||||||
v >= range.lower.2 && v <= range.upper.2
|
&& s >= range.lower.1
|
||||||
|
&& s <= range.upper.1
|
||||||
|
&& v >= range.lower.2
|
||||||
|
&& v <= range.upper.2
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn filter_image(image: &DynamicImage, hsv_ranges: &[HsvRange]) -> anyhow::Result<DynamicImage> {
|
pub fn apply_to_image(
|
||||||
|
image: &DynamicImage,
|
||||||
|
hsv_ranges: &[HsvRange],
|
||||||
|
) -> anyhow::Result<DynamicImage> {
|
||||||
// 1. 统一转换为连续内存的 RGB8 缓冲区 (对应 Python 的 Image 到 RGB/BGR 数组转换)
|
// 1. 统一转换为连续内存的 RGB8 缓冲区 (对应 Python 的 Image 到 RGB/BGR 数组转换)
|
||||||
let rgb_img = image.to_rgb8();
|
let rgb_img = image.to_rgb8();
|
||||||
let (width, height) = rgb_img.dimensions();
|
let (width, height) = rgb_img.dimensions();
|
||||||
@@ -50,6 +49,13 @@ pub fn filter_image(image: &DynamicImage, hsv_ranges: &[HsvRange]) -> anyhow::Re
|
|||||||
|
|
||||||
Ok(DynamicImage::ImageRgb8(filtered_buffer))
|
Ok(DynamicImage::ImageRgb8(filtered_buffer))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct HsvRange {
|
||||||
|
pub lower: (u8, u8, u8), // (H, S, V)
|
||||||
|
pub upper: (u8, u8, u8), // (H, S, V)
|
||||||
|
}
|
||||||
|
|
||||||
impl HsvRange {
|
impl HsvRange {
|
||||||
pub const fn new(lower: (u8, u8, u8), upper: (u8, u8, u8)) -> Self {
|
pub const fn new(lower: (u8, u8, u8), upper: (u8, u8, u8)) -> Self {
|
||||||
Self { lower, upper }
|
Self { lower, upper }
|
||||||
@@ -65,7 +71,8 @@ impl HsvRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 校验下界不能大于上界
|
// 2. 校验下界不能大于上界
|
||||||
if self.lower.0 > self.upper.0 || self.lower.1 > self.upper.1 || self.lower.2 > self.upper.2 {
|
if self.lower.0 > self.upper.0 || self.lower.1 > self.upper.1 || self.lower.2 > self.upper.2
|
||||||
|
{
|
||||||
return Err("HSV范围下界不能大于上界".to_string());
|
return Err("HSV范围下界不能大于上界".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,18 +101,51 @@ impl ColorPreset {
|
|||||||
pub fn matches(&self) -> &[HsvRange] {
|
pub fn matches(&self) -> &[HsvRange] {
|
||||||
match self {
|
match self {
|
||||||
ColorPreset::Red => &[
|
ColorPreset::Red => &[
|
||||||
HsvRange { lower: (0, 50, 50), upper: (10, 255, 255) },
|
HsvRange {
|
||||||
HsvRange { lower: (170, 50, 50), upper: (180, 255, 255) },
|
lower: (0, 50, 50),
|
||||||
|
upper: (10, 255, 255),
|
||||||
|
},
|
||||||
|
HsvRange {
|
||||||
|
lower: (170, 50, 50),
|
||||||
|
upper: (180, 255, 255),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
ColorPreset::Blue => &[HsvRange { lower: (100, 50, 50), upper: (130, 255, 255) }],
|
ColorPreset::Blue => &[HsvRange {
|
||||||
ColorPreset::Green => &[HsvRange { lower: (40, 50, 50), upper: (80, 255, 255) }],
|
lower: (100, 50, 50),
|
||||||
ColorPreset::Yellow => &[HsvRange { lower: (20, 50, 50), upper: (40, 255, 255) }],
|
upper: (130, 255, 255),
|
||||||
ColorPreset::Orange => &[HsvRange { lower: (10, 50, 50), upper: (20, 255, 255) }],
|
}],
|
||||||
ColorPreset::Purple => &[HsvRange { lower: (130, 50, 50), upper: (170, 255, 255) }],
|
ColorPreset::Green => &[HsvRange {
|
||||||
ColorPreset::Cyan => &[HsvRange { lower: (80, 50, 50), upper: (100, 255, 255) }],
|
lower: (40, 50, 50),
|
||||||
ColorPreset::Black => &[HsvRange { lower: (0, 0, 0), upper: (180, 255, 50) }],
|
upper: (80, 255, 255),
|
||||||
ColorPreset::White => &[HsvRange { lower: (0, 0, 200), upper: (180, 30, 255) }],
|
}],
|
||||||
ColorPreset::Gray => &[HsvRange { lower: (0, 0, 50), upper: (180, 30, 200) }],
|
ColorPreset::Yellow => &[HsvRange {
|
||||||
|
lower: (20, 50, 50),
|
||||||
|
upper: (40, 255, 255),
|
||||||
|
}],
|
||||||
|
ColorPreset::Orange => &[HsvRange {
|
||||||
|
lower: (10, 50, 50),
|
||||||
|
upper: (20, 255, 255),
|
||||||
|
}],
|
||||||
|
ColorPreset::Purple => &[HsvRange {
|
||||||
|
lower: (130, 50, 50),
|
||||||
|
upper: (170, 255, 255),
|
||||||
|
}],
|
||||||
|
ColorPreset::Cyan => &[HsvRange {
|
||||||
|
lower: (80, 50, 50),
|
||||||
|
upper: (100, 255, 255),
|
||||||
|
}],
|
||||||
|
ColorPreset::Black => &[HsvRange {
|
||||||
|
lower: (0, 0, 0),
|
||||||
|
upper: (180, 255, 50),
|
||||||
|
}],
|
||||||
|
ColorPreset::White => &[HsvRange {
|
||||||
|
lower: (0, 0, 200),
|
||||||
|
upper: (180, 30, 255),
|
||||||
|
}],
|
||||||
|
ColorPreset::Gray => &[HsvRange {
|
||||||
|
lower: (0, 0, 50),
|
||||||
|
upper: (180, 30, 200),
|
||||||
|
}],
|
||||||
ColorPreset::Custom(ranges) => ranges,
|
ColorPreset::Custom(ranges) => ranges,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +244,6 @@ impl ColorFilter for ColorPreset {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 多路颜色“或”逻辑组合子(并集网络)
|
/// 多路颜色“或”逻辑组合子(并集网络)
|
||||||
pub struct MultiOrColorRestrict<'a> {
|
pub struct MultiOrColorRestrict<'a> {
|
||||||
pub filters: Vec<&'a dyn ColorFilter>,
|
pub filters: Vec<&'a dyn ColorFilter>,
|
||||||
@@ -246,4 +285,3 @@ macro_rules! color_any_of {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,25 +1,23 @@
|
|||||||
use crate::charset::{TokenFilter, ValidationCtx};
|
use crate::models::ocr::metadata::Resize;
|
||||||
use crate::model_metadata::{ModelMetadata, Resize};
|
|
||||||
use crate::models::base::ModelArgs;
|
use crate::models::ocr::color_filter::{HsvRange, apply_to_image};
|
||||||
use crate::models::loader::{ModelLoader, ModelSession, ModelType};
|
// use ddddocr_tract::session::{ModelOutput, OcrSession};
|
||||||
use crate::utils::color_filter::{ColorFilter, HsvRange, filter_image};
|
|
||||||
use crate::utils::image_io::png_rgba_white_preprocess;
|
use crate::utils::image_io::png_rgba_white_preprocess;
|
||||||
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
||||||
use anyhow::Context;
|
use anyhow::Result;
|
||||||
use anyhow::{Result, anyhow};
|
use image::DynamicImage;
|
||||||
use image::{DynamicImage, ImageBuffer, Rgb};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use tract_onnx::prelude::tract_ndarray::{ArrayView2, Ix2, s};
|
// use tract_onnx::prelude::tract_ndarray::{ Ix2, s};
|
||||||
use tract_onnx::prelude::{
|
// use tract_onnx::prelude::{DatumType, Tensor, tract_ndarray};
|
||||||
DatumType, Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp, tract_ndarray, tvec,
|
// !!!【核心纠正】:彻底弃用 tract_ndarray,全线转用标准 ndarray
|
||||||
};
|
use ndarray::ArrayView2;
|
||||||
// 引入 cv_ops 模块中的 OpenCV HSV 转换算子
|
// pub enum ModelOutput {
|
||||||
use crate::utils::cv_ops::rgb_to_opencv_hsv;
|
// Indices(ndarray::Array1<i64>), // 拥有完整所有权的 1维数组,可任意传递和返回
|
||||||
|
// Logits(ndarray::Array2<f32>), // 拥有完整所有权的 2维矩阵,可任意传递和返回
|
||||||
/// 推理最终输出的强类型外壳(完全 Owned,无任何生命周期,可直接转 JSON)
|
// }
|
||||||
|
use crate::{OcrEngine, OcrOutput};
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub enum OcrResult {
|
pub enum OcrResult {
|
||||||
/// 纯文本分支(对应 probability = false)
|
/// 纯文本分支(对应 probability = false)
|
||||||
@@ -104,103 +102,40 @@ impl fmt::Display for OcrResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Ocr {
|
pub struct Ocr<'a> {
|
||||||
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
pub(crate) session: &'a dyn OcrEngine,
|
||||||
pub model_metadata: ModelMetadata,
|
pub(crate) png_fix: bool,
|
||||||
}
|
pub(crate) probability: bool,
|
||||||
impl ModelSession for Ocr {
|
|
||||||
fn get_model_type(&self) -> ModelType {
|
|
||||||
todo!("使用thiserror作为错误处理的库,thiserror 专门用于开发库(Library)");
|
|
||||||
}
|
|
||||||
fn desc(&self) -> String {
|
|
||||||
"Ocr Model 加载成功".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Ocr {
|
|
||||||
pub fn new(model_path: String, model_metadata: ModelMetadata) -> Result<Self, anyhow::Error> {
|
|
||||||
|
|
||||||
let session = ModelLoader::load_model(&model_path)?.session;
|
|
||||||
Ok(Self {
|
|
||||||
session,
|
|
||||||
model_metadata,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/// 对应 Python 的 _inference
|
|
||||||
fn inference(&self, tensor: Tensor) -> anyhow::Result<Tensor> {
|
|
||||||
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
|
|
||||||
// let result = self.session.run(tvec!(tensor.into()))?;
|
|
||||||
let mut result = self
|
|
||||||
.session
|
|
||||||
.run(tvec!(tensor.into()))
|
|
||||||
.context("执行模型推理失败")?;
|
|
||||||
println!("模型输出原始数据: {:?}", result);
|
|
||||||
Ok(result.swap_remove(0).into_tensor())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn predictor(&'_ self) -> OcrPredictor<'_> {
|
|
||||||
OcrPredictor::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OcrPredictor<'a> {
|
|
||||||
ocr: &'a Ocr,
|
|
||||||
/// 是否修复PNG格式问题
|
|
||||||
png_fix: bool,
|
|
||||||
/// 是否返回概率信息
|
|
||||||
probability: bool,
|
|
||||||
/// 颜色过滤:保留的颜色列表
|
/// 颜色过滤:保留的颜色列表
|
||||||
color_filter: Result<Option<Vec<HsvRange>>, String>,
|
pub(crate) final_color_ranges: Result<Option<Vec<HsvRange>>, String>,
|
||||||
|
|
||||||
/// 字符集范围
|
/// 字符集范围
|
||||||
charset_restrict: Option<Vec<usize>>,
|
pub(crate) final_charset_indices: Option<Vec<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> OcrPredictor<'a> {
|
impl<'a> Ocr<'a> {
|
||||||
// 初始化任务,设置默认参数
|
// 初始化任务,设置默认参数
|
||||||
pub fn new(ocr: &'a Ocr) -> Self {
|
|
||||||
Self {
|
pub fn new(session: &'a dyn OcrEngine) -> Self {
|
||||||
ocr,
|
Ocr {
|
||||||
|
session,
|
||||||
png_fix: false, // 默认值
|
png_fix: false, // 默认值
|
||||||
probability: false,
|
probability: false,
|
||||||
color_filter: Ok(None),
|
final_color_ranges: Ok(None),
|
||||||
charset_restrict: None,
|
final_charset_indices: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn png_fix(mut self, value: bool) -> Self {
|
|
||||||
self.png_fix = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn probability(mut self, value: bool) -> Self {
|
|
||||||
self.probability = value;
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
impl<'a> Ocr<'a> {
|
||||||
|
pub fn predict(&self, image: &DynamicImage) -> anyhow::Result<OcrResult> {
|
||||||
|
println!("当前颜色过滤器状态: {:?}", self.final_color_ranges);
|
||||||
|
|
||||||
pub fn color_filter(mut self, filter: &dyn ColorFilter) -> Self {
|
|
||||||
// 一句话把活全包了!错误信息无缝传递,完美熔断
|
|
||||||
match filter.collect_to_vec() {
|
|
||||||
Ok(new_ranges) => self.color_filter = Ok(new_ranges),
|
|
||||||
Err(err_msg) => self.color_filter = Err(err_msg), // 校验失败,Builder 正式中毒
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn charset_restrict(mut self, restrict: &dyn TokenFilter) -> Self {
|
|
||||||
let charset = &self.ocr.model_metadata.charset;
|
|
||||||
let tokens = &charset.tokens;
|
|
||||||
self.charset_restrict = restrict.apply_to_charset(tokens);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> OcrPredictor<'a> {
|
|
||||||
pub fn predict(self, image: &DynamicImage) -> anyhow::Result<OcrResult> {
|
|
||||||
println!("当前颜色过滤器状态: {:?}", self.color_filter);
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 管道节点 1: 颜色过滤流水线
|
// 管道节点 1: 颜色过滤流水线
|
||||||
// 使用 Cow (Copy-On-Write) 智能指针。
|
// 使用 Cow (Copy-On-Write) 智能指针。
|
||||||
// 如果未开启过滤,img_cow 内部只是持有原图的【只读借用】,发生【零内存分配】!
|
// 如果未开启过滤,img_cow 内部只是持有原图的【只读借用】,发生【零内存分配】!
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
let img_cow = match &self.color_filter {
|
let img_cow = match &self.final_color_ranges {
|
||||||
Err(err_msg) => {
|
Err(err_msg) => {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"颜色过滤器初始化失败,全链路短路: {}",
|
"颜色过滤器初始化失败,全链路短路: {}",
|
||||||
@@ -213,34 +148,34 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
}
|
}
|
||||||
Ok(Some(ranges)) => {
|
Ok(Some(ranges)) => {
|
||||||
// 只有真正需要过滤时,才在内部提取像素并生成清洗后的 Owned 新图
|
// 只有真正需要过滤时,才在内部提取像素并生成清洗后的 Owned 新图
|
||||||
let filtered_img = filter_image(image, ranges)?;
|
let filtered_img = apply_to_image(image, ranges)?;
|
||||||
Cow::Owned(filtered_img)
|
Cow::Owned(filtered_img)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let tensor = self.preprocess_image(&img_cow)?;
|
let tensor = self.preprocess_image(&img_cow)?;
|
||||||
|
|
||||||
let raw_tensor = self.ocr.inference(tensor)?;
|
let raw_tensor = self.session.inference(tensor)?;
|
||||||
|
|
||||||
// 3. 后处理分流:直接返回 OcrResult
|
// 3. 后处理分流:直接返回 OcrResult
|
||||||
let ocr_output = match raw_tensor.datum_type() {
|
// let ocr_output = match raw_tensor.datum_type() {
|
||||||
DatumType::I64 => self.process_i64_tensor(raw_tensor)?,
|
// DatumType::I64 => self.process_i64_tensor(raw_tensor)?,
|
||||||
DatumType::F32 => self.process_f32_tensor(raw_tensor)?,
|
// DatumType::F32 => self.process_f32_tensor(raw_tensor)?,
|
||||||
_ => OcrResult::Unsupported {
|
// _ => OcrResult::Unsupported {
|
||||||
message: format!("不支持的模型输出数据类型: {:?}", raw_tensor.datum_type()),
|
// message: format!("不支持的模型输出数据类型: {:?}", raw_tensor.datum_type()),
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
|
|
||||||
// let raw_indices = self.ocr.extract_indices_from_tensor(&raw_tensor)?;
|
// let raw_indices = self.ocr.extract_indices_from_tensor(&raw_tensor)?;
|
||||||
// // 步骤 2: 将索引切片 `&[i64]` 传给解码器进行 CTC 去重和字符映射
|
// // 步骤 2: 将索引切片 `&[i64]` 传给解码器进行 CTC 去重和字符映射
|
||||||
// let final_text = self.ctc_decode_to_string(&raw_indices);
|
// let final_text = self.ctc_decode_to_string(&raw_indices);
|
||||||
|
let ocr_output = self.process_model_output(raw_tensor);
|
||||||
Ok(ocr_output)
|
ocr_output
|
||||||
}
|
}
|
||||||
/// 对应 Python 的 _preprocess_image
|
/// 对应 Python 的 _preprocess_image
|
||||||
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
|
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
|
||||||
fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result<Tensor> {
|
fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result<ndarray::Array4<f32>> {
|
||||||
// 1. 获取模型元数据配置
|
// 1. 获取模型元数据配置
|
||||||
let meta = &self.ocr.model_metadata;
|
let meta = self.session.metadata();
|
||||||
let norm = &meta.normalization; // 获取归一化器
|
let norm = &meta.normalization; // 获取归一化器
|
||||||
|
|
||||||
// A. 修复 PNG 透明背景 (内部逻辑你之前已实现)
|
// A. 修复 PNG 透明背景 (内部逻辑你之前已实现)
|
||||||
@@ -270,12 +205,12 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
let resized_img = resize_image(¤t_img, target_w, target_h);
|
let resized_img = resize_image(¤t_img, target_w, target_h);
|
||||||
|
|
||||||
// 4. 管道节点 3: 颜色通道转换(单通道灰度 vs 三通道 RGB)与 4D 张量填充
|
// 4. 管道节点 3: 颜色通道转换(单通道灰度 vs 三通道 RGB)与 4D 张量填充
|
||||||
let tensor = match meta.channel {
|
let array4 = match meta.channel {
|
||||||
// --- 情况 A: 单通道(灰度图),对应 Python 的 len(shape) == 2 展开 ---
|
// --- 情况 A: 单通道(灰度图),对应 Python 的 len(shape) == 2 展开 ---
|
||||||
1 => {
|
1 => {
|
||||||
let gray_img = convert_to_grayscale(&resized_img);
|
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),
|
(1, 1, target_h as usize, target_w as usize),
|
||||||
|(_, _, y, x)| {
|
|(_, _, y, x)| {
|
||||||
let pixel = gray_img.get_pixel(x as u32, y as u32)[0] as f32;
|
let pixel = gray_img.get_pixel(x as u32, y as u32)[0] as f32;
|
||||||
@@ -284,14 +219,14 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
norm.normalize(pixel)
|
norm.normalize(pixel)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Tensor::from(array)
|
array
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 情况 B: 三通道(RGB),对应 Python 的 transpose(2, 0, 1) 的 CHW 布局 ---
|
// --- 情况 B: 三通道(RGB),对应 Python 的 transpose(2, 0, 1) 的 CHW 布局 ---
|
||||||
3 => {
|
3 => {
|
||||||
let rgb_img = resized_img.to_rgb8();
|
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),
|
(1, 3, target_h as usize, target_w as usize),
|
||||||
|(_, c, y, x)| {
|
|(_, c, y, x)| {
|
||||||
let pixel = rgb_img.get_pixel(x as u32, y as u32)[c] as f32;
|
let pixel = rgb_img.get_pixel(x as u32, y as u32)[c] as f32;
|
||||||
@@ -300,13 +235,14 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
norm.normalize(pixel)
|
norm.normalize(pixel)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Tensor::from(array)
|
// Tensor::from(array)
|
||||||
|
array
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => return Err(anyhow::anyhow!("不支持的通道数配置: {}", meta.channel)),
|
_ => return Err(anyhow::anyhow!("不支持的通道数配置: {}", meta.channel)),
|
||||||
};
|
};
|
||||||
|
Ok(array4)
|
||||||
Ok(tensor)
|
// Ok(tensor)
|
||||||
|
|
||||||
// let h = 64u32;
|
// let h = 64u32;
|
||||||
// let w = (current_img.width() as f32 * (h as f32 / current_img.height() as f32)) as u32;
|
// let w = (current_img.width() as f32 * (h as f32 / current_img.height() as f32)) as u32;
|
||||||
@@ -327,14 +263,65 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
//
|
//
|
||||||
// Ok(tensor)
|
// 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))
|
||||||
}
|
}
|
||||||
impl<'a> OcrPredictor<'a> {
|
}
|
||||||
|
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 {
|
fn is_valid_indices(&self, idx: usize) -> bool {
|
||||||
if idx >= self.ocr.model_metadata.charset.size() {
|
if idx >= self.session.metadata().charset.size() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
match &self.charset_restrict {
|
match &self.final_charset_indices {
|
||||||
Some(v) => v.binary_search(&idx).is_ok(),
|
Some(v) => v.binary_search(&idx).is_ok(),
|
||||||
None => true,
|
None => true,
|
||||||
}
|
}
|
||||||
@@ -342,9 +329,9 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
/// 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印
|
/// 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印
|
||||||
/// 这里的 &str 完美借用了自 tokens,依然是彻底的零拷贝!
|
/// 这里的 &str 完美借用了自 tokens,依然是彻底的零拷贝!
|
||||||
pub fn valid_tokens(&self) -> Vec<&str> {
|
pub fn valid_tokens(&self) -> Vec<&str> {
|
||||||
let charset = &self.ocr.model_metadata.charset;
|
let charset = &self.session.metadata().charset;
|
||||||
let tokens = &charset.tokens;
|
let tokens = &charset.tokens;
|
||||||
match &self.charset_restrict {
|
match &self.final_charset_indices {
|
||||||
Some(indices) => indices
|
Some(indices) => indices
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&idx| tokens.get(idx).map(|cow| cow.as_ref()))
|
.filter_map(|&idx| tokens.get(idx).map(|cow| cow.as_ref()))
|
||||||
@@ -354,9 +341,9 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn valid_size(&self) -> usize {
|
pub fn valid_size(&self) -> usize {
|
||||||
match &self.charset_restrict {
|
match &self.final_charset_indices {
|
||||||
Some(indices) => indices.len(),
|
Some(indices) => indices.len(),
|
||||||
None => self.ocr.model_metadata.charset.tokens.len(),
|
None => self.session.metadata().charset.tokens.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 变体 B 核心处理器:单次遍历 2D 视图,融合计算 Softmax、Argmax、置信度并输出概率大包
|
/// 变体 B 核心处理器:单次遍历 2D 视图,融合计算 Softmax、Argmax、置信度并输出概率大包
|
||||||
@@ -368,7 +355,7 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
let classes = matrix_view.ncols();
|
let classes = matrix_view.ncols();
|
||||||
|
|
||||||
// 1. 预分配满额概率矩阵内存
|
// 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 predicted_indices = Vec::with_capacity(steps);
|
||||||
let mut confidence_sum = 0.0f32;
|
let mut confidence_sum = 0.0f32;
|
||||||
|
|
||||||
@@ -413,98 +400,98 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
(probabilities_list, confidence, predicted_indices)
|
(probabilities_list, confidence, predicted_indices)
|
||||||
}
|
}
|
||||||
/// 变体 A 专属提取器:直接从 I64 Tensor 零拷贝提取 CTC 文本与初始概率包
|
/// 变体 A 专属提取器:直接从 I64 Tensor 零拷贝提取 CTC 文本与初始概率包
|
||||||
fn process_i64_tensor(&self, raw_tensor: Tensor) -> anyhow::Result<OcrResult> {
|
// fn process_i64_tensor(&self, raw_tensor: Tensor) -> anyhow::Result<OcrResult> {
|
||||||
// 1. 拿到底层的动态维度只读视图
|
// // 1. 拿到底层的动态维度只读视图
|
||||||
let view = raw_tensor.to_array_view::<i64>()?;
|
// let view = raw_tensor.to_array_view::<i64>()?;
|
||||||
|
//
|
||||||
// 2. 索要底层连续的只读切片引用
|
// // 2. 索要底层连续的只读切片引用
|
||||||
let slice = view
|
// let slice = view
|
||||||
.as_slice()
|
// .as_slice()
|
||||||
.ok_or_else(|| anyhow::anyhow!("I64 模型输出内存不连续,无法执行零拷贝解码"))?;
|
// .ok_or_else(|| anyhow::anyhow!("I64 模型输出内存不连续,无法执行零拷贝解码"))?;
|
||||||
|
//
|
||||||
// 3. 直接喂给 CTC 解码器(无任何物理克隆开销)
|
// // 3. 直接喂给 CTC 解码器(无任何物理克隆开销)
|
||||||
let final_text = self.ctc_decode_to_string(slice);
|
// let final_text = self.ctc_decode_to_string(slice);
|
||||||
|
//
|
||||||
// 4. 组装返回
|
// // 4. 组装返回
|
||||||
if self.probability {
|
// if self.probability {
|
||||||
Ok(OcrResult::Probability {
|
// Ok(OcrResult::Probability {
|
||||||
text: final_text,
|
// text: final_text,
|
||||||
probabilities: vec![], // I64 模型物理上丢失了全量 Logits 分值网,降级处理
|
// probabilities: vec![], // I64 模型物理上丢失了全量 Logits 分值网,降级处理
|
||||||
confidence: 1.0, // 判定即百分之百置信
|
// confidence: 1.0, // 判定即百分之百置信
|
||||||
})
|
// })
|
||||||
} else {
|
// } else {
|
||||||
Ok(OcrResult::Text(final_text))
|
// Ok(OcrResult::Text(final_text))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
/// 变体二(F32)的总体管线:负责降维,并分流文本和概率
|
// /// 变体二(F32)的总体管线:负责降维,并分流文本和概率
|
||||||
fn process_f32_tensor(&self, raw_tensor: Tensor) -> anyhow::Result<OcrResult> {
|
// fn process_f32_tensor(&self, raw_tensor: Tensor) -> anyhow::Result<OcrResult> {
|
||||||
let shape = raw_tensor.shape();
|
// let shape = raw_tensor.shape();
|
||||||
println!("模型输出shape数据: {:?}", shape);
|
// println!("模型输出shape数据: {:?}", shape);
|
||||||
let view = raw_tensor.to_array_view::<f32>()?;
|
// let view = raw_tensor.to_array_view::<f32>()?;
|
||||||
|
//
|
||||||
// 1. 极其纯粹的、无拷贝的多维 Shape 压扁清洗
|
// // 1. 极其纯粹的、无拷贝的多维 Shape 压扁清洗
|
||||||
let (steps, classes, data_dyn_view) = match shape.len() {
|
// let (steps, classes, data_dyn_view) = match shape.len() {
|
||||||
3 => {
|
// 3 => {
|
||||||
if shape[1] == 1 {
|
// if shape[1] == 1 {
|
||||||
// 形状: [Steps, 1, Classes] -> 你的原有逻辑
|
// // 形状: [Steps, 1, Classes] -> 你的原有逻辑
|
||||||
(shape[0], shape[2], view.into_dyn())
|
// (shape[0], shape[2], view.into_dyn())
|
||||||
} else if shape[0] == 1 {
|
// } else if shape[0] == 1 {
|
||||||
// 形状: [1, Steps, Classes] -> 另一种常见导出格式
|
// // 形状: [1, Steps, Classes] -> 另一种常见导出格式
|
||||||
(shape[1], shape[2], view.into_dyn())
|
// (shape[1], shape[2], view.into_dyn())
|
||||||
} else {
|
// } else {
|
||||||
// 默认取第一个 batch: [Batch, Steps, Classes]
|
// // 默认取第一个 batch: [Batch, Steps, Classes]
|
||||||
// 使用 slice 对应 Python 的 output[0, :, :]
|
// // 使用 slice 对应 Python 的 output[0, :, :]
|
||||||
let sliced = view.slice(s![0, .., ..]);
|
// let sliced = view.slice(s![0, .., ..]);
|
||||||
(shape[1], shape[2], sliced.into_dyn())
|
// (shape[1], shape[2], sliced.into_dyn())
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// 形状: [Steps, Classes] -> 已经剥离了 Batch 维度
|
// // 形状: [Steps, Classes] -> 已经剥离了 Batch 维度
|
||||||
2 => (shape[0], shape[1], view.into_dyn()),
|
// 2 => (shape[0], shape[1], view.into_dyn()),
|
||||||
// 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑)
|
// // 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑)
|
||||||
// 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
|
// // 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
|
||||||
1 => (1, shape[0], view.into_dyn()),
|
// 1 => (1, shape[0], view.into_dyn()),
|
||||||
_ => return Err(anyhow::anyhow!("不支持的输出维度: {:?}", shape)),
|
// _ => return Err(anyhow::anyhow!("不支持的输出维度: {:?}", shape)),
|
||||||
};
|
// };
|
||||||
let matrix_cow = data_dyn_view
|
// let matrix_cow = data_dyn_view
|
||||||
.to_shape(Ix2(steps, classes))
|
// .to_shape(Ix2(steps, classes))
|
||||||
.map_err(|e| anyhow::anyhow!("转换为2D静态矩阵失败: {:?}", e))?;
|
// .map_err(|e| anyhow::anyhow!("转换为2D静态矩阵失败: {:?}", e))?;
|
||||||
|
//
|
||||||
let matrix_view: ArrayView2<f32> = matrix_cow.view();
|
// let matrix_view: ArrayView2<f32> = matrix_cow.view();
|
||||||
|
//
|
||||||
// 2. 根据业务参数明确分流
|
// // 2. 根据业务参数明确分流
|
||||||
if self.probability {
|
// if self.probability {
|
||||||
// 走向 B1:调用刚刚拆分出来的“全量概率计算器”
|
// // 走向 B1:调用刚刚拆分出来的“全量概率计算器”
|
||||||
let (probabilities_list, confidence, predicted_indices) =
|
// let (probabilities_list, confidence, predicted_indices) =
|
||||||
self.compute_f32_full_probability(matrix_view);
|
// self.compute_f32_full_probability(matrix_view);
|
||||||
// 5. 执行 CTC 解码
|
// // 5. 执行 CTC 解码
|
||||||
let final_text = self.ctc_decode_to_string(&predicted_indices);
|
// let final_text = self.ctc_decode_to_string(&predicted_indices);
|
||||||
|
//
|
||||||
Ok(OcrResult::Probability {
|
// Ok(OcrResult::Probability {
|
||||||
text: final_text,
|
// text: final_text,
|
||||||
probabilities: probabilities_list,
|
// probabilities: probabilities_list,
|
||||||
confidence: confidence as f64,
|
// confidence: confidence as f64,
|
||||||
})
|
// })
|
||||||
} else {
|
// } else {
|
||||||
// 走向 B2:极速免 Softmax 提取纯文本(代码保持原地提取,简单短小不需要再拆)
|
// // 走向 B2:极速免 Softmax 提取纯文本(代码保持原地提取,简单短小不需要再拆)
|
||||||
let predicted_indices: Vec<i64> = matrix_view
|
// let predicted_indices: Vec<i64> = matrix_view
|
||||||
.outer_iter()
|
// .outer_iter()
|
||||||
.map(|row| {
|
// .map(|row| {
|
||||||
row.iter()
|
// row.iter()
|
||||||
.enumerate()
|
// .enumerate()
|
||||||
.max_by(|(_, a), (_, b)| a.total_cmp(b))
|
// .max_by(|(_, a), (_, b)| a.total_cmp(b))
|
||||||
.map(|(idx, _)| idx as i64)
|
// .map(|(idx, _)| idx as i64)
|
||||||
.unwrap_or(0)
|
// .unwrap_or(0)
|
||||||
})
|
// })
|
||||||
.collect();
|
// .collect();
|
||||||
|
//
|
||||||
let final_text = self.ctc_decode_to_string(&predicted_indices);
|
// let final_text = self.ctc_decode_to_string(&predicted_indices);
|
||||||
Ok(OcrResult::Text(final_text))
|
// Ok(OcrResult::Text(final_text))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
/// 获取有效字符索引列表 (用于外部验证或过滤)
|
/// 获取有效字符索引列表 (用于外部验证或过滤)
|
||||||
fn ctc_decode_to_string(&self, predicted_indices: &[i64]) -> String {
|
fn ctc_decode_to_string(&self, predicted_indices: &[i64]) -> String {
|
||||||
println!("indices模型输出原始数据: {:?}", predicted_indices);
|
println!("indices模型输出原始数据: {:?}", predicted_indices);
|
||||||
let charset = &self.ocr.model_metadata.charset;
|
let charset = &self.session.metadata().charset;
|
||||||
let tokens = &charset.tokens;
|
let tokens = &charset.tokens;
|
||||||
// let valid_indices = &charset.valid_indices;
|
// let valid_indices = &charset.valid_indices;
|
||||||
|
|
||||||
@@ -532,7 +519,7 @@ impl<'a> OcrPredictor<'a> {
|
|||||||
|
|
||||||
// 史诗级加速点:如果是 None,说明没限制,根本不进入分支,直接放行!
|
// 史诗级加速点:如果是 None,说明没限制,根本不进入分支,直接放行!
|
||||||
// 只有当有具体限制(Some)时,才去跑 4-5 次 CPU 寄存器级别的二分查找
|
// 只有当有具体限制(Some)时,才去跑 4-5 次 CPU 寄存器级别的二分查找
|
||||||
if let Some(ref indices) = self.charset_restrict {
|
if let Some(ref indices) = self.final_charset_indices {
|
||||||
if indices.binary_search(&u_idx).is_err() {
|
if indices.binary_search(&u_idx).is_err() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,72 @@
|
|||||||
use crate::charset::{CHARSET_BETA, CHARSET_OLD, Charset};
|
use anyhow::{anyhow, Result};
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
// ==========================================
|
||||||
use std::path::Path;
|
// 3. 字符集核心结构体 (重命名为 Charset)
|
||||||
|
// ==========================================
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Charset {
|
||||||
|
// 使用 Cow 统一静态切片和动态读取的 Vec<String>,内部实现真正的零拷贝
|
||||||
|
pub tokens: Vec<Cow<'static, str>>,
|
||||||
|
// 反向查找表,保证字符转索引为 O(1)
|
||||||
|
pub char_to_idx: HashMap<Cow<'static, str>, usize>,
|
||||||
|
// 当前处于激活状态的有效索引缓存 (用于 CTC 解码前的过滤加速)
|
||||||
|
// pub valid_indices: HashSet<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Charset {
|
||||||
|
// 内部底层统一收拢构造
|
||||||
|
pub fn new(tokens: Vec<Cow<'static, str>>) -> Self {
|
||||||
|
let mut char_to_idx = HashMap::with_capacity(tokens.len());
|
||||||
|
for (idx, token) in tokens.iter().enumerate() {
|
||||||
|
char_to_idx.entry(token.clone()).or_insert(idx);
|
||||||
|
// 如果字符集有重复,保留第一个遇到的索引 (符合 Python .index 逻辑)
|
||||||
|
// char_to_idx.entry(token.to_string()).or_insert(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
tokens,
|
||||||
|
char_to_idx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 业务策略方法 ---
|
||||||
|
|
||||||
|
/// 将字符转为索引,不存在返回 -1 (保持与原 Python 库行为一致)
|
||||||
|
pub fn char_to_index(&self, char_str: &str) -> i32 {
|
||||||
|
if let Some(&idx) = self.char_to_idx.get(char_str) {
|
||||||
|
idx as i32
|
||||||
|
} else {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将索引转为字符引用,零拷贝。若越界返回 None
|
||||||
|
pub fn index_to_char_ref(&self, index: usize) -> Option<&str> {
|
||||||
|
self.tokens.get(index).map(|cow| cow.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_valid_char(&self, char_str: &str) -> bool {
|
||||||
|
self.char_to_idx.get(char_str).is_some()
|
||||||
|
}
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
self.tokens.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. 标准 Display 接口实现 (对应 __str__)
|
||||||
|
// ==========================================
|
||||||
|
impl std::fmt::Display for Charset {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Charset [Total Size: {}", self.size(),)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 1. 辅助定义的枚举与结构体
|
// 1. 辅助定义的枚举与结构体
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -74,28 +135,6 @@ pub struct ModelMetadata {
|
|||||||
|
|
||||||
impl ModelMetadata {
|
impl ModelMetadata {
|
||||||
// --- 优雅的工厂模式构造器 ---
|
// --- 优雅的工厂模式构造器 ---
|
||||||
/// 从预设的旧版字符集创建
|
|
||||||
pub fn from_builtin_old() -> Self {
|
|
||||||
Self::from_static_slice(
|
|
||||||
CHARSET_OLD,
|
|
||||||
false,
|
|
||||||
Resize::DynamicWidth(64),
|
|
||||||
1,
|
|
||||||
Normalization::ZeroToOne,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从预设的 Beta 版字符集创建
|
|
||||||
pub fn from_builtin_beta() -> Self {
|
|
||||||
Self::from_static_slice(
|
|
||||||
CHARSET_BETA,
|
|
||||||
false,
|
|
||||||
Resize::DynamicWidth(64),
|
|
||||||
1,
|
|
||||||
Normalization::MinusOneToOne,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 通用的静态切片转换构造器
|
/// 通用的静态切片转换构造器
|
||||||
pub fn from_static_slice(
|
pub fn from_static_slice(
|
||||||
slice: &[&'static str],
|
slice: &[&'static str],
|
||||||
@@ -113,19 +152,8 @@ impl ModelMetadata {
|
|||||||
normalization,
|
normalization,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn from_json_str(json_str: &str) -> Result<Self> {
|
||||||
/// 从外部外部 JSON 文件动态加载字符集
|
let dto: ModelMetadataDto = serde_json::from_str(json_str)
|
||||||
pub fn from_json_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
if !path.exists() {
|
|
||||||
return Err(anyhow!("模型元数据配置文件不存在: {:?}", path));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = File::open(path)?;
|
|
||||||
let mut content = String::new();
|
|
||||||
file.read_to_string(&mut content)?;
|
|
||||||
|
|
||||||
let dto: ModelMetadataDto = serde_json::from_str(&content)
|
|
||||||
.map_err(|e| anyhow!("JSON 反序列化失败,请检查字段是否完整: {}", e))?;
|
.map_err(|e| anyhow!("JSON 反序列化失败,请检查字段是否完整: {}", e))?;
|
||||||
|
|
||||||
// 1. 将 DTO 的字符串数组转化为强类型的 Charset
|
// 1. 将 DTO 的字符串数组转化为强类型的 Charset
|
||||||
@@ -163,4 +191,10 @@ impl ModelMetadata {
|
|||||||
normalization: dto.normalization,
|
normalization: dto.normalization,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/// 机制 2:从内存字节流加载(极大地方便 include_bytes! 或网络下载)
|
||||||
|
pub fn from_json_bytes(bytes: &[u8]) -> Result<Self> {
|
||||||
|
let json_str = std::str::from_utf8(bytes)
|
||||||
|
.map_err(|e| anyhow!("JSON 字节流不是合法的 UTF-8 编码: {}", e))?;
|
||||||
|
Self::from_json_str(json_str)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
9
ddddocr-core/src/models/ocr/mod.rs
Normal file
9
ddddocr-core/src/models/ocr/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod builder;
|
||||||
|
mod executor;
|
||||||
|
pub mod metadata;
|
||||||
|
pub mod color_filter;
|
||||||
|
mod token_filter;
|
||||||
|
|
||||||
|
pub use builder::OcrBuilder;
|
||||||
|
pub use executor::{Ocr, OcrResult};
|
||||||
|
// pub use ddddocr_tract::session::OcrSession;
|
||||||
146
ddddocr-core/src/models/ocr/token_filter.rs
Normal file
146
ddddocr-core/src/models/ocr/token_filter.rs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
/// 字符集范围限制枚举
|
||||||
|
pub struct ValidationCtx<'a> {
|
||||||
|
pub text: &'a str, // 当前 Token 的文本内容
|
||||||
|
pub token_id: usize, // 当前 Token 的 ID 索引
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 统一的约束接口
|
||||||
|
pub trait TokenFilter {
|
||||||
|
fn matches(&self, ctx: &ValidationCtx) -> bool;
|
||||||
|
/// 预估容量提示,帮助精准开辟 Vec 内存
|
||||||
|
fn estimated_capacity(&self) -> usize {
|
||||||
|
128
|
||||||
|
}
|
||||||
|
/// 【新引入的架构级核心方法】
|
||||||
|
/// 统一接管全量字符集的密集遍历、CTC Blank放行、去重、排序及空交集退化兜底
|
||||||
|
fn apply_to_charset(&self, tokens: &[Cow<str>]) -> Option<Vec<usize>> {
|
||||||
|
let mut has_any_match = false;
|
||||||
|
let estimated_capacity = self.estimated_capacity();
|
||||||
|
|
||||||
|
// 1. 精准开辟内存,完美利用容量提示,避免动态乱涨
|
||||||
|
let mut temp_indices = Vec::with_capacity(estimated_capacity.max(16));
|
||||||
|
|
||||||
|
// 2. 高性能原地单次流式迭代
|
||||||
|
for (idx, token) in tokens.iter().enumerate() {
|
||||||
|
let token_str = token.as_ref();
|
||||||
|
|
||||||
|
// 规则 A: CTC Blank 空字符串或 0 号索引无条件放行
|
||||||
|
if token_str.is_empty() || idx == 0 {
|
||||||
|
temp_indices.push(idx);
|
||||||
|
continue; // 关键:直接跳过,防止后续 matches 匹配成功导致重复 push 产生 Bug
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规则 B: 组装无拷贝上下文
|
||||||
|
let ctx = ValidationCtx {
|
||||||
|
text: token_str,
|
||||||
|
token_id: idx,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 规则 C: 路由到各自具体实现的特异性匹配中(如 Digit 判定、TopN 判定、组合子判定等)
|
||||||
|
if self.matches(&ctx) {
|
||||||
|
temp_indices.push(idx);
|
||||||
|
has_any_match = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 终极防御:如果整个模型字符集除了 Blank,一个都没对上,直接退化为 None(全量识别)
|
||||||
|
if !has_any_match {
|
||||||
|
println!("警告:当前限制策略与模型字符集完全没有交集!已自动恢复全量识别。");
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// 4. 排序并去重,为 Ocr 引擎后续进行极其高频的『二分查找』筑起绝对安全的底层保障
|
||||||
|
temp_indices.sort_unstable();
|
||||||
|
temp_indices.dedup();
|
||||||
|
Some(temp_indices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CharRestrict {
|
||||||
|
Digit,
|
||||||
|
Lowercase,
|
||||||
|
Uppercase,
|
||||||
|
CustomList(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenFilter for CharRestrict {
|
||||||
|
fn matches(&self, ctx: &ValidationCtx) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Digit => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_digit(),
|
||||||
|
Self::Lowercase => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_lowercase(),
|
||||||
|
Self::Uppercase => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_uppercase(),
|
||||||
|
Self::CustomList(vec) => vec.iter().any(|t| t == ctx.text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn estimated_capacity(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Digit => 16,
|
||||||
|
Self::Lowercase | Self::Uppercase => 32,
|
||||||
|
Self::CustomList(vec) => vec.len() + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum IdRestrict {
|
||||||
|
TopN(usize),
|
||||||
|
IdRange(std::ops::Range<usize>),
|
||||||
|
IdList(Vec<usize>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenFilter for IdRestrict {
|
||||||
|
fn matches(&self, ctx: &ValidationCtx) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::TopN(n) => ctx.token_id < *n,
|
||||||
|
Self::IdRange(range) => range.contains(&ctx.token_id),
|
||||||
|
Self::IdList(vec) => vec.contains(&ctx.token_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn estimated_capacity(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::TopN(n) => *n + 1,
|
||||||
|
// 2. IdRange:标准标准库 Range 的长度
|
||||||
|
// 注意:因为范围可能是 1000..2000,它的 len() 返回的是 usize
|
||||||
|
Self::IdRange(range) => range.len() + 1,
|
||||||
|
// 3. IdList:Vec 里的元素个数
|
||||||
|
Self::IdList(vec) => vec.len() + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 多路“或”逻辑组合子(支持 N 个规则无缝并集)
|
||||||
|
pub struct MultiOrRestrict<'a> {
|
||||||
|
pub filters: Vec<&'a dyn TokenFilter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TokenFilter for MultiOrRestrict<'a> {
|
||||||
|
fn matches(&self, ctx: &ValidationCtx) -> bool {
|
||||||
|
// 核心高阶函数:只要有一个过滤器命中,该 Token 即可放行
|
||||||
|
self.filters.iter().any(|f| f.matches(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimated_capacity(&self) -> usize {
|
||||||
|
// 将所有过滤器的预估容量累加,作为最终容量参考
|
||||||
|
self.filters.iter().map(|f| f.estimated_capacity()).sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// =====================================================================
|
||||||
|
// 声明式宏:替代 `+` 运算符,解决组合扩展痛苦
|
||||||
|
// =====================================================================
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! any_of {
|
||||||
|
// 场景 A:如果用户只传了一个规则,免去构建 Vec 的开销,直接返回其引用
|
||||||
|
($only:expr) => {
|
||||||
|
&$only as &dyn $crate::TokenFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
// 场景 B:如果用户传入了多个规则,自动织成一张静态组合网
|
||||||
|
($($filter:expr),+ $(,)?) => {
|
||||||
|
&$crate::MultiOrRestrict {
|
||||||
|
filters: vec![ $( &$filter as &dyn $crate::TokenFilter ),+ ]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ use base64::{Engine as _, engine::general_purpose};
|
|||||||
use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, Luma, Rgb, RgbImage, Rgba};
|
use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, Luma, Rgb, RgbImage, Rgba};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tract_onnx::prelude::tract_ndarray::{Array3, ArrayD, ArrayViewD};
|
use ndarray::{Array3, ArrayD, ArrayViewD};
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ColorMode {
|
pub enum ColorMode {
|
||||||
RGB,
|
RGB,
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
use image::{ImageBuffer, Luma};
|
use image::{ImageBuffer, Luma};
|
||||||
|
use ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip};
|
||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min};
|
||||||
use tract_onnx::prelude::tract_ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip};
|
|
||||||
|
|
||||||
|
// 模拟openCV
|
||||||
/// 1. 计算两个数组的绝对差值 (对应 cv2.absdiff)
|
/// 1. 计算两个数组的绝对差值 (对应 cv2.absdiff)
|
||||||
pub fn abs_diff(a: &ArrayView3<u8>, b: &ArrayView3<u8>) -> Array3<u8> {
|
pub fn abs_diff(a: &ArrayView3<u8>, b: &ArrayView3<u8>) -> Array3<u8> {
|
||||||
// 利用 ndarray 的 map_collect,生成差值的绝对值数组
|
// 利用 ndarray 的 map_collect,生成差值的绝对值数组
|
||||||
@@ -72,6 +73,9 @@ pub fn find_contours_and_max(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>) -> Opt
|
|||||||
Some(max_label)
|
Some(max_label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// 根据目标连通域标签,计算其在图像中的外接矩形边界框(对应 `cv2.boundingRect`)
|
||||||
|
///
|
||||||
|
/// 返回格式: `(min_x, min_y, width, height)`
|
||||||
pub fn bounding_rect(
|
pub fn bounding_rect(
|
||||||
labelled: &ImageBuffer<Luma<u32>, Vec<u32>>,
|
labelled: &ImageBuffer<Luma<u32>, Vec<u32>>,
|
||||||
max_label: u32,
|
max_label: u32,
|
||||||
@@ -95,13 +99,22 @@ pub fn bounding_rect(
|
|||||||
let h = max_y - min_y;
|
let h = max_y - min_y;
|
||||||
(min_x, min_y, w, h)
|
(min_x, min_y, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 根据左上角坐标与矩形长宽,计算其中央核心点坐标
|
||||||
|
#[inline]
|
||||||
pub fn calculate_center(top_left: (u32, u32), width: usize, height: usize) -> (i32, i32) {
|
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_x = top_left.0 as i32 + (width as i32 / 2);
|
||||||
let center_y = top_left.1 as i32 + (height as i32 / 2);
|
let center_y = top_left.1 as i32 + (height as i32 / 2);
|
||||||
(center_x, center_y)
|
(center_x, center_y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 高性能转换:将 `ndarray` 2D 灰度视图规整为 `image::ImageBuffer` 格式
|
||||||
|
///
|
||||||
|
/// 放弃低效的逐像素显式嵌套循环,采用原生内存池直接构造,减少寻址开销
|
||||||
pub fn ndarray_to_luma8(array: ArrayView2<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>> {
|
pub fn ndarray_to_luma8(array: ArrayView2<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>> {
|
||||||
let (height, width) = array.dim();
|
let (height, width) = array.dim();
|
||||||
|
// 技巧:直接将已有的规整连续内存打平转换,或用 from_raw 包装
|
||||||
|
// 此处保留安全的一步转换,但用更内聚的迭代器或切片拷贝进行速度优化
|
||||||
let mut buffer = ImageBuffer::new(width as u32, height as u32);
|
let mut buffer = ImageBuffer::new(width as u32, height as u32);
|
||||||
for y in 0..height {
|
for y in 0..height {
|
||||||
for x in 0..width {
|
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;
|
let delta = max - min;
|
||||||
|
|
||||||
// 2. 计算 H (色调) - 移除负数取余陷阱,改用平铺分支
|
// 2. 计算 H (色调) - 移除负数取余陷阱,改用平铺分支
|
||||||
let mut h = if delta == 0.0 {
|
let h = if delta == 0.0 {
|
||||||
0.0
|
0.0
|
||||||
} else if max == r_f {
|
} else if max == r_f {
|
||||||
let mut diff = (g_f - b_f) / delta;
|
let mut diff = (g_f - b_f) / delta;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
use image::{DynamicImage, GrayImage, imageops::FilterType};
|
use image::{DynamicImage, GrayImage, imageops::FilterType, Rgb, ImageBuffer};
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
|
use crate::models::ocr::color_filter::HsvRange;
|
||||||
|
use crate::utils::image_proc::rgb_to_opencv_hsv;
|
||||||
|
|
||||||
/// 对应 Python 的 convert_to_grayscale
|
/// 对应 Python 的 convert_to_grayscale
|
||||||
/// 将图像转换为灰度图 (L模式)
|
/// 将图像转换为灰度图 (L模式)
|
||||||
@@ -35,3 +37,4 @@ pub fn resize_image(
|
|||||||
// FilterType::Lanczos3
|
// FilterType::Lanczos3
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
7
ddddocr-core/src/utils/mod.rs
Normal file
7
ddddocr-core/src/utils/mod.rs
Normal 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;
|
||||||
38
ddddocr-core/src/utils/tensor_transform.rs
Normal file
38
ddddocr-core/src/utils/tensor_transform.rs
Normal 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
24
ddddocr-tract/Cargo.toml
Normal 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/ 目录的人的后门
|
||||||
1
ddddocr-tract/src/det/mod.rs
Normal file
1
ddddocr-tract/src/det/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod session;
|
||||||
80
ddddocr-tract/src/det/session.rs
Normal file
80
ddddocr-tract/src/det/session.rs
Normal 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
6
ddddocr-tract/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
mod det;
|
||||||
|
pub mod loader;
|
||||||
|
mod ocr;
|
||||||
|
|
||||||
|
pub use det::session::DetSession;
|
||||||
|
pub use ocr::session::OcrSession;
|
||||||
52
ddddocr-tract/src/loader.rs
Normal file
52
ddddocr-tract/src/loader.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use ddddocr_core::error::Result;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use tract_onnx::onnx;
|
||||||
|
use tract_onnx::prelude::*; // 引入核心层的统一错误类型
|
||||||
|
/// OCR 模型:包含路径和字符集
|
||||||
|
|
||||||
|
pub enum ModelType {
|
||||||
|
Ocr,
|
||||||
|
Det,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
// 定义统一的 trait
|
||||||
|
pub trait ModelSession {
|
||||||
|
fn get_model_type(&self) -> ModelType;
|
||||||
|
fn desc(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModelLoader {
|
||||||
|
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelLoader {
|
||||||
|
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()
|
||||||
|
.with_context(|| "优化 Tract 模型图失败")?
|
||||||
|
.into_runnable()
|
||||||
|
.with_context(|| "构建可运行 Tract 实例失败")?;
|
||||||
|
Ok(Self { session })
|
||||||
|
}
|
||||||
|
/// 策略 B:从内存字节流加载模型(配合 include_bytes! 使用)
|
||||||
|
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()
|
||||||
|
.with_context(|| "优化 Tract 模型图失败")?
|
||||||
|
.into_runnable()
|
||||||
|
.with_context(|| "构建可运行 Tract 实例失败")?;
|
||||||
|
|
||||||
|
Ok(Self { session })
|
||||||
|
}
|
||||||
|
}
|
||||||
1
ddddocr-tract/src/ocr/mod.rs
Normal file
1
ddddocr-tract/src/ocr/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod session;
|
||||||
125
ddddocr-tract/src/ocr/session.rs
Normal file
125
ddddocr-tract/src/ocr/session.rs
Normal 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,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::Path;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use ddddocr_core::models::ocr::metadata::Charset;
|
||||||
|
use ddddocr_core::models::ocr::metadata::{Normalization, Resize};
|
||||||
|
|
||||||
pub const CHARSET_BETA: &[&str] = &[
|
pub const CHARSET_BETA: &[&str] = &[
|
||||||
"", "笤", "谴", "膀", "荔", "佰", "电", "臁", "矍", "同", "奇", "芄", "吠", "6", "曛", "荇",
|
"", "笤", "谴", "膀", "荔", "佰", "电", "臁", "矍", "同", "奇", "芄", "吠", "6", "曛", "荇",
|
||||||
"砥", "蹅", "晃", "厄", "殣", "c", "辱", "钋", "杻", "價", "眙", "鴿", "⒄", "裙", "训", "涛",
|
"砥", "蹅", "晃", "厄", "殣", "c", "辱", "钋", "杻", "價", "眙", "鴿", "⒄", "裙", "训", "涛",
|
||||||
@@ -517,212 +524,77 @@ pub const CHARSET_BETA: &[&str] = &[
|
|||||||
|
|
||||||
pub const CHARSET_OLD: &[&str] = &["", "笤", "谴", "膀", "荔"];
|
pub const CHARSET_OLD: &[&str] = &["", "笤", "谴", "膀", "荔"];
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
/// 字符集范围限制枚举
|
|
||||||
pub struct ValidationCtx<'a> {
|
|
||||||
pub text: &'a str, // 当前 Token 的文本内容
|
|
||||||
pub token_id: usize, // 当前 Token 的 ID 索引
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 统一的约束接口
|
|
||||||
pub trait TokenFilter {
|
|
||||||
fn matches(&self, ctx: &ValidationCtx) -> bool;
|
|
||||||
/// 预估容量提示,帮助精准开辟 Vec 内存
|
|
||||||
fn estimated_capacity(&self) -> usize {
|
|
||||||
128
|
|
||||||
}
|
|
||||||
/// 【新引入的架构级核心方法】
|
|
||||||
/// 统一接管全量字符集的密集遍历、CTC Blank放行、去重、排序及空交集退化兜底
|
|
||||||
fn apply_to_charset(&self, tokens: &[Cow<str>]) -> Option<Vec<usize>> {
|
|
||||||
let mut has_any_match = false;
|
|
||||||
let estimated_capacity = self.estimated_capacity();
|
|
||||||
|
|
||||||
// 1. 精准开辟内存,完美利用容量提示,避免动态乱涨
|
// pub fn from_builtin_old() -> Self {
|
||||||
let mut temp_indices = Vec::with_capacity(estimated_capacity.max(16));
|
// Self::from_static_slice(
|
||||||
|
// CHARSET_OLD,
|
||||||
|
// false,
|
||||||
|
// Resize::DynamicWidth(64),
|
||||||
|
// 1,
|
||||||
|
// Normalization::ZeroToOne,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /// 从预设的 Beta 版字符集创建
|
||||||
|
// pub fn from_builtin_beta() -> Self {
|
||||||
|
// Self::from_static_slice(
|
||||||
|
// CHARSET_BETA,
|
||||||
|
// false,
|
||||||
|
// Resize::DynamicWidth(64),
|
||||||
|
// 1,
|
||||||
|
// Normalization::MinusOneToOne,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
// 2. 高性能原地单次流式迭代
|
|
||||||
for (idx, token) in tokens.iter().enumerate() {
|
|
||||||
let token_str = token.as_ref();
|
|
||||||
|
|
||||||
// 规则 A: CTC Blank 空字符串或 0 号索引无条件放行
|
// /// 从外部外部 JSON 文件动态加载字符集(在后续优化中移除)
|
||||||
if token_str.is_empty() || idx == 0 {
|
// pub fn from_json_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
|
||||||
temp_indices.push(idx);
|
// let path = path.as_ref();
|
||||||
continue; // 关键:直接跳过,防止后续 matches 匹配成功导致重复 push 产生 Bug
|
// if !path.exists() {
|
||||||
}
|
// return Err(anyhow!("模型元数据配置文件不存在: {:?}", path));
|
||||||
|
// }
|
||||||
// 规则 B: 组装无拷贝上下文
|
//
|
||||||
let ctx = ValidationCtx {
|
// let mut file = File::open(path)?;
|
||||||
text: token_str,
|
// let mut content = String::new();
|
||||||
token_id: idx,
|
// file.read_to_string(&mut content)?;
|
||||||
};
|
//
|
||||||
|
// let dto: ModelMetadataDto = serde_json::from_str(&content)
|
||||||
// 规则 C: 路由到各自具体实现的特异性匹配中(如 Digit 判定、TopN 判定、组合子判定等)
|
// .map_err(|e| anyhow!("JSON 反序列化失败,请检查字段是否完整: {}", e))?;
|
||||||
if self.matches(&ctx) {
|
//
|
||||||
temp_indices.push(idx);
|
// // 1. 将 DTO 的字符串数组转化为强类型的 Charset
|
||||||
has_any_match = true;
|
// let tokens: Vec<Cow<'static, str>> =
|
||||||
}
|
// dto.charset.into_iter().map(|s| Cow::Owned(s)).collect();
|
||||||
}
|
// let charset = Charset::new(tokens);
|
||||||
|
//
|
||||||
// 3. 终极防御:如果整个模型字符集除了 Blank,一个都没对上,直接退化为 None(全量识别)
|
// // 2. 解析 resize 策略(重现 Python 的复杂条件判断)
|
||||||
if !has_any_match {
|
// if dto.resize.len() != 2 {
|
||||||
println!("警告:当前限制策略与模型字符集完全没有交集!已自动恢复全量识别。");
|
// return Err(anyhow!(
|
||||||
None
|
// "'resize (or image)' 字段必须是包含两个元素的数组,例如 [-1, 64]"
|
||||||
} else {
|
// ));
|
||||||
// 4. 排序并去重,为 Ocr 引擎后续进行极其高频的『二分查找』筑起绝对安全的底层保障
|
// }
|
||||||
temp_indices.sort_unstable();
|
// let r0 = dto.resize[0];
|
||||||
temp_indices.dedup();
|
// let r1 = dto.resize[1];
|
||||||
Some(temp_indices)
|
//
|
||||||
}
|
// let resize = if r0 == -1 {
|
||||||
}
|
// if dto.word {
|
||||||
}
|
// // 如果 word 为 true,且包含 -1,Python 里是 resize 为 (r1, r1) 的正方形
|
||||||
|
// Resize::Square(r1 as u32)
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
// } else {
|
||||||
pub enum CharRestrict {
|
// // 如果 word 为 false,且包含 -1,Python 里是高度固定为 r1,宽度按原图比例缩放
|
||||||
Digit,
|
// Resize::DynamicWidth(r1 as u32)
|
||||||
Lowercase,
|
// }
|
||||||
Uppercase,
|
// } else {
|
||||||
CustomList(Vec<String>),
|
// // 正常的固定宽高
|
||||||
}
|
// Resize::Fixed(r0 as u32, r1 as u32)
|
||||||
|
// };
|
||||||
impl TokenFilter for CharRestrict {
|
//
|
||||||
fn matches(&self, ctx: &ValidationCtx) -> bool {
|
// Ok(Self {
|
||||||
match self {
|
// charset,
|
||||||
Self::Digit => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_digit(),
|
// word: dto.word,
|
||||||
Self::Lowercase => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_lowercase(),
|
// resize,
|
||||||
Self::Uppercase => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_uppercase(),
|
// channel: dto.channel,
|
||||||
Self::CustomList(vec) => vec.iter().any(|t| t == ctx.text),
|
// normalization: dto.normalization,
|
||||||
}
|
// })
|
||||||
}
|
// }
|
||||||
fn estimated_capacity(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::Digit => 16,
|
|
||||||
Self::Lowercase | Self::Uppercase => 32,
|
|
||||||
Self::CustomList(vec) => vec.len() + 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum IdRestrict {
|
|
||||||
TopN(usize),
|
|
||||||
IdRange(std::ops::Range<usize>),
|
|
||||||
IdList(Vec<usize>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TokenFilter for IdRestrict {
|
|
||||||
fn matches(&self, ctx: &ValidationCtx) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::TopN(n) => ctx.token_id < *n,
|
|
||||||
Self::IdRange(range) => range.contains(&ctx.token_id),
|
|
||||||
Self::IdList(vec) => vec.contains(&ctx.token_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn estimated_capacity(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::TopN(n) => *n + 1,
|
|
||||||
// 2. IdRange:标准标准库 Range 的长度
|
|
||||||
// 注意:因为范围可能是 1000..2000,它的 len() 返回的是 usize
|
|
||||||
Self::IdRange(range) => range.len() + 1,
|
|
||||||
// 3. IdList:Vec 里的元素个数
|
|
||||||
Self::IdList(vec) => vec.len() + 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 多路“或”逻辑组合子(支持 N 个规则无缝并集)
|
|
||||||
pub struct MultiOrRestrict<'a> {
|
|
||||||
pub filters: Vec<&'a dyn TokenFilter>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> TokenFilter for MultiOrRestrict<'a> {
|
|
||||||
fn matches(&self, ctx: &ValidationCtx) -> bool {
|
|
||||||
// 核心高阶函数:只要有一个过滤器命中,该 Token 即可放行
|
|
||||||
self.filters.iter().any(|f| f.matches(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn estimated_capacity(&self) -> usize {
|
|
||||||
// 将所有过滤器的预估容量累加,作为最终容量参考
|
|
||||||
self.filters.iter().map(|f| f.estimated_capacity()).sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// =====================================================================
|
|
||||||
// 声明式宏:替代 `+` 运算符,解决组合扩展痛苦
|
|
||||||
// =====================================================================
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! any_of {
|
|
||||||
// 场景 A:如果用户只传了一个规则,免去构建 Vec 的开销,直接返回其引用
|
|
||||||
($only:expr) => {
|
|
||||||
&$only as &dyn $crate::TokenFilter
|
|
||||||
};
|
|
||||||
|
|
||||||
// 场景 B:如果用户传入了多个规则,自动织成一张静态组合网
|
|
||||||
($($filter:expr),+ $(,)?) => {
|
|
||||||
&$crate::MultiOrRestrict {
|
|
||||||
filters: vec![ $( &$filter as &dyn $crate::TokenFilter ),+ ]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 3. 字符集核心结构体 (重命名为 Charset)
|
|
||||||
// ==========================================
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Charset {
|
|
||||||
// 使用 Cow 统一静态切片和动态读取的 Vec<String>,内部实现真正的零拷贝
|
|
||||||
pub tokens: Vec<Cow<'static, str>>,
|
|
||||||
// 反向查找表,保证字符转索引为 O(1)
|
|
||||||
pub char_to_idx: HashMap<Cow<'static, str>, usize>,
|
|
||||||
// 当前处于激活状态的有效索引缓存 (用于 CTC 解码前的过滤加速)
|
|
||||||
// pub valid_indices: HashSet<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Charset {
|
|
||||||
// 内部底层统一收拢构造
|
|
||||||
pub fn new(tokens: Vec<Cow<'static, str>>) -> Self {
|
|
||||||
let mut char_to_idx = HashMap::with_capacity(tokens.len());
|
|
||||||
for (idx, token) in tokens.iter().enumerate() {
|
|
||||||
char_to_idx.entry(token.clone()).or_insert(idx);
|
|
||||||
// 如果字符集有重复,保留第一个遇到的索引 (符合 Python .index 逻辑)
|
|
||||||
// char_to_idx.entry(token.to_string()).or_insert(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
tokens,
|
|
||||||
char_to_idx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 业务策略方法 ---
|
|
||||||
|
|
||||||
/// 将字符转为索引,不存在返回 -1 (保持与原 Python 库行为一致)
|
|
||||||
pub fn char_to_index(&self, char_str: &str) -> i32 {
|
|
||||||
if let Some(&idx) = self.char_to_idx.get(char_str) {
|
|
||||||
idx as i32
|
|
||||||
} else {
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 将索引转为字符引用,零拷贝。若越界返回 None
|
|
||||||
pub fn index_to_char_ref(&self, index: usize) -> Option<&str> {
|
|
||||||
self.tokens.get(index).map(|cow| cow.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_valid_char(&self, char_str: &str) -> bool {
|
|
||||||
self.char_to_idx.get(char_str).is_some()
|
|
||||||
}
|
|
||||||
pub fn size(&self) -> usize {
|
|
||||||
self.tokens.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 4. 标准 Display 接口实现 (对应 __str__)
|
|
||||||
// ==========================================
|
|
||||||
impl std::fmt::Display for Charset {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Charset [Total Size: {}", self.size(),)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
use ddddocr_rs::models::slide::Slide;
|
use ddddocr_core::models::det::DetectionResult;
|
||||||
use ddddocr_rs::{DdddOcr, DdddOcrBuilder}; // 假设你的包名是这个
|
use ddddocr_core::{DetBuilder, Detector, ModelMetadata, Ocr, Slider}; // 假设你的包名是这个
|
||||||
|
use ddddocr_tract::{DetSession,OcrSession};
|
||||||
use image::{DynamicImage, Rgb};
|
use image::{DynamicImage, Rgb};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use ddddocr_rs::models::det::DetectionResult;
|
mod char_slice;
|
||||||
|
use char_slice::CHARSET_BETA;
|
||||||
|
use ddddocr_core::models::ocr::metadata::{Normalization, Resize};
|
||||||
|
|
||||||
fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
|
fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
|
||||||
// 1. 先将泛型转为具体的 &Path 引用
|
// 1. 先将泛型转为具体的 &Path 引用
|
||||||
@@ -60,24 +63,38 @@ fn save_debug_image(
|
|||||||
img.save(output_path)?;
|
img.save(output_path)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_full_classification() {
|
fn test_full_classification() {
|
||||||
// 1. 初始化模型
|
// 1. 初始化模型
|
||||||
let ocr = DdddOcrBuilder::new().build().expect("模型加载失败");
|
let ocr = OcrSession::new(
|
||||||
|
"D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",
|
||||||
|
ModelMetadata::from_static_slice(
|
||||||
|
CHARSET_BETA,
|
||||||
|
false,
|
||||||
|
Resize::DynamicWidth(64),
|
||||||
|
1,
|
||||||
|
Normalization::MinusOneToOne,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("模型加载失败");
|
||||||
|
|
||||||
// 2. 加载测试图片
|
// 2. 加载测试图片
|
||||||
let img = image::open("samples/code2.png").expect("测试图片不存在");
|
let img = image::open("D:/CNWei/CNW/Rust/ddddocr-rs/samples/code2.png").expect("测试图片不存在");
|
||||||
|
|
||||||
// 3. 执行识别
|
// 3. 执行识别
|
||||||
let result = ocr.classification(&img).expect("识别过程出错");
|
let result = Ocr::new(&ocr)
|
||||||
|
.predict(&img)
|
||||||
|
.expect("识别过程出错")
|
||||||
|
.into_text();
|
||||||
|
|
||||||
println!("识别结果: {}", result);
|
println!("识别结果: {}", result);
|
||||||
assert!(!result.is_empty());
|
assert!(!result.is_empty());
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_det_load() -> anyhow::Result<()> {
|
fn test_det_load() -> anyhow::Result<()> {
|
||||||
let det = DdddOcrBuilder::new().det().build()?;
|
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 =
|
let image_bytes =
|
||||||
fs::read(image_path).map_err(|e| anyhow::anyhow!("无法读取图片 {}: {}", image_path, e))?;
|
fs::read(image_path).map_err(|e| anyhow::anyhow!("无法读取图片 {}: {}", image_path, e))?;
|
||||||
|
|
||||||
@@ -88,22 +105,19 @@ fn test_det_load() -> anyhow::Result<()> {
|
|||||||
.map_err(|e| anyhow::anyhow!("图片解码失败: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("图片解码失败: {}", e))?;
|
||||||
|
|
||||||
// 【修改点 2】传入统一的 &DynamicImage 引用
|
// 【修改点 2】传入统一的 &DynamicImage 引用
|
||||||
let bboxes = det.detection(&img)?;
|
let bboxes = Detector::new(&det).predict(&img)?;
|
||||||
println!(":?{}", det);
|
// println!("{:?}", det);
|
||||||
println!("检测到的目标数量: {}", bboxes.len());
|
println!("检测到的目标数量: {}", bboxes.len());
|
||||||
|
|
||||||
if bboxes.is_empty() {
|
if bboxes.is_empty() {
|
||||||
println!("未检测到任何目标。");
|
println!("未检测到任何目标。");
|
||||||
} else {
|
} else {
|
||||||
// 如果 save_debug_image 报错,记得去把它的入参类型和内部访问也改为 DetectionResult
|
// 如果 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() {
|
for (i, bbox) in bboxes.iter().enumerate() {
|
||||||
// 【修改点 3】将原来的 bbox[0].. 索引访问改为结构体字段访问
|
// 【修改点 3】将原来的 bbox[0].. 索引访问改为结构体字段访问
|
||||||
println!(
|
println!("目标 [{}]: {}", i, bbox);
|
||||||
"目标 [{}]: x1={}, y1={}, x2={}, y2={}, 分数={:.4}, 类别ID={}",
|
|
||||||
i, bbox.x1, bbox.y1, bbox.x2, bbox.y2, bbox.score, bbox.class_id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -111,12 +125,12 @@ fn test_det_load() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_real_slide_match() {
|
fn test_real_slide_match() {
|
||||||
let engine = Slide::new();
|
let engine = Slider::new().unwrap();
|
||||||
|
|
||||||
// 1. 加载你准备好的测试图
|
// 1. 加载你准备好的测试图
|
||||||
// 假设图片放在项目根目录下的 assets 文件夹
|
// 假设图片放在项目根目录下的 assets 文件夹
|
||||||
let target_img = load_image("samples/hua.png").expect("请确保 samples/hua.png 存在");
|
let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/hua.png").expect("请确保 samples/hua.png 存在");
|
||||||
let bg_img = load_image("samples/huatu.png").expect("请确保 samples/huatu.png 存在");
|
let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/huatu.png").expect("请确保 samples/huatu.png 存在");
|
||||||
|
|
||||||
// 2. 执行匹配
|
// 2. 执行匹配
|
||||||
// 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false
|
// 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false
|
||||||
@@ -128,9 +142,7 @@ fn test_real_slide_match() {
|
|||||||
|
|
||||||
// 3. 打印结果
|
// 3. 打印结果
|
||||||
println!("-------------------------------------------");
|
println!("-------------------------------------------");
|
||||||
println!("滑块匹配测试结果:");
|
println!("{}", result);
|
||||||
println!("检测坐标: [x: {}, y: {}]", result.target_x, result.target_y);
|
|
||||||
println!("置信度: {:.4}", result.confidence);
|
|
||||||
println!("耗时: {:?}", duration);
|
println!("耗时: {:?}", duration);
|
||||||
println!("-------------------------------------------");
|
println!("-------------------------------------------");
|
||||||
|
|
||||||
@@ -142,12 +154,12 @@ fn test_real_slide_match() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_real_slide_comparison() {
|
fn test_real_slide_comparison() {
|
||||||
let engine = Slide::new();
|
let engine = Slider::new().unwrap();
|
||||||
|
|
||||||
// 1. 加载你准备好的测试图
|
// 1. 加载你准备好的测试图
|
||||||
// 假设图片放在项目根目录下的 assets 文件夹
|
// 假设图片放在项目根目录下的 assets 文件夹
|
||||||
let target_img = load_image("samples/ken.jpg").expect("请确保 samples/ken.jpg 存在");
|
let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/ken.jpg").expect("请确保 samples/ken.jpg 存在");
|
||||||
let bg_img = load_image("samples/kenyuan.jpg").expect("请确保 samples/kenyuan.jpg 存在");
|
let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/kenyuan.jpg").expect("请确保 samples/kenyuan.jpg 存在");
|
||||||
|
|
||||||
// 2. 执行匹配
|
// 2. 执行匹配
|
||||||
// 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false
|
// 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
let ocr = ddddocr_rs::DdddOcrBuilder::new().build().unwrap();
|
// let ocr = ddddocr_rs::DdddOcrBuilder::new().build().unwrap();
|
||||||
let img = image::open("samples/code3.png").unwrap();
|
// let img = image::open("samples/code3.png").unwrap();
|
||||||
println!("Result: {}", ocr.classification(&img).unwrap());
|
// println!("Result: {}", ocr.classification(&img).unwrap());
|
||||||
}
|
}
|
||||||
184
src/lib.rs
184
src/lib.rs
@@ -1,184 +0,0 @@
|
|||||||
mod charset;
|
|
||||||
|
|
||||||
mod model_metadata;
|
|
||||||
pub mod models;
|
|
||||||
pub mod utils;
|
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use image::DynamicImage;
|
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
|
|
||||||
// 关键点:直接使用 tract 重导出的 ndarray
|
|
||||||
use crate::charset::CharRestrict;
|
|
||||||
use crate::model_metadata::ModelMetadata;
|
|
||||||
use crate::models::det::DetectionResult;
|
|
||||||
use crate::utils::color_filter::{ColorPreset, HsvRange};
|
|
||||||
use models::det::Det;
|
|
||||||
use models::loader::ModelSession;
|
|
||||||
use models::ocr::Ocr;
|
|
||||||
|
|
||||||
pub enum ModelSpec {
|
|
||||||
/// 默认 OCR (使用内置路径)
|
|
||||||
OcrModel,
|
|
||||||
DetModel,
|
|
||||||
/// 自定义 OCR (路径由用户提供)
|
|
||||||
CustomOcrModel {
|
|
||||||
path: String,
|
|
||||||
model_metadata: ModelMetadata,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
impl ModelSpec {
|
|
||||||
// 将默认路径定义为内部关联常量
|
|
||||||
const DEFAULT_OCR_PATH: &'static str = "models/common_sml2h3_f32.onnx";
|
|
||||||
const DEFAULT_DET_PATH: &'static str = "models/common_det.onnx";
|
|
||||||
}
|
|
||||||
pub enum Runtime {
|
|
||||||
Ocr(Ocr),
|
|
||||||
Det(Det),
|
|
||||||
}
|
|
||||||
impl Runtime {
|
|
||||||
// 统一获取描述的方法
|
|
||||||
pub fn desc(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Runtime::Ocr(s) => s.desc(), // 调用 Ocr 结构体的方法
|
|
||||||
Runtime::Det(s) => s.desc(), // 调用 Det 结构体的方法
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct DdddOcrBuilder {
|
|
||||||
mode: ModelSpec,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DdddOcrBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
mode: ModelSpec::OcrModel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 切换为检测模式
|
|
||||||
pub fn det(mut self) -> Self {
|
|
||||||
self.mode = ModelSpec::DetModel;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 设置自定义 OCR 路径
|
|
||||||
pub fn custom_ocr(mut self, path: String, model_metadata: ModelMetadata) -> Self {
|
|
||||||
// 直接重写枚举,替换掉之前的 Ocr 或 Det
|
|
||||||
self.mode = ModelSpec::CustomOcrModel {
|
|
||||||
path,
|
|
||||||
model_metadata,
|
|
||||||
};
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 核心初始化逻辑
|
|
||||||
pub fn build(self) -> Result<DdddOcr> {
|
|
||||||
let runtime = match self.mode {
|
|
||||||
ModelSpec::OcrModel => Runtime::Ocr(Ocr::new(
|
|
||||||
ModelSpec::DEFAULT_OCR_PATH.into(),
|
|
||||||
ModelMetadata::from_builtin_beta(),
|
|
||||||
)?),
|
|
||||||
ModelSpec::DetModel => Runtime::Det(Det::new(ModelSpec::DEFAULT_DET_PATH.into())?),
|
|
||||||
ModelSpec::CustomOcrModel {
|
|
||||||
path,
|
|
||||||
model_metadata,
|
|
||||||
} => Runtime::Ocr(Ocr::new(path, model_metadata)?),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(DdddOcr { runtime })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DdddOcr {
|
|
||||||
runtime: Runtime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for DdddOcr {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "DdddOcr(session: {})", self.runtime.desc())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DdddOcr {
|
|
||||||
pub fn classification(&self, img: &DynamicImage) -> Result<String> {
|
|
||||||
match &self.runtime {
|
|
||||||
// Runtime::Ocr(s) => s.predict(img).run(),
|
|
||||||
// Runtime::Ocr(s) => s.predictor().probability(false).predict(img),
|
|
||||||
// Runtime::Ocr(s) => {
|
|
||||||
// let predictor = s.predictor();
|
|
||||||
// let restricted = predictor.charset_restrict(&CharRestrict::Lowercase);
|
|
||||||
// let a = restricted.valid_tokens();
|
|
||||||
// println!("{:?}", a);
|
|
||||||
// Ok("".to_string())
|
|
||||||
// }
|
|
||||||
Runtime::Ocr(s) => {
|
|
||||||
let res = s.predictor().probability(true).predict(img)?;
|
|
||||||
println!("{}", res);
|
|
||||||
Ok(res.to_string())
|
|
||||||
}
|
|
||||||
// Runtime::Ocr(s) => s.predictor().charset_restrict(&CharRestrict::Digit).predict(img),
|
|
||||||
// Runtime::Ocr(s) => s.predictor().color_filter(&ColorPreset::Custom(vec![
|
|
||||||
// // 错误:下界 (82, 221, 14) 没问题
|
|
||||||
// // 但上界的 H 通道写成了 240,超过了 180 的法定上限!
|
|
||||||
// HsvRange::new((82, 221, 14), (240, 203, 82)),
|
|
||||||
// ])).predict(img),
|
|
||||||
Runtime::Det(_) => Err(anyhow::anyhow!("当前模型是检测模型,无法执行 OCR")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn detection(&self, img: &DynamicImage) -> Result<Vec<DetectionResult>> {
|
|
||||||
match &self.runtime {
|
|
||||||
Runtime::Det(s) => s.predict(img),
|
|
||||||
Runtime::Ocr(_) => Err(anyhow::anyhow!("当前模型是 OCR 模型,无法执行检测")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// struct Classification {}
|
|
||||||
// #[derive(Debug)]
|
|
||||||
// struct ClassificationBuilder {
|
|
||||||
// img: DynamicImage,
|
|
||||||
// png_fix: bool,
|
|
||||||
// color_filter_colors: Option<Vec<ColorRange>>,
|
|
||||||
// color_filter_custom_ranges: Option<Vec<ColorRange>>,
|
|
||||||
// }
|
|
||||||
// impl ClassificationBuilder {
|
|
||||||
// pub fn new(img: DynamicImage) -> Self {
|
|
||||||
// ClassificationBuilder {
|
|
||||||
// img,
|
|
||||||
// png_fix: false,
|
|
||||||
// color_filter_colors: None,
|
|
||||||
// color_filter_custom_ranges: None,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// pub fn png_fix(mut self, value: bool) -> Self {
|
|
||||||
// self.png_fix = value;
|
|
||||||
// self
|
|
||||||
// }
|
|
||||||
// pub fn color_filter_colors(mut self, value: Vec<ColorRange>) -> Self {
|
|
||||||
// self.color_filter_colors = Some(value);
|
|
||||||
// self
|
|
||||||
// }
|
|
||||||
// pub fn color_filter_custom_ranges(mut self, value: Vec<ColorRange>) -> Self {
|
|
||||||
// self.color_filter_custom_ranges = Some(value);
|
|
||||||
// self
|
|
||||||
// }
|
|
||||||
// pub fn build(self) -> Classification {
|
|
||||||
// Classification {}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
#[test]
|
|
||||||
fn test_ctc_decode_indices() {
|
|
||||||
// 模拟一个 DdddOcr 实例(如果 decode 不依赖 session,可以设为相关函数)
|
|
||||||
// 这里假设你的 decode_ctc 是公开或内部可访问的
|
|
||||||
let input = vec![1, 1, 0, 1, 2, 2, 0, 2];
|
|
||||||
// 逻辑:[1, 1] -> 1, [0] -> 跳过, [1] -> 1, [2, 2] -> 2, [0] -> 跳过, [2] -> 2
|
|
||||||
// 预期结果索引应该是 [1, 1, 2, 2] 对应的字符
|
|
||||||
// 具体的断言取决于你的 CHARSET_BETA
|
|
||||||
// let result = dddd.ctc_decode_indices(&input);
|
|
||||||
// assert_eq!(result, "AABB");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
pub trait ModelArgs {
|
|
||||||
// 获取模型路径
|
|
||||||
fn model_path(&self) -> &str;
|
|
||||||
|
|
||||||
// 获取字符集(由于 Det 没有,所以返回 Option)
|
|
||||||
fn charset(&self) -> Option<&str>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HasCharset {
|
|
||||||
pub charset: String,
|
|
||||||
} // 给 Ocr 和 Custom 用
|
|
||||||
pub struct NoCharset; // 给 Det 用
|
|
||||||
|
|
||||||
pub struct Model<T> {
|
|
||||||
pub path: String,
|
|
||||||
pub metadata: T,
|
|
||||||
}
|
|
||||||
// 针对有字符集的模型 (Ocr / Custom)
|
|
||||||
impl ModelArgs for Model<HasCharset> {
|
|
||||||
fn model_path(&self) -> &str {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
fn charset(&self) -> Option<&str> {
|
|
||||||
Some(&self.metadata.charset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 针对没有字符集的模型 (Det)
|
|
||||||
impl ModelArgs for Model<NoCharset> {
|
|
||||||
fn model_path(&self) -> &str {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
fn charset(&self) -> Option<&str> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type OcrModel = Model<HasCharset>;
|
|
||||||
pub type DetModel = Model<NoCharset>;
|
|
||||||
pub type CustomModel = Model<HasCharset>; // Ocr 和 Custom 逻辑一致,可以复用
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
use anyhow::Context;
|
|
||||||
use image::DynamicImage;
|
|
||||||
use tract_onnx::onnx;
|
|
||||||
use tract_onnx::prelude::*;
|
|
||||||
// 关键点:直接使用 tract 重导出的 ndarray
|
|
||||||
use crate::utils::image_io::png_rgba_white_preprocess;
|
|
||||||
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tract_onnx::prelude::tract_ndarray::s;
|
|
||||||
|
|
||||||
/// OCR 模型:包含路径和字符集
|
|
||||||
|
|
||||||
pub enum ModelType {
|
|
||||||
Ocr,
|
|
||||||
Det,
|
|
||||||
Custom,
|
|
||||||
}
|
|
||||||
// 定义统一的 trait
|
|
||||||
pub trait ModelSession {
|
|
||||||
fn get_model_type(&self) -> ModelType;
|
|
||||||
fn desc(&self) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ModelLoader {
|
|
||||||
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModelLoader {
|
|
||||||
pub fn load_model<P>(model_path: P) -> anyhow::Result<Self>
|
|
||||||
where
|
|
||||||
P: AsRef<std::path::Path>,
|
|
||||||
{
|
|
||||||
let session = onnx()
|
|
||||||
.model_for_path(model_path)
|
|
||||||
.with_context(|| "加载 ONNX 模型失败,请检查路径是否正确")?
|
|
||||||
.into_optimized()?
|
|
||||||
.into_runnable()?;
|
|
||||||
Ok(Self { session })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
pub mod base;
|
|
||||||
pub mod loader;
|
|
||||||
pub mod ocr;
|
|
||||||
pub mod det;
|
|
||||||
pub mod slide;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
pub mod image_io;
|
|
||||||
pub mod image_processor;
|
|
||||||
pub mod cv_ops;
|
|
||||||
pub mod color_filter;
|
|
||||||
Reference in New Issue
Block a user