feat(ocr,det,slide): 重构配置解析流程,移除非必要的生命周期方法
- 优化 规范化模型目录 - 重构 Ocr,Detector配置解析流程
This commit is contained in:
3
src/algo/mod.rs
Normal file
3
src/algo/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod slide;
|
||||
|
||||
pub use slide::{SlideResult, Slider};
|
||||
18
src/error.rs
Normal file
18
src/error.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
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. 或者直接将模型文件重命名并放置在您运行程序的“当前工作目录”或“可执行文件同级目录”下。
|
||||
================================================================================";
|
||||
24
src/lib.rs
24
src/lib.rs
@@ -1,24 +1,14 @@
|
||||
mod charset;
|
||||
mod algo;
|
||||
mod error;
|
||||
mod model_metadata;
|
||||
pub mod models;
|
||||
pub mod utils;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use image::DynamicImage;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
pub use crate::models::det::{Detector,DetectionResult};
|
||||
pub use crate::models::ocr::{Ocr, OcrPredictor, OcrResult};
|
||||
pub use crate::models::slide::{Slider, SlideResult};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// 关键点:直接使用 tract 重导出的 ndarray
|
||||
use crate::charset::CharRestrict;
|
||||
pub use crate::model_metadata::ModelMetadata;
|
||||
use crate::utils::color_filter::{ColorPreset, HsvRange};
|
||||
pub use crate::algo::{SlideResult, Slider};
|
||||
pub use crate::models::det::{DetBuilder, DetSession, DetectionResult, Detector};
|
||||
pub use crate::models::ocr::{Ocr, OcrBuilder, OcrResult, OcrSession};
|
||||
use models::loader::ModelSession;
|
||||
|
||||
pub use models::ocr::model_metadata::ModelMetadata;
|
||||
|
||||
// pub enum ModelSpec {
|
||||
// /// 默认 OCR (使用内置路径)
|
||||
@@ -99,7 +89,7 @@ use models::loader::ModelSession;
|
||||
//
|
||||
// impl Display for DdddOcr {
|
||||
// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// write!(f, "DdddOcr(session: {})", self.runtime.desc())
|
||||
// write!(f, "DdddOcr(ocr: {})", self.runtime.desc())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
@@ -175,7 +165,7 @@ use models::loader::ModelSession;
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_ctc_decode_indices() {
|
||||
// 模拟一个 DdddOcr 实例(如果 decode 不依赖 session,可以设为相关函数)
|
||||
// 模拟一个 DdddOcr 实例(如果 decode 不依赖 ocr,可以设为相关函数)
|
||||
// 这里假设你的 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,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 逻辑一致,可以复用
|
||||
25
src/models/det/builder.rs
Normal file
25
src/models/det/builder.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::models::det::executor::Detector;
|
||||
use crate::models::det::session::DetSession;
|
||||
|
||||
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: &DetSession) -> Detector<'_> {
|
||||
Detector {
|
||||
session,
|
||||
use_gpu: self.use_gpu,
|
||||
device_id: self.device_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::model_metadata::ModelMetadata;
|
||||
use crate::models::ocr::model_metadata::ModelMetadata;
|
||||
use crate::models::loader::{ModelLoader, ModelSession, ModelType};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use image::{DynamicImage, GenericImageView, imageops::FilterType};
|
||||
@@ -10,6 +10,7 @@ const DEFAULT_DET_PATH: &'static str = "common_det.onnx";
|
||||
|
||||
// 预设的提示信息常量
|
||||
use crate::error::MODEL_DOWNLOAD_HELP;
|
||||
use crate::models::det::session::DetSession;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DetectionResult {
|
||||
@@ -21,30 +22,25 @@ pub struct DetectionResult {
|
||||
pub class_id: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Detector {
|
||||
session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
||||
}
|
||||
impl ModelSession for Detector {
|
||||
fn get_model_type(&self) -> ModelType {
|
||||
todo!()
|
||||
}
|
||||
fn desc(&self) -> String {
|
||||
"Detection Model 加载成功".to_string()
|
||||
}
|
||||
}
|
||||
impl Detector {
|
||||
pub fn new<P>(model_path: P) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let session = ModelLoader::model_for_path(&model_path)?.session;
|
||||
Ok(Self { session })
|
||||
}
|
||||
|
||||
pub fn model_from_bytes(model_bytes: &[u8]) -> Result<Self, anyhow::Error> {
|
||||
let session = ModelLoader::model_from_bytes(model_bytes)?.session;
|
||||
Ok(Self { session })
|
||||
|
||||
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Detector<'a> {
|
||||
pub(crate) session: &'a DetSession,
|
||||
pub(crate) use_gpu: bool,
|
||||
pub(crate) device_id: u8,
|
||||
}
|
||||
|
||||
impl<'a> Detector<'a> {
|
||||
pub fn new(session: &'a DetSession) -> Self {
|
||||
Detector {
|
||||
session,
|
||||
use_gpu: false,
|
||||
device_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn predict(&self, image: &DynamicImage) -> Result<Vec<DetectionResult>> {
|
||||
@@ -256,8 +252,10 @@ impl Detector {
|
||||
let (input_tensor, ratio) = self.preproc(dynamic_img, (416, 416))?;
|
||||
|
||||
// tract 推理
|
||||
let outputs = self.session.run(tvec!(input_tensor.into()))?;
|
||||
let output_array = outputs[0]
|
||||
// let outputs = self.session.session.run(tvec!(input_tensor.into()))?;
|
||||
let outputs = self.session.inference(input_tensor)?;
|
||||
// let output_array = outputs[0]
|
||||
let output_array = outputs
|
||||
.to_array_view::<f32>()?
|
||||
.to_owned()
|
||||
.into_dimensionality::<Ix3>()?;
|
||||
7
src/models/det/mod.rs
Normal file
7
src/models/det/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod builder;
|
||||
mod executor;
|
||||
mod session;
|
||||
|
||||
pub use builder::DetBuilder;
|
||||
pub use executor::{DetectionResult, Detector};
|
||||
pub use session::DetSession;
|
||||
43
src/models/det/session.rs
Normal file
43
src/models/det/session.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::models::loader::{ModelLoader, ModelSession, ModelType};
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use tract_onnx::prelude::{tvec, Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DetSession {
|
||||
pub(crate) session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
||||
}
|
||||
|
||||
impl ModelSession for DetSession {
|
||||
fn get_model_type(&self) -> ModelType {
|
||||
todo!()
|
||||
}
|
||||
fn desc(&self) -> String {
|
||||
"Detection Model 加载成功".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl DetSession {
|
||||
pub fn new<P>(model_path: P) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let session = ModelLoader::model_for_path(&model_path)?.session;
|
||||
Ok(Self { session })
|
||||
}
|
||||
|
||||
pub fn model_from_bytes(model_bytes: &[u8]) -> Result<Self, anyhow::Error> {
|
||||
let session = ModelLoader::model_from_bytes(model_bytes)?.session;
|
||||
Ok(Self { session })
|
||||
}
|
||||
pub fn inference(&self, tensor: Tensor) -> anyhow::Result<Tensor> {
|
||||
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
|
||||
// let result = self.ocr.run(tvec!(tensor.into()))?;
|
||||
let mut result = self
|
||||
.session
|
||||
.run(tvec!(tensor.into()))
|
||||
.context("执行模型推理失败")?;
|
||||
println!("模型输出原始数据: {:?}", result);
|
||||
Ok(result.swap_remove(0).into_tensor())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
pub mod base;
|
||||
pub mod loader;
|
||||
pub mod ocr;
|
||||
pub mod det;
|
||||
pub mod slide;
|
||||
pub mod det;
|
||||
72
src/models/ocr/builder.rs
Normal file
72
src/models/ocr/builder.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use crate::models::ocr::charset::TokenFilter;
|
||||
use crate::models::ocr::executor::Ocr;
|
||||
use crate::models::ocr::session::OcrSession;
|
||||
use crate::models::ocr::color_filter::ColorFilter;
|
||||
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: &OcrSession) -> Ocr<'_> {
|
||||
// 1. 原地解析颜色过滤器
|
||||
let final_color_ranges = match &self.color_filter {
|
||||
Some(filter) => filter.collect_to_vec(),
|
||||
None => Ok(None),
|
||||
};
|
||||
// 2. 原地解析字符集过滤
|
||||
let tokens = &session.model_metadata.charset.tokens;
|
||||
let 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,27 +1,16 @@
|
||||
use crate::charset::{TokenFilter, ValidationCtx};
|
||||
use crate::model_metadata::{ModelMetadata, Resize};
|
||||
use crate::models::base::ModelArgs;
|
||||
use crate::models::loader::{ModelLoader, ModelSession, ModelType};
|
||||
use crate::utils::color_filter::{ColorFilter, HsvRange, filter_image};
|
||||
use crate::models::ocr::model_metadata::Resize;
|
||||
|
||||
use crate::models::ocr::session::OcrSession;
|
||||
use crate::models::ocr::color_filter::{HsvRange, filter_image};
|
||||
use crate::utils::image_io::png_rgba_white_preprocess;
|
||||
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
||||
use anyhow::Context;
|
||||
use anyhow::{Result, anyhow};
|
||||
use image::{DynamicImage, ImageBuffer, Rgb};
|
||||
use anyhow::Result;
|
||||
use image::DynamicImage;
|
||||
use serde::Serialize;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use tract_onnx::prelude::tract_ndarray::{ArrayView2, Ix2, s};
|
||||
use tract_onnx::prelude::{
|
||||
DatumType, Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp, tract_ndarray, tvec,
|
||||
};
|
||||
|
||||
// 引入 cv_ops 模块中的 OpenCV HSV 转换算子
|
||||
use crate::utils::cv_ops::rgb_to_opencv_hsv;
|
||||
|
||||
/// 推理最终输出的强类型外壳(完全 Owned,无任何生命周期,可直接转 JSON)
|
||||
use tract_onnx::prelude::{DatumType, Tensor, tract_ndarray};
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum OcrResult {
|
||||
/// 纯文本分支(对应 probability = false)
|
||||
@@ -106,113 +95,40 @@ impl fmt::Display for OcrResult {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ocr {
|
||||
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
||||
pub model_metadata: ModelMetadata,
|
||||
}
|
||||
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<P>(model_path: P, model_metadata: ModelMetadata) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let session = ModelLoader::model_for_path(model_path)?.session;
|
||||
Ok(Self {
|
||||
session,
|
||||
model_metadata,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn model_from_bytes(model_bytes: &[u8], model_metadata: ModelMetadata)->Result<Self, anyhow::Error>{
|
||||
let session = ModelLoader::model_from_bytes(model_bytes)?.session;
|
||||
Ok(Self {
|
||||
session,
|
||||
model_metadata,
|
||||
})
|
||||
}
|
||||
/// 对应 Python 的 _inference
|
||||
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,
|
||||
pub struct Ocr<'a> {
|
||||
pub(crate) session: &'a OcrSession,
|
||||
pub(crate) png_fix: bool,
|
||||
pub(crate) 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 {
|
||||
ocr,
|
||||
|
||||
pub fn new(session: &'a OcrSession) -> Self {
|
||||
Ocr {
|
||||
session,
|
||||
png_fix: false, // 默认值
|
||||
probability: false,
|
||||
color_filter: Ok(None),
|
||||
charset_restrict: None,
|
||||
final_color_ranges: Ok(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
|
||||
}
|
||||
|
||||
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);
|
||||
impl<'a> Ocr<'a> {
|
||||
pub fn predict(&self, image: &DynamicImage) -> anyhow::Result<OcrResult> {
|
||||
println!("当前颜色过滤器状态: {:?}", self.final_color_ranges);
|
||||
|
||||
// =====================================================================
|
||||
// 管道节点 1: 颜色过滤流水线
|
||||
// 使用 Cow (Copy-On-Write) 智能指针。
|
||||
// 如果未开启过滤,img_cow 内部只是持有原图的【只读借用】,发生【零内存分配】!
|
||||
// =====================================================================
|
||||
let img_cow = match &self.color_filter {
|
||||
let img_cow = match &self.final_color_ranges {
|
||||
Err(err_msg) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"颜色过滤器初始化失败,全链路短路: {}",
|
||||
@@ -231,7 +147,7 @@ impl<'a> OcrPredictor<'a> {
|
||||
};
|
||||
let tensor = self.preprocess_image(&img_cow)?;
|
||||
|
||||
let raw_tensor = self.ocr.inference(tensor)?;
|
||||
let raw_tensor = self.session.inference(tensor)?;
|
||||
|
||||
// 3. 后处理分流:直接返回 OcrResult
|
||||
let ocr_output = match raw_tensor.datum_type() {
|
||||
@@ -252,7 +168,7 @@ impl<'a> OcrPredictor<'a> {
|
||||
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
|
||||
fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result<Tensor> {
|
||||
// 1. 获取模型元数据配置
|
||||
let meta = &self.ocr.model_metadata;
|
||||
let meta = &self.session.model_metadata;
|
||||
let norm = &meta.normalization; // 获取归一化器
|
||||
|
||||
// A. 修复 PNG 透明背景 (内部逻辑你之前已实现)
|
||||
@@ -340,13 +256,13 @@ impl<'a> OcrPredictor<'a> {
|
||||
// Ok(tensor)
|
||||
}
|
||||
}
|
||||
impl<'a> OcrPredictor<'a> {
|
||||
impl<'a> Ocr<'a> {
|
||||
fn is_valid_indices(&self, idx: usize) -> bool {
|
||||
if idx >= self.ocr.model_metadata.charset.size() {
|
||||
if idx >= self.session.model_metadata.charset.size() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &self.charset_restrict {
|
||||
match &self.final_charset_indices {
|
||||
Some(v) => v.binary_search(&idx).is_ok(),
|
||||
None => true,
|
||||
}
|
||||
@@ -354,9 +270,9 @@ impl<'a> OcrPredictor<'a> {
|
||||
/// 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印
|
||||
/// 这里的 &str 完美借用了自 tokens,依然是彻底的零拷贝!
|
||||
pub fn valid_tokens(&self) -> Vec<&str> {
|
||||
let charset = &self.ocr.model_metadata.charset;
|
||||
let charset = &self.session.model_metadata.charset;
|
||||
let tokens = &charset.tokens;
|
||||
match &self.charset_restrict {
|
||||
match &self.final_charset_indices {
|
||||
Some(indices) => indices
|
||||
.iter()
|
||||
.filter_map(|&idx| tokens.get(idx).map(|cow| cow.as_ref()))
|
||||
@@ -366,9 +282,9 @@ impl<'a> OcrPredictor<'a> {
|
||||
}
|
||||
}
|
||||
pub fn valid_size(&self) -> usize {
|
||||
match &self.charset_restrict {
|
||||
match &self.final_charset_indices {
|
||||
Some(indices) => indices.len(),
|
||||
None => self.ocr.model_metadata.charset.tokens.len(),
|
||||
None => self.session.model_metadata.charset.tokens.len(),
|
||||
}
|
||||
}
|
||||
/// 变体 B 核心处理器:单次遍历 2D 视图,融合计算 Softmax、Argmax、置信度并输出概率大包
|
||||
@@ -516,7 +432,7 @@ impl<'a> OcrPredictor<'a> {
|
||||
/// 获取有效字符索引列表 (用于外部验证或过滤)
|
||||
fn ctc_decode_to_string(&self, predicted_indices: &[i64]) -> String {
|
||||
println!("indices模型输出原始数据: {:?}", predicted_indices);
|
||||
let charset = &self.ocr.model_metadata.charset;
|
||||
let charset = &self.session.model_metadata.charset;
|
||||
let tokens = &charset.tokens;
|
||||
// let valid_indices = &charset.valid_indices;
|
||||
|
||||
@@ -544,7 +460,7 @@ impl<'a> OcrPredictor<'a> {
|
||||
|
||||
// 史诗级加速点:如果是 None,说明没限制,根本不进入分支,直接放行!
|
||||
// 只有当有具体限制(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() {
|
||||
continue;
|
||||
}
|
||||
10
src/models/ocr/mod.rs
Normal file
10
src/models/ocr/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
mod builder;
|
||||
mod executor;
|
||||
mod session;
|
||||
pub mod charset;
|
||||
pub mod model_metadata;
|
||||
pub mod color_filter;
|
||||
|
||||
pub use builder::OcrBuilder;
|
||||
pub use executor::{Ocr, OcrResult};
|
||||
pub use session::OcrSession;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::charset::{CHARSET_BETA, CHARSET_OLD, Charset};
|
||||
use crate::models::ocr::charset::{CHARSET_BETA, CHARSET_OLD, Charset};
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Cow;
|
||||
53
src/models/ocr/session.rs
Normal file
53
src/models/ocr/session.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::models::ocr::model_metadata::ModelMetadata;
|
||||
use crate::models::loader::{ModelLoader, ModelSession, ModelType};
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use tract_onnx::prelude::{tvec, Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp};
|
||||
|
||||
pub struct OcrSession {
|
||||
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
||||
pub model_metadata: ModelMetadata,
|
||||
}
|
||||
impl ModelSession for OcrSession {
|
||||
fn get_model_type(&self) -> ModelType {
|
||||
todo!("使用thiserror作为错误处理的库,thiserror 专门用于开发库(Library)");
|
||||
}
|
||||
fn desc(&self) -> String {
|
||||
"Ocr Model 加载成功".to_string()
|
||||
}
|
||||
}
|
||||
impl OcrSession {
|
||||
pub fn new<P>(model_path: P, model_metadata: ModelMetadata) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let session = ModelLoader::model_for_path(model_path)?.session;
|
||||
Ok(Self {
|
||||
session,
|
||||
model_metadata,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn model_from_bytes(
|
||||
model_bytes: &[u8],
|
||||
model_metadata: ModelMetadata,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let session = ModelLoader::model_from_bytes(model_bytes)?.session;
|
||||
Ok(Self {
|
||||
session,
|
||||
model_metadata,
|
||||
})
|
||||
}
|
||||
/// 对应 Python 的 _inference
|
||||
pub fn inference(&self, tensor: Tensor) -> anyhow::Result<Tensor> {
|
||||
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
|
||||
// let result = self.ocr.run(tvec!(tensor.into()))?;
|
||||
let mut result = self
|
||||
.session
|
||||
.run(tvec!(tensor.into()))
|
||||
.context("执行模型推理失败")?;
|
||||
println!("模型输出原始数据: {:?}", result);
|
||||
Ok(result.swap_remove(0).into_tensor())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod image_io;
|
||||
pub mod image_processor;
|
||||
pub mod cv_ops;
|
||||
pub mod color_filter;
|
||||
@@ -1,4 +1,4 @@
|
||||
use ddddocr_rs::{Ocr, Slider, Detector, ModelMetadata}; // 假设你的包名是这个
|
||||
use ddddocr_rs::{Ocr, Slider, Detector, ModelMetadata, OcrSession, DetBuilder, DetSession}; // 假设你的包名是这个
|
||||
use image::{DynamicImage, Rgb};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
@@ -64,20 +64,20 @@ fn save_debug_image(
|
||||
#[test]
|
||||
fn test_full_classification() {
|
||||
// 1. 初始化模型
|
||||
let ocr = Ocr::new("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",ModelMetadata::from_builtin_beta()).expect("模型加载失败");
|
||||
let ocr = OcrSession::new("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",ModelMetadata::from_builtin_beta()).expect("模型加载失败");
|
||||
|
||||
// 2. 加载测试图片
|
||||
let img = image::open("samples/code2.png").expect("测试图片不存在");
|
||||
|
||||
// 3. 执行识别
|
||||
let result = ocr.predictor().predict(&img).expect("识别过程出错").into_text();
|
||||
let result = Ocr::new(&ocr).predict(&img).expect("识别过程出错").into_text();
|
||||
|
||||
println!("识别结果: {}", result);
|
||||
assert!(!result.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn test_det_load() -> anyhow::Result<()> {
|
||||
let det = Detector::new("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_det.onnx")?;
|
||||
let det = DetSession::new("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_det.onnx")?;
|
||||
let image_path = "samples/det1.png";
|
||||
let image_bytes =
|
||||
fs::read(image_path).map_err(|e| anyhow::anyhow!("无法读取图片 {}: {}", image_path, e))?;
|
||||
@@ -89,8 +89,8 @@ fn test_det_load() -> anyhow::Result<()> {
|
||||
.map_err(|e| anyhow::anyhow!("图片解码失败: {}", e))?;
|
||||
|
||||
// 【修改点 2】传入统一的 &DynamicImage 引用
|
||||
let bboxes = det.predict(&img)?;
|
||||
println!("{:?}", det);
|
||||
let bboxes = Detector::new(&det).predict(&img)?;
|
||||
// println!("{:?}", det);
|
||||
println!("检测到的目标数量: {}", bboxes.len());
|
||||
|
||||
if bboxes.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user