docs(core): 为 ddddocr-core 全量补充并精简 Rustdoc 文档

This commit is contained in:
2026-08-05 17:40:32 +08:00
parent 84cc97b201
commit 0bddaeba24
17 changed files with 130 additions and 121 deletions

View File

@@ -1,3 +1,5 @@
//! 目标检测模块:检测器构建器与执行入口。
mod builder;
mod executor;

View File

@@ -1,11 +1,15 @@
//! 检测器构建器。
use crate::det::executor::Detector;
// use ddddocr_tract::det::session::DetSession;
use crate::traits::DetEngine;
/// 检测器构建器,通过 [`crate::Detector::builder`] 创建。
#[derive(Default)]
pub struct DetBuilder;
impl DetBuilder {
fn build_with<E: DetEngine>(self, session: &E) -> Detector<'_> {
Detector { session }
fn build_with<E: DetEngine>(self, runtime: &E) -> Detector<'_> {
Detector { runtime }
}
}

View File

@@ -1,3 +1,5 @@
//! 目标检测执行器:检测后处理与预测入口。
use crate::error::{Result, TensorError};
use image::{DynamicImage, GenericImageView, imageops::FilterType};
use ndarray::{Array2, Array3, Array4, Axis, prelude::*, s};
@@ -7,6 +9,8 @@ use std::fmt;
// use ddddocr_tract::det::session::DetSession;
use crate::{DetBuilder, DetOutput, OcrBuilder};
use crate::traits::DetEngine;
/// 目标检测结果:原图像素坐标系下的边界框、置信度与类别 ID。
#[derive(Debug, Clone, Copy)]
pub struct DetectionResult {
pub x1: i32,
@@ -28,24 +32,25 @@ impl fmt::Display for DetectionResult {
}
}
/// 目标检测器:对输入图像执行检测并返回结果。
pub struct Detector<'a> {
pub(crate) session: &'a dyn DetEngine,
pub(crate) runtime: &'a dyn DetEngine,
}
impl<'a> Detector<'a> {
pub fn new(session: &'a dyn DetEngine) -> Self {
Detector { session }
pub fn new(runtime: &'a dyn DetEngine) -> Self {
Detector { runtime }
}
pub fn builder() -> DetBuilder {
DetBuilder::default()
}
}
impl<'a> Detector<'a> {
/// 对输入图像执行目标检测,返回检测框列表。
pub fn predict(&self, image: &DynamicImage) -> Result<Vec<DetectionResult>> {
// Rust 中通常在调用层处理文件/PIL转换这里直接进入核心逻辑
Ok(self.get_bbox(image)?)
}
/// 2. preproc: 纯 Rust 实现 (替代 OpenCV)
fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> (Array4<f32>, f32) {
let (target_h, target_w) = input_size;
let (img_w, img_h) = image.dimensions();
@@ -87,7 +92,6 @@ impl<'a> Detector<'a> {
(array, r)
}
/// 3. demo_postprocess (逻辑与 Python 一致)
fn demo_postprocess(&self, mut outputs: Array3<f32>, img_size: (i32, i32)) -> Array3<f32> {
let strides = [8, 16, 32];
@@ -123,7 +127,6 @@ impl<'a> Detector<'a> {
outputs
}
/// 4. nms
fn nms(&self, boxes: &Array2<f32>, scores: &Array1<f32>, nms_thr: f32) -> Vec<usize> {
let mut keep = Vec::new();
let x1 = boxes.column(0);
@@ -183,8 +186,7 @@ impl<'a> Detector<'a> {
keep
}
/// 5. multiclass_nms
//multiclass_nms_class_agnostic
/// 多类别 NMS 后处理:按分数阈值筛选候选框,并用 NMS 阈值去重。
pub fn multiclass_nms(
&self,
boxes: &Array2<f32>, // [25200, 4] -> xyxy 格式
@@ -241,7 +243,7 @@ impl<'a> Detector<'a> {
})
.collect()
}
/// 6. get_bbox (完全解耦 OpenCV)
/// 对图像执行完整检测流程(预处理、推理、后处理),返回像素坐标系下的检测框。
pub fn get_bbox(
&self,
dynamic_img: &DynamicImage,
@@ -254,7 +256,7 @@ impl<'a> Detector<'a> {
// tract 推理
// let outputs = self.session.session.run(tvec!(input_tensor.into()))?;
let outputs = self.session.inference(input_tensor)?;
let outputs = self.runtime.inference(input_tensor)?;
// let output_array = outputs[0]
// 2. 无缝、安全地解包出标准 3维 矩阵
let DetOutput::Detection(output_array) = outputs;

View File

@@ -1,3 +1,5 @@
//! OCR 模块:识别器构建器、执行入口及元数据、字符集等类型。
mod builder;
mod charset;
mod color_filter;

View File

@@ -1,8 +1,12 @@
//! OCR 构建器。
use crate::ocr::executor::Ocr;
// use ddddocr_tract::session::OcrSession;
use crate::traits::OcrEngine;
use crate::ocr::color_filter::ColorFilter;
use crate::ocr::token_filter::TokenFilter;
/// OCR 构建器:配置识别选项后绑定引擎会话构建 [`crate::Ocr`]。
#[derive(Default)]
pub struct OcrBuilder {
/// 是否修复PNG格式问题
@@ -49,6 +53,7 @@ impl OcrBuilder {
self.charset_restrict = Some(Box::new(restrict));
self
}
/// 绑定引擎会话并构建 OCR 识别器。
pub fn build_with<E: OcrEngine>(self, runtime: &E) -> Ocr<'_> {
// 1. 原地解析颜色过滤器
let final_color_ranges = match &self.color_filter {

View File

@@ -1,9 +1,12 @@
//! 字符集token 列表与索引的双向映射。
use std::borrow::Cow;
use std::collections::HashMap;
// ==========================================
// 3. 字符集核心结构体 (重命名为 Charset)
// ==========================================
/// 字符集token 列表与索引的双向映射。
#[derive(Debug, Clone)]
pub struct Charset {
// 使用 Cow 统一静态切片和动态读取的 Vec<String>,内部实现真正的零拷贝
@@ -32,7 +35,7 @@ impl Charset {
// --- 业务策略方法 ---
/// 字符转索引,不存在返回 -1 (保持与原 Python 行为一致)
/// 字符转索引,不存在返回 -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
@@ -41,7 +44,7 @@ impl Charset {
}
}
/// 索引转字符引用,零拷贝。若越界返回 None
/// 索引转字符引用,越界返回 `None`。
pub fn index_to_char_ref(&self, index: usize) -> Option<&str> {
self.tokens.get(index).map(|cow| cow.as_ref())
}

View File

@@ -1,9 +1,10 @@
//! 颜色过滤HSV 区间匹配与颜色预设。
use crate::error::{ImagePreprocessError, Result};
use crate::utils::image_processor::rgb_to_opencv_hsv;
use image::{DynamicImage, ImageBuffer, Rgb};
use std::str::FromStr;
/// 核心区间判定辅助函数
#[inline(always)]
fn is_pixel_matched(ranges: &[HsvRange], h: u8, s: u8, v: u8) -> bool {
ranges.iter().any(|range| {
@@ -15,6 +16,7 @@ fn is_pixel_matched(ranges: &[HsvRange], h: u8, s: u8, v: u8) -> bool {
&& v <= range.upper.2
})
}
/// 按 HSV 区间过滤图像:未命中任一区间的像素刷白。
pub fn apply_to_image(
image: &DynamicImage,
hsv_ranges: &[HsvRange],
@@ -58,6 +60,7 @@ pub fn apply_to_image(
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
/// HSV 颜色区间,下界与上界各为 `(H, S, V)`。
pub struct HsvRange {
pub lower: (u8, u8, u8), // (H, S, V)
pub upper: (u8, u8, u8), // (H, S, V)
@@ -69,8 +72,7 @@ impl HsvRange {
}
}
impl HsvRange {
/// 验证当前 HSV 范围是否合法
/// 对应 Python 逻辑H 在 0-180S/V 在 0-255且下界 <= 上界
/// 校验区间是否合法H 0-180S/V 0-255且下界 <= 上界)。
pub fn validate(&self) -> Result<(), ImagePreprocessError> {
// 1. 校验 H 通道边界 (OpenCV 中 H 范围是 0-180)
if self.lower.0 > 180 || self.upper.0 > 180 {
@@ -93,6 +95,7 @@ impl HsvRange {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// 颜色预设:常见颜色对应的 HSV 区间集合。
pub enum ColorPreset {
Red,
Blue,
@@ -108,9 +111,7 @@ pub enum ColorPreset {
}
impl ColorPreset {
/// 纯裸数据定义,没有任何结构体包装,干净利落
/// 返回值:(范围数量, 范围数组)
/// 完美的零成本抽象:利用常量提升将数据直接打入只读数据段 (.rodata)
/// 返回预设对应的 HSV 区间列表。
pub fn matches(&self) -> &[HsvRange] {
match self {
ColorPreset::Red => &[
@@ -162,7 +163,7 @@ impl ColorPreset {
ColorPreset::Custom(ranges) => ranges,
}
}
/// 校验逻辑:在这里实现完美的“责任分离”
/// 校验预设的 HSV 区间是否合法。
pub fn validate(&self) -> Result<(), ImagePreprocessError> {
match self {
// 1. 快捷变体完全绕过根本不校验0 运行时开销放行!
@@ -202,23 +203,20 @@ impl FromStr for ColorPreset {
// 3. 颜色约束特征Trait与组合子设计模式
// =====================================================================
/// 颜色匹配上下文:当前像素的 HSV 值。
pub struct PixelCtx {
pub hsv: (u8, u8, u8),
}
/// 统一的颜色约束接口
/// 颜色过滤约束接口:提供一组 HSV 区间。
pub trait ColorFilter {
/// 将自身的有效约束平铺追加到统一的目标容器中
fn append_ranges(&self, target: &mut Vec<HsvRange>);
/// 预估范围数量,借助原生内置的 len() 实现 O(1) 完美控容
fn estimated_count(&self) -> usize;
/// 将自身的有效约束平铺追加到统一目标容器中
/// 验证当前过滤器是否合法默认直接放行Ok(())
/// 验证过滤器配置是否合法,默认直接放行。
fn validate_self(&self) -> Result<(), ImagePreprocessError> {
Ok(())
}
/// 【新扩展的架构方法】将自身安全的合并到已有的普通容器中,并完成去重和排序
/// 完美的责任分离Builder 不再需要关心怎么分配内存、怎么排序去重
/// 收集全部有效区间并排序去重;无有效区间时返回 `None`。
fn collect_to_vec(&self) -> Result<Option<Vec<HsvRange>>, ImagePreprocessError> {
// 1. 触发自检
self.validate_self()?;
@@ -288,6 +286,7 @@ impl<'a> ColorFilter for MultiOrColorRestrict<'a> {
// 4. 声明式宏:一语定乾坤
// =====================================================================
/// 组合多个颜色过滤器为「或」关系的快捷宏。
#[macro_export]
macro_rules! color_any_of {
($only:expr) => {

View File

@@ -1,41 +1,37 @@
//! OCR 执行器:预测入口与结果类型。
use crate::ocr::metadata::Resize;
use crate::ocr::color_filter::{HsvRange, apply_to_image};
// use ddddocr_tract::session::{ModelOutput, OcrSession};
use crate::error::{ImagePreprocessError, Result, TensorError};
use crate::traits::OcrEngine;
use crate::utils::image_convert::png_rgba_white_preprocess;
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
use crate::{OcrBuilder, OcrOutput};
use image::DynamicImage;
use ndarray::ArrayView2;
use std::borrow::Cow;
use std::fmt;
// use tract_onnx::prelude::tract_ndarray::{ Ix2, s};
// use tract_onnx::prelude::{DatumType, Tensor, tract_ndarray};
// !!!【核心纠正】:彻底弃用 tract_ndarray全线转用标准 ndarray
use ndarray::ArrayView2;
// pub enum ModelOutput {
// Indices(ndarray::Array1<i64>), // 拥有完整所有权的 1维数组可任意传递和返回
// Logits(ndarray::Array2<f32>), // 拥有完整所有权的 2维矩阵可任意传递和返回
// }
use crate::error::{ImagePreprocessError, Result, TensorError};
use crate::{OcrBuilder, OcrOutput};
use crate::traits::OcrEngine;
use tracing::{ warn};
use tracing::warn;
/// OCR 识别结果:纯文本或携带概率的文本。
#[derive(Debug, Clone)]
pub enum OcrResult {
/// 纯文本分支(对应 probability = false
/// 纯文本结果(`probability = false` 时返回)。
Text(String),
/// 包含全量概率的分支(对应 probability = true
/// 携带概率的结果(`probability = true` 时返回)。
Probability {
text: String,
/// 满额概率矩阵 [Steps, Classes]
/// 全量概率矩阵 `[Steps, Classes]`。
probabilities: Vec<Vec<f32>>,
/// 全局平均置信度
/// 全局平均置信度
confidence: f64,
},
/// 不支持的模型或未知输出
/// 不支持的模型或未知输出
Unsupported { message: String },
}
impl OcrResult {
/// 消费自身,直接提取最终文本
/// 消费自身提取最终文本
pub fn into_text(self) -> String {
match self {
OcrResult::Text(text) => text,
@@ -103,6 +99,7 @@ impl fmt::Display for OcrResult {
}
}
/// OCR 识别器:预处理、推理调度与后处理解码。
pub struct Ocr<'a> {
pub(crate) runtime: &'a dyn OcrEngine,
pub(crate) png_fix: bool,
@@ -129,9 +126,9 @@ impl<'a> Ocr<'a> {
pub fn builder() -> OcrBuilder {
OcrBuilder::default()
}
}
impl<'a> Ocr<'a> {
/// 对输入图像执行 OCR 识别并返回结果。
pub fn predict(&self, image: &DynamicImage) -> Result<OcrResult> {
println!("当前颜色过滤器状态: {:?}", self.final_color_ranges);
@@ -179,9 +176,10 @@ impl<'a> Ocr<'a> {
let ocr_output = self.process_model_output(raw_tensor)?;
Ok(ocr_output)
}
/// 对应 Python 的 _preprocess_image
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
fn preprocess_image(&self, img: &DynamicImage) -> Result<ndarray::Array4<f32>,ImagePreprocessError> {
fn preprocess_image(
&self,
img: &DynamicImage,
) -> Result<ndarray::Array4<f32>, ImagePreprocessError> {
// 1. 获取模型元数据配置
let meta = self.runtime.metadata();
let norm = &meta.normalization; // 获取归一化器
@@ -255,30 +253,10 @@ impl<'a> Ocr<'a> {
}
};
Ok(array4)
// Ok(tensor)
// let h = 64u32;
// let w = (current_img.width() as f32 * (h as f32 / current_img.height() as f32)) as u32;
// let gray_img = convert_to_grayscale(&current_img);
// let resized = resize_image(&gray_img, w, h);
// // resized.save("debug_preprocessed.png").unwrap();
// // 1. 预处理:转灰度 -> Resize -> 归一化
// // let resized = img.resize_exact(w, h, FilterType::Lanczos3).to_luma8();
//
// // 使用 tract_ndarray 构造,避免版本冲突
// let array =
// tract_ndarray::Array4::from_shape_fn((1, 1, h as usize, w as usize), |(_, _, y, x)| {
// let pixel = resized.get_pixel(x as u32, y as u32)[0] as f32;
// (pixel / 255.0 - 0.5) / 0.5
// });
//
// let tensor = Tensor::from(array);
//
// Ok(tensor)
}
// 这段代码未来直接放入 ddddocr-core
fn process_model_output(&self, output: OcrOutput) -> Result<OcrResult,TensorError> {
fn process_model_output(&self, output: OcrOutput) -> Result<OcrResult, TensorError> {
match output {
OcrOutput::Indices(array1) => {
// 对应你原来的 process_i64_tensor
@@ -340,8 +318,7 @@ impl<'a> Ocr<'a> {
None => true,
}
}
/// 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印
/// 这里的 &str 完美借用了自 tokens依然是彻底的零拷贝
/// 返回当前生效的可用 token 列表。
pub fn valid_tokens(&self) -> Vec<&str> {
let charset = &self.runtime.metadata().charset;
let tokens = &charset.tokens;
@@ -360,7 +337,6 @@ impl<'a> Ocr<'a> {
None => self.runtime.metadata().charset.tokens.len(),
}
}
/// 变体 B 核心处理器:单次遍历 2D 视图,融合计算 Softmax、Argmax、置信度并输出概率大包
fn compute_f32_full_probability(
&self,
matrix_view: ArrayView2<f32>,
@@ -502,7 +478,6 @@ impl<'a> Ocr<'a> {
// Ok(OcrResult::Text(final_text))
// }
// }
/// 获取有效字符索引列表 (用于外部验证或过滤)
fn ctc_decode_to_string(&self, predicted_indices: &[i64]) -> String {
println!("indices模型输出原始数据: {:?}", predicted_indices);
let charset = &self.runtime.metadata().charset;
@@ -542,8 +517,7 @@ impl<'a> Ocr<'a> {
// 5. 字符映射
if let Some(char_str) = tokens.get(u_idx) {
res.push_str(char_str);
}
else {
} else {
warn!("警告: 预测索引 {} 超出字符集范围", u_idx);
}
}

View File

@@ -1,3 +1,5 @@
//! OCR 模型元数据:归一化、缩放策略与模型信息。
// =====================================================================
// 1. 辅助定义的枚举与结构体
// =====================================================================
@@ -5,6 +7,7 @@
use crate::ocr::Charset;
use std::borrow::Cow;
/// 像素归一化策略。
#[derive(Debug, Clone, Copy)]
pub enum Normalization {
/// 映射到 [0.0, 1.0] -> pixel / 255.0
@@ -14,7 +17,6 @@ pub enum Normalization {
}
impl Normalization {
/// 统一归一化计算逻辑
#[inline(always)]
pub fn normalize(&self, pixel: f32) -> f32 {
match self {
@@ -35,17 +37,15 @@ pub enum Resize {
Square(u32),
}
/// OCR 模型元数据:字符集、缩放策略、通道数与归一化配置。
#[derive(Debug, Clone)]
pub struct ModelMetadata {
/// 字符集管理器
pub charset: Charset,
/// 是否为单字识别模型
pub word: bool,
/// 预处理的缩放策略
pub resize: Resize,
/// 图像通道数 (1 或 3)
/// 图像通道数1 或 3
pub channel: u8,
/// 新增:传递给核心业务使用的归一化配置
pub normalization: Normalization,
}
@@ -66,7 +66,7 @@ impl ModelMetadata {
}
}
// --- 优雅的工厂模式构造器 ---
/// 通用的静态切片转换构造器
/// 从静态字符切片构建元数据并自动生成字符集。
pub fn from_static_slice(
slice: &[&'static str],
word: bool,

View File

@@ -1,20 +1,22 @@
//! 字符集限制:按字符属性或索引过滤识别范围。
use std::borrow::Cow;
/// 字符集范围限制枚举
/// 字符集校验上下文:当前 token 的文本与索引。
pub struct ValidationCtx<'a> {
pub text: &'a str, // 当前 Token 的文本内容
pub token_id: usize, // 当前 Token 的 ID 索引
}
/// 统一的约束接口
/// 字符集限制接口:决定某个 token 是否放行。
pub trait TokenFilter {
fn matches(&self, ctx: &ValidationCtx) -> bool;
/// 预估容量提示,帮助精准开辟 Vec 内存
/// 预估匹配数量的容量提示。
fn estimated_capacity(&self) -> usize {
128
}
/// 统一接管全量字符集的密集遍历、CTC Blank放行、去重、排序及空交集退化兜底
/// 遍历全量字符集筛选可用索引(放行 CTC blank、排序去重、空交集返回 `None`)。
fn apply_to_charset(&self, tokens: &[Cow<str>]) -> Option<Vec<usize>> {
let mut has_any_match = false;
let estimated_capacity = self.estimated_capacity();
@@ -58,6 +60,7 @@ pub trait TokenFilter {
}
}
/// 按字符属性限制:数字、大小写字母或自定义列表。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CharRestrict {
Digit,
@@ -84,6 +87,7 @@ impl TokenFilter for CharRestrict {
}
}
/// 按索引限制:前 N 个、索引范围或索引列表。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdRestrict {
TopN(usize),
@@ -128,7 +132,7 @@ impl<'a> TokenFilter for MultiOrRestrict<'a> {
}
}
/// 声明式宏:解决组合扩展痛苦
/// 组合多个字符限制规则为「或」关系的快捷宏。
#[macro_export]
macro_rules! any_of {
// 场景 A如果用户只传了一个规则免去构建 Vec 的开销,直接返回其引用

View File

@@ -1,3 +1,5 @@
//! 滑块验证码匹配:模板匹配与差异比较两种模式。
use crate::error::{ImagePreprocessError, Result};
use crate::utils::image_convert::{ColorMode, image_to_ndarray};
use crate::utils::image_processor;
@@ -12,8 +14,11 @@ use imageproc::region_labelling::{Connectivity, connected_components};
use imageproc::template_matching::{MatchTemplateMethod, match_template};
use ndarray::{ArrayView2, ArrayView3};
use std::fmt;
/// 滑块匹配结果:检测中心坐标与置信度。
#[derive(Debug)]
pub struct SlideResult {
/// 检测中心坐标 `[x, y]`。
pub target: [i32; 2],
pub target_x: i32,
pub target_y: i32,
@@ -29,13 +34,14 @@ impl fmt::Display for SlideResult {
}
}
/// 滑块匹配服务:提供模板匹配与差异比较两种识别模式。
pub struct Slider;
impl Slider {
pub fn new() -> Self {
Self
}
/// 对应 Python: slide_match 滑块匹配接口
/// 模板匹配滑块:在背景图中定位滑块中心(对应 Python 的 `slide_match`)。
pub fn slide_match(
&self,
target_image: &DynamicImage,
@@ -48,8 +54,7 @@ impl Slider {
self.perform_slide_match(target_array.view(), background_array.view(), simple_target)
.map_err(Into::into)
}
/// 对应 Python: slide_comparison 差异比较接口
/// 用于比较带坑位的图片与原始背景图,定位差异点
/// 差异比较滑块:对比带坑位的图与原始背景图,定位差异中心(对应 Python 的 `slide_comparison`)。
pub fn slide_comparison(
&self,
target_image: &DynamicImage,
@@ -63,7 +68,7 @@ impl Slider {
self.perform_slide_comparison(target_array.view(), background_array.view())
.map_err(Into::into)
}
/// 对应 Python: _perform_slide_comparison
/// 差异比较核心实现(对应 Python 的 `_perform_slide_comparison`)。
pub fn perform_slide_comparison(
&self,
target: ArrayView3<u8>,
@@ -133,7 +138,7 @@ impl Slider {
}
}
/// 对应 Python: _perform_slide_match
/// 模板匹配核心实现(对应 Python 的 `_perform_slide_match`)。
// 在 SlideEngine 中修改此入口进行测试
fn perform_slide_match(
&self,
@@ -180,9 +185,7 @@ impl Slider {
Ok(self.edge_based_match(target_gray.view(), background_gray.view()))
}
}
/// 对应 Python: _simple_template_match
/// 使用 SAD (Sum of Absolute Differences) 算法
/// 核心模板匹配SAD + 有效像素过滤
/// 简单模式模板匹配:直接对灰度图做归一化互相关(对应 Python 的 `_simple_template_match`)。
fn simple_template_match(
&self,
target: ArrayView2<u8>,
@@ -221,8 +224,7 @@ impl Slider {
}
}
/// 对应 Python: _edge_based_match
/// 基于边缘检测的滑块匹配 (对齐 Python _edge_based_match)
/// 边缘模式模板匹配:基于 Canny 边缘检测后再匹配(对应 Python 的 `_edge_based_match`)。
pub fn edge_based_match(
&self,
target: ArrayView2<u8>,

View File

@@ -1,19 +1,20 @@
//! 推理引擎统一抽象接口。
use crate::error::TensorError;
use crate::types::{ModelInfo, TensorInfo};
use crate::{DetOutput, ModelMetadata, OcrOutput};
use std::path::Path;
/// 查询模型输入/输出信息的接口。
pub trait Info {
fn input_info(&self) -> crate::error::Result<Vec<TensorInfo>>;
fn output_info(&self) -> crate::error::Result<Vec<TensorInfo>>;
fn model_info(&self) -> crate::error::Result<ModelInfo>;
}
/// 核心层定义的统一推理引擎接口
/// 未来的 ddddocr-tract 和 ddddocr-ort 都必须实现这个 Trait
/// 推理引擎统一抽象接口,由 ddddocr-tract、ddddocr-ort 等引擎 crate 实现
pub trait InferenceEngine {
/// 关联类型:具体的 Session 需要声明自己到底产出什么枚举
/// 引擎产出的输出枚举OCR 为 [`crate::OcrOutput`],检测为 [`crate::DetOutput`])。
type Output;
fn inference(
&self,
@@ -21,12 +22,15 @@ pub trait InferenceEngine {
) -> crate::error::Result<Self::Output, TensorError>;
}
/// OCR 引擎接口:输出 [`crate::OcrOutput`],并提供模型元数据。
pub trait OcrEngine: InferenceEngine<Output = OcrOutput> + Info {
fn metadata(&self) -> &ModelMetadata;
}
/// 目标检测引擎接口:输出 [`crate::DetOutput`]。
pub trait DetEngine: InferenceEngine<Output = DetOutput> {}
/// 模型加载器:从本地路径或字节流构建引擎会话。
pub trait Loader {
type Session;
type Error;

View File

@@ -1,3 +1,5 @@
//! 图像与张量工具:加载、格式转换、图像处理与归一化。
pub mod image_convert;
mod image_helper;
pub mod image_processor;

View File

@@ -1,7 +1,10 @@
//! 图像格式转换DynamicImage 与 ndarray 数组互转。
use crate::error::{ImagePreprocessError, Result};
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
use ndarray::{Array3, ArrayViewD};
/// 图像通道模式。
#[derive(Debug)]
pub enum ColorMode {
RGB,
@@ -9,7 +12,7 @@ pub enum ColorMode {
L,
}
/// 封装数组转图像的逻辑,
/// 将 ndarray 数组转换为图像(自动识别 HWC 通道数)。
// 对应 Python 版 _numpy_to_pil_image
pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage,ImagePreprocessError> {
let shape = array.shape();

View File

@@ -1,3 +1,5 @@
//! 图像输入源从路径、Base64、字节等统一加载图片。
use crate::error::{DdddError, Result};
use crate::utils::image_convert::ndarray_to_hwc_image;
use base64::{engine::general_purpose, Engine as _};
@@ -8,8 +10,9 @@ use std::fmt::{Debug, Formatter};
use std::fs;
use std::path::Path;
use std::path::PathBuf;
/// Base64 字符串包装。
pub struct Base64<'a>(pub &'a str);
/// 专属图像输入源转换器
/// 统一图像输入源,包装加载完成的图像。
pub struct ImageSource {
inner: DynamicImage,
}
@@ -21,6 +24,7 @@ impl ImageSource {
}
}
/// 从多种输入类型转换为 [`ImageSource`] 的转换接口。
pub trait TryFromImage<T>: Sized {
// 唯一的转换入口,通过目标类型来调用
fn try_from_image(value: T) -> Result<Self>;
@@ -109,7 +113,7 @@ impl<'a> TryFromImage<Base64<'a>> for ImageSource {
}
}
/// 模拟 Python 的 load_image_from_input
/// 从任意受支持的输入类型加载图像。
#[allow(dead_code)]
pub fn load_image_from_input<I>(input: I) -> Result<DynamicImage>
where
@@ -119,7 +123,7 @@ where
Ok(img)
}
/// 将base64编码的图片转换为 DynamicImage
/// 将 Base64 编码的图片转换为 DynamicImage
pub fn base64_to_image(b64_str: &str) -> Result<DynamicImage> {
// 过滤掉可能存在的 base64 前缀,例如 "data:utils/png;base64,"
let clean_b64 = if let Some(pos) = b64_str.find(",") {

View File

@@ -1,10 +1,12 @@
//! 图像处理算法OpenCV 风格的常用函数封装。
use image::{imageops::FilterType, DynamicImage, GrayImage, ImageBuffer, Luma};
use ndarray::{azip, Array2, Array3, ArrayView2, ArrayView3};
use std::cmp::{max, min};
// 模拟openCV
/// 1. 计算两个数组的绝对差值 (对应 cv2.absdiff)
/// 计算两个 HWC 数组的绝对差值对应 cv2.absdiff)。
pub fn abs_diff(a: &ArrayView3<u8>, b: &ArrayView3<u8>) -> Array3<u8> {
// 利用 ndarray 的 map_collect生成差值的绝对值数组
// 或者直接使用 zip_mut_with 处理以减少内存分配
@@ -27,7 +29,7 @@ pub fn rgb_to_gray(rgb: ArrayView3<u8>) -> Array2<u8> {
})
}
/// 找匹配结果图中的最大值及其坐标 (模拟 cv2.minMaxLoc 的一部分)
/// 找匹配结果图中的最大值及其坐标(对应 cv2.minMaxLoc)。
pub fn min_max_loc(result_map: &ImageBuffer<Luma<f32>, Vec<f32>>) -> (f32, (u32, u32)) {
// 4. 找到最佳匹配位置 (对齐 cv2.minMaxLoc)
let mut max_val: f32 = -1.0;
@@ -48,8 +50,7 @@ pub fn min_max_loc(result_map: &ImageBuffer<Luma<f32>, Vec<f32>>) -> (f32, (u32,
(max_val, max_loc)
}
/// 1. 模拟 findContours 并获取最大面积区域的 Label
/// 返回 Option<u32>,如果找不到任何区域则返回 None
/// 模拟 findContours:返回面积最大的连通域标签,找不到时返回 `None`。
pub fn find_contours_and_max(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>) -> Option<u32> {
// 统计每个标签出现的频率(即面积)
let mut max_label = 0;
@@ -74,9 +75,7 @@ pub fn find_contours_and_max(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>) -> Opt
Some(max_label)
}
}
/// 根据目标连通域标签,计算其在图像中的外接矩形边界框(对应 `cv2.boundingRect`
///
/// 返回格式: `(min_x, min_y, width, height)`
/// 计算指定连通域标签的外接矩形(对应 cv2.boundingRect,返回 `(min_x, min_y, width, height)`。
pub fn bounding_rect(
labelled: &ImageBuffer<Luma<u32>, Vec<u32>>,
max_label: u32,
@@ -109,9 +108,7 @@ pub fn calculate_center(top_left: (u32, u32), width: usize, height: usize) -> (i
(center_x, center_y)
}
/// 高性能转换:将 `ndarray` 2D 灰度视图规整为 `image::ImageBuffer` 格式
///
/// 放弃低效的逐像素显式嵌套循环,采用原生内存池直接构造,减少寻址开销
/// 将 2D 灰度 ndarray 视图转换为灰度 ImageBuffer
pub fn ndarray_to_luma8(array: ArrayView2<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>> {
let (height, width) = array.dim();
// 技巧:直接将已有的规整连续内存打平转换,或用 from_raw 包装
@@ -128,6 +125,7 @@ pub fn ndarray_to_luma8(array: ArrayView2<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>>
// 5. 核心高性能图像转换算法 (纯 Rust 编写)
// =====================================================================
/// RGB 像素转换为 OpenCV 风格的 HSV 值。
#[inline(always)]
pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
// 1. 规避高昂的除法,直接转为 f32 进行比对
@@ -174,15 +172,13 @@ pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
(h_opencv, s_opencv, v_opencv)
}
/// 对应 Python 的 convert_to_grayscale
/// 将图像转换为灰度图 (L模式)
/// 将图像转换为灰度图L 模式)。
pub fn convert_to_grayscale(image: &DynamicImage) -> GrayImage {
// Rust utils 库的 to_luma8 会根据标准的亮度公式进行转换
image.to_luma8()
}
/// 对应 Python 的 resize_image
/// 调整图像尺寸。当前版本仅实现 keep_aspect_ratio=false
/// 按指定宽高调整图像尺寸。
pub fn resize_image(
image: &DynamicImage,
target_width: u32,

View File

@@ -1,7 +1,10 @@
//! 张量变换:将异构形状的模型输出规整为统一格式。
use crate::OcrOutput;
use crate::error::{Result, TensorError};
use ndarray::s;
/// 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵
/// 将异构形状的模型输出规整为标准 `[Steps, Classes]` Logits 矩阵。
pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) -> Result<OcrOutput,TensorError> {
let (steps, classes, data_dyn_view) = match shape.len() {
3 => {