docs(core): 为 ddddocr-core 全量补充并精简 Rustdoc 文档
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
//! 目标检测模块:检测器构建器与执行入口。
|
||||||
|
|
||||||
mod builder;
|
mod builder;
|
||||||
mod executor;
|
mod executor;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
//! 检测器构建器。
|
||||||
|
|
||||||
use crate::det::executor::Detector;
|
use crate::det::executor::Detector;
|
||||||
// use ddddocr_tract::det::session::DetSession;
|
// use ddddocr_tract::det::session::DetSession;
|
||||||
use crate::traits::DetEngine;
|
use crate::traits::DetEngine;
|
||||||
|
|
||||||
|
/// 检测器构建器,通过 [`crate::Detector::builder`] 创建。
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DetBuilder;
|
pub struct DetBuilder;
|
||||||
|
|
||||||
impl DetBuilder {
|
impl DetBuilder {
|
||||||
fn build_with<E: DetEngine>(self, session: &E) -> Detector<'_> {
|
fn build_with<E: DetEngine>(self, runtime: &E) -> Detector<'_> {
|
||||||
Detector { session }
|
Detector { runtime }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! 目标检测执行器:检测后处理与预测入口。
|
||||||
|
|
||||||
use crate::error::{Result, TensorError};
|
use crate::error::{Result, TensorError};
|
||||||
use image::{DynamicImage, GenericImageView, imageops::FilterType};
|
use image::{DynamicImage, GenericImageView, imageops::FilterType};
|
||||||
use ndarray::{Array2, Array3, Array4, Axis, prelude::*, s};
|
use ndarray::{Array2, Array3, Array4, Axis, prelude::*, s};
|
||||||
@@ -7,6 +9,8 @@ use std::fmt;
|
|||||||
// use ddddocr_tract::det::session::DetSession;
|
// use ddddocr_tract::det::session::DetSession;
|
||||||
use crate::{DetBuilder, DetOutput, OcrBuilder};
|
use crate::{DetBuilder, DetOutput, OcrBuilder};
|
||||||
use crate::traits::DetEngine;
|
use crate::traits::DetEngine;
|
||||||
|
|
||||||
|
/// 目标检测结果:原图像素坐标系下的边界框、置信度与类别 ID。
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct DetectionResult {
|
pub struct DetectionResult {
|
||||||
pub x1: i32,
|
pub x1: i32,
|
||||||
@@ -28,24 +32,25 @@ impl fmt::Display for DetectionResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 目标检测器:对输入图像执行检测并返回结果。
|
||||||
pub struct Detector<'a> {
|
pub struct Detector<'a> {
|
||||||
pub(crate) session: &'a dyn DetEngine,
|
pub(crate) runtime: &'a dyn DetEngine,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Detector<'a> {
|
impl<'a> Detector<'a> {
|
||||||
pub fn new(session: &'a dyn DetEngine) -> Self {
|
pub fn new(runtime: &'a dyn DetEngine) -> Self {
|
||||||
Detector { session }
|
Detector { runtime }
|
||||||
}
|
}
|
||||||
pub fn builder() -> DetBuilder {
|
pub fn builder() -> DetBuilder {
|
||||||
DetBuilder::default()
|
DetBuilder::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a> Detector<'a> {
|
impl<'a> Detector<'a> {
|
||||||
|
/// 对输入图像执行目标检测,返回检测框列表。
|
||||||
pub fn predict(&self, image: &DynamicImage) -> Result<Vec<DetectionResult>> {
|
pub fn predict(&self, image: &DynamicImage) -> Result<Vec<DetectionResult>> {
|
||||||
// Rust 中通常在调用层处理文件/PIL转换,这里直接进入核心逻辑
|
// Rust 中通常在调用层处理文件/PIL转换,这里直接进入核心逻辑
|
||||||
Ok(self.get_bbox(image)?)
|
Ok(self.get_bbox(image)?)
|
||||||
}
|
}
|
||||||
/// 2. preproc: 纯 Rust 实现 (替代 OpenCV)
|
|
||||||
fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> (Array4<f32>, f32) {
|
fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> (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();
|
||||||
@@ -87,7 +92,6 @@ impl<'a> Detector<'a> {
|
|||||||
(array, r)
|
(array, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 3. demo_postprocess (逻辑与 Python 一致)
|
|
||||||
fn demo_postprocess(&self, mut outputs: Array3<f32>, img_size: (i32, i32)) -> Array3<f32> {
|
fn demo_postprocess(&self, mut outputs: Array3<f32>, img_size: (i32, i32)) -> Array3<f32> {
|
||||||
let strides = [8, 16, 32];
|
let strides = [8, 16, 32];
|
||||||
|
|
||||||
@@ -123,7 +127,6 @@ impl<'a> Detector<'a> {
|
|||||||
outputs
|
outputs
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 4. nms
|
|
||||||
fn nms(&self, boxes: &Array2<f32>, scores: &Array1<f32>, nms_thr: f32) -> Vec<usize> {
|
fn nms(&self, boxes: &Array2<f32>, scores: &Array1<f32>, nms_thr: f32) -> Vec<usize> {
|
||||||
let mut keep = Vec::new();
|
let mut keep = Vec::new();
|
||||||
let x1 = boxes.column(0);
|
let x1 = boxes.column(0);
|
||||||
@@ -183,8 +186,7 @@ impl<'a> Detector<'a> {
|
|||||||
keep
|
keep
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 5. multiclass_nms
|
/// 多类别 NMS 后处理:按分数阈值筛选候选框,并用 NMS 阈值去重。
|
||||||
//multiclass_nms_class_agnostic
|
|
||||||
pub fn multiclass_nms(
|
pub fn multiclass_nms(
|
||||||
&self,
|
&self,
|
||||||
boxes: &Array2<f32>, // [25200, 4] -> xyxy 格式
|
boxes: &Array2<f32>, // [25200, 4] -> xyxy 格式
|
||||||
@@ -241,7 +243,7 @@ impl<'a> Detector<'a> {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
/// 6. get_bbox (完全解耦 OpenCV)
|
/// 对图像执行完整检测流程(预处理、推理、后处理),返回像素坐标系下的检测框。
|
||||||
pub fn get_bbox(
|
pub fn get_bbox(
|
||||||
&self,
|
&self,
|
||||||
dynamic_img: &DynamicImage,
|
dynamic_img: &DynamicImage,
|
||||||
@@ -254,7 +256,7 @@ impl<'a> Detector<'a> {
|
|||||||
|
|
||||||
// tract 推理
|
// tract 推理
|
||||||
// let outputs = self.session.session.run(tvec!(input_tensor.into()))?;
|
// 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]
|
// let output_array = outputs[0]
|
||||||
// 2. 无缝、安全地解包出标准 3维 矩阵
|
// 2. 无缝、安全地解包出标准 3维 矩阵
|
||||||
let DetOutput::Detection(output_array) = outputs;
|
let DetOutput::Detection(output_array) = outputs;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! OCR 模块:识别器构建器、执行入口及元数据、字符集等类型。
|
||||||
|
|
||||||
mod builder;
|
mod builder;
|
||||||
mod charset;
|
mod charset;
|
||||||
mod color_filter;
|
mod color_filter;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
//! OCR 构建器。
|
||||||
|
|
||||||
use crate::ocr::executor::Ocr;
|
use crate::ocr::executor::Ocr;
|
||||||
// use ddddocr_tract::session::OcrSession;
|
// use ddddocr_tract::session::OcrSession;
|
||||||
use crate::traits::OcrEngine;
|
use crate::traits::OcrEngine;
|
||||||
use crate::ocr::color_filter::ColorFilter;
|
use crate::ocr::color_filter::ColorFilter;
|
||||||
use crate::ocr::token_filter::TokenFilter;
|
use crate::ocr::token_filter::TokenFilter;
|
||||||
|
|
||||||
|
/// OCR 构建器:配置识别选项后绑定引擎会话构建 [`crate::Ocr`]。
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct OcrBuilder {
|
pub struct OcrBuilder {
|
||||||
/// 是否修复PNG格式问题
|
/// 是否修复PNG格式问题
|
||||||
@@ -49,6 +53,7 @@ impl OcrBuilder {
|
|||||||
self.charset_restrict = Some(Box::new(restrict));
|
self.charset_restrict = Some(Box::new(restrict));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
/// 绑定引擎会话并构建 OCR 识别器。
|
||||||
pub fn build_with<E: OcrEngine>(self, runtime: &E) -> Ocr<'_> {
|
pub fn build_with<E: OcrEngine>(self, runtime: &E) -> Ocr<'_> {
|
||||||
// 1. 原地解析颜色过滤器
|
// 1. 原地解析颜色过滤器
|
||||||
let final_color_ranges = match &self.color_filter {
|
let final_color_ranges = match &self.color_filter {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
//! 字符集:token 列表与索引的双向映射。
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 3. 字符集核心结构体 (重命名为 Charset)
|
// 3. 字符集核心结构体 (重命名为 Charset)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
/// 字符集:token 列表与索引的双向映射。
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Charset {
|
pub struct Charset {
|
||||||
// 使用 Cow 统一静态切片和动态读取的 Vec<String>,内部实现真正的零拷贝
|
// 使用 Cow 统一静态切片和动态读取的 Vec<String>,内部实现真正的零拷贝
|
||||||
@@ -32,7 +35,7 @@ impl Charset {
|
|||||||
|
|
||||||
// --- 业务策略方法 ---
|
// --- 业务策略方法 ---
|
||||||
|
|
||||||
/// 将字符转为索引,不存在返回 -1 (保持与原 Python 库行为一致)
|
/// 字符转索引,不存在时返回 -1(与 Python 版行为一致)。
|
||||||
pub fn char_to_index(&self, char_str: &str) -> i32 {
|
pub fn char_to_index(&self, char_str: &str) -> i32 {
|
||||||
if let Some(&idx) = self.char_to_idx.get(char_str) {
|
if let Some(&idx) = self.char_to_idx.get(char_str) {
|
||||||
idx as i32
|
idx as i32
|
||||||
@@ -41,7 +44,7 @@ impl Charset {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将索引转为字符引用,零拷贝。若越界返回 None
|
/// 索引转字符引用,越界时返回 `None`。
|
||||||
pub fn index_to_char_ref(&self, index: usize) -> Option<&str> {
|
pub fn index_to_char_ref(&self, index: usize) -> Option<&str> {
|
||||||
self.tokens.get(index).map(|cow| cow.as_ref())
|
self.tokens.get(index).map(|cow| cow.as_ref())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
//! 颜色过滤:HSV 区间匹配与颜色预设。
|
||||||
|
|
||||||
use crate::error::{ImagePreprocessError, Result};
|
use crate::error::{ImagePreprocessError, Result};
|
||||||
use crate::utils::image_processor::rgb_to_opencv_hsv;
|
use crate::utils::image_processor::rgb_to_opencv_hsv;
|
||||||
use image::{DynamicImage, ImageBuffer, Rgb};
|
use image::{DynamicImage, ImageBuffer, Rgb};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// 核心区间判定辅助函数
|
|
||||||
#[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| {
|
||||||
@@ -15,6 +16,7 @@ fn is_pixel_matched(ranges: &[HsvRange], h: u8, s: u8, v: u8) -> bool {
|
|||||||
&& v <= range.upper.2
|
&& v <= range.upper.2
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/// 按 HSV 区间过滤图像:未命中任一区间的像素刷白。
|
||||||
pub fn apply_to_image(
|
pub fn apply_to_image(
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
hsv_ranges: &[HsvRange],
|
hsv_ranges: &[HsvRange],
|
||||||
@@ -58,6 +60,7 @@ pub fn apply_to_image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
/// HSV 颜色区间,下界与上界各为 `(H, S, V)`。
|
||||||
pub struct HsvRange {
|
pub struct HsvRange {
|
||||||
pub lower: (u8, u8, u8), // (H, S, V)
|
pub lower: (u8, u8, u8), // (H, S, V)
|
||||||
pub upper: (u8, u8, u8), // (H, S, V)
|
pub upper: (u8, u8, u8), // (H, S, V)
|
||||||
@@ -69,8 +72,7 @@ impl HsvRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl HsvRange {
|
impl HsvRange {
|
||||||
/// 验证当前 HSV 范围是否合法
|
/// 校验区间是否合法(H 0-180,S/V 0-255,且下界 <= 上界)。
|
||||||
/// 对应 Python 逻辑:H 在 0-180,S/V 在 0-255,且下界 <= 上界
|
|
||||||
pub fn validate(&self) -> Result<(), ImagePreprocessError> {
|
pub fn validate(&self) -> Result<(), ImagePreprocessError> {
|
||||||
// 1. 校验 H 通道边界 (OpenCV 中 H 范围是 0-180)
|
// 1. 校验 H 通道边界 (OpenCV 中 H 范围是 0-180)
|
||||||
if self.lower.0 > 180 || self.upper.0 > 180 {
|
if self.lower.0 > 180 || self.upper.0 > 180 {
|
||||||
@@ -93,6 +95,7 @@ impl HsvRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
/// 颜色预设:常见颜色对应的 HSV 区间集合。
|
||||||
pub enum ColorPreset {
|
pub enum ColorPreset {
|
||||||
Red,
|
Red,
|
||||||
Blue,
|
Blue,
|
||||||
@@ -108,9 +111,7 @@ pub enum ColorPreset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ColorPreset {
|
impl ColorPreset {
|
||||||
/// 纯裸数据定义,没有任何结构体包装,干净利落
|
/// 返回预设对应的 HSV 区间列表。
|
||||||
/// 返回值:(范围数量, 范围数组)
|
|
||||||
/// 完美的零成本抽象:利用常量提升将数据直接打入只读数据段 (.rodata)
|
|
||||||
pub fn matches(&self) -> &[HsvRange] {
|
pub fn matches(&self) -> &[HsvRange] {
|
||||||
match self {
|
match self {
|
||||||
ColorPreset::Red => &[
|
ColorPreset::Red => &[
|
||||||
@@ -162,7 +163,7 @@ impl ColorPreset {
|
|||||||
ColorPreset::Custom(ranges) => ranges,
|
ColorPreset::Custom(ranges) => ranges,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 校验逻辑:在这里实现完美的“责任分离”
|
/// 校验预设的 HSV 区间是否合法。
|
||||||
pub fn validate(&self) -> Result<(), ImagePreprocessError> {
|
pub fn validate(&self) -> Result<(), ImagePreprocessError> {
|
||||||
match self {
|
match self {
|
||||||
// 1. 快捷变体:完全绕过,根本不校验,0 运行时开销放行!
|
// 1. 快捷变体:完全绕过,根本不校验,0 运行时开销放行!
|
||||||
@@ -202,23 +203,20 @@ impl FromStr for ColorPreset {
|
|||||||
// 3. 颜色约束特征(Trait)与组合子设计模式
|
// 3. 颜色约束特征(Trait)与组合子设计模式
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
|
/// 颜色匹配上下文:当前像素的 HSV 值。
|
||||||
pub struct PixelCtx {
|
pub struct PixelCtx {
|
||||||
pub hsv: (u8, u8, u8),
|
pub hsv: (u8, u8, u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 统一的颜色约束接口
|
/// 颜色过滤约束接口:提供一组 HSV 区间。
|
||||||
pub trait ColorFilter {
|
pub trait ColorFilter {
|
||||||
/// 将自身的有效约束平铺追加到统一的目标容器中
|
|
||||||
fn append_ranges(&self, target: &mut Vec<HsvRange>);
|
fn append_ranges(&self, target: &mut Vec<HsvRange>);
|
||||||
/// 预估范围数量,借助原生内置的 len() 实现 O(1) 完美控容
|
|
||||||
fn estimated_count(&self) -> usize;
|
fn estimated_count(&self) -> usize;
|
||||||
/// 将自身的有效约束平铺追加到统一目标容器中
|
/// 验证过滤器配置是否合法,默认直接放行。
|
||||||
/// 验证当前过滤器是否合法,默认直接放行(Ok(()))
|
|
||||||
fn validate_self(&self) -> Result<(), ImagePreprocessError> {
|
fn validate_self(&self) -> Result<(), ImagePreprocessError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
/// 【新扩展的架构方法】将自身安全的合并到已有的普通容器中,并完成去重和排序
|
/// 收集全部有效区间并排序去重;无有效区间时返回 `None`。
|
||||||
/// 完美的责任分离:Builder 不再需要关心怎么分配内存、怎么排序去重
|
|
||||||
fn collect_to_vec(&self) -> Result<Option<Vec<HsvRange>>, ImagePreprocessError> {
|
fn collect_to_vec(&self) -> Result<Option<Vec<HsvRange>>, ImagePreprocessError> {
|
||||||
// 1. 触发自检
|
// 1. 触发自检
|
||||||
self.validate_self()?;
|
self.validate_self()?;
|
||||||
@@ -288,6 +286,7 @@ impl<'a> ColorFilter for MultiOrColorRestrict<'a> {
|
|||||||
// 4. 声明式宏:一语定乾坤
|
// 4. 声明式宏:一语定乾坤
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
|
/// 组合多个颜色过滤器为「或」关系的快捷宏。
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! color_any_of {
|
macro_rules! color_any_of {
|
||||||
($only:expr) => {
|
($only:expr) => {
|
||||||
|
|||||||
@@ -1,41 +1,37 @@
|
|||||||
|
//! OCR 执行器:预测入口与结果类型。
|
||||||
|
|
||||||
use crate::ocr::metadata::Resize;
|
use crate::ocr::metadata::Resize;
|
||||||
|
|
||||||
use crate::ocr::color_filter::{HsvRange, apply_to_image};
|
use crate::ocr::color_filter::{HsvRange, apply_to_image};
|
||||||
// use ddddocr_tract::session::{ModelOutput, OcrSession};
|
// 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_convert::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 crate::{OcrBuilder, OcrOutput};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
|
use ndarray::ArrayView2;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
// use tract_onnx::prelude::tract_ndarray::{ Ix2, s};
|
use tracing::warn;
|
||||||
// use tract_onnx::prelude::{DatumType, Tensor, tract_ndarray};
|
/// OCR 识别结果:纯文本或携带概率的文本。
|
||||||
// !!!【核心纠正】:彻底弃用 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};
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum OcrResult {
|
pub enum OcrResult {
|
||||||
/// 纯文本分支(对应 probability = false)
|
/// 纯文本结果(`probability = false` 时返回)。
|
||||||
Text(String),
|
Text(String),
|
||||||
/// 包含全量概率的分支(对应 probability = true)
|
/// 携带概率的结果(`probability = true` 时返回)。
|
||||||
Probability {
|
Probability {
|
||||||
text: String,
|
text: String,
|
||||||
/// 满额概率矩阵 [Steps, Classes]
|
/// 全量概率矩阵 `[Steps, Classes]`。
|
||||||
probabilities: Vec<Vec<f32>>,
|
probabilities: Vec<Vec<f32>>,
|
||||||
/// 全局平均置信度
|
/// 全局平均置信度。
|
||||||
confidence: f64,
|
confidence: f64,
|
||||||
},
|
},
|
||||||
/// 不支持的模型或未知输出
|
/// 不支持的模型或未知输出。
|
||||||
Unsupported { message: String },
|
Unsupported { message: String },
|
||||||
}
|
}
|
||||||
impl OcrResult {
|
impl OcrResult {
|
||||||
/// 消费自身,直接提取最终文本
|
/// 消费自身并提取最终文本。
|
||||||
pub fn into_text(self) -> String {
|
pub fn into_text(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
OcrResult::Text(text) => text,
|
OcrResult::Text(text) => text,
|
||||||
@@ -103,6 +99,7 @@ impl fmt::Display for OcrResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// OCR 识别器:预处理、推理调度与后处理解码。
|
||||||
pub struct Ocr<'a> {
|
pub struct Ocr<'a> {
|
||||||
pub(crate) runtime: &'a dyn OcrEngine,
|
pub(crate) runtime: &'a dyn OcrEngine,
|
||||||
pub(crate) png_fix: bool,
|
pub(crate) png_fix: bool,
|
||||||
@@ -129,9 +126,9 @@ impl<'a> Ocr<'a> {
|
|||||||
pub fn builder() -> OcrBuilder {
|
pub fn builder() -> OcrBuilder {
|
||||||
OcrBuilder::default()
|
OcrBuilder::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
impl<'a> Ocr<'a> {
|
impl<'a> Ocr<'a> {
|
||||||
|
/// 对输入图像执行 OCR 识别并返回结果。
|
||||||
pub fn predict(&self, image: &DynamicImage) -> Result<OcrResult> {
|
pub fn predict(&self, image: &DynamicImage) -> Result<OcrResult> {
|
||||||
println!("当前颜色过滤器状态: {:?}", self.final_color_ranges);
|
println!("当前颜色过滤器状态: {:?}", self.final_color_ranges);
|
||||||
|
|
||||||
@@ -179,9 +176,10 @@ impl<'a> Ocr<'a> {
|
|||||||
let ocr_output = self.process_model_output(raw_tensor)?;
|
let ocr_output = self.process_model_output(raw_tensor)?;
|
||||||
Ok(ocr_output)
|
Ok(ocr_output)
|
||||||
}
|
}
|
||||||
/// 对应 Python 的 _preprocess_image
|
fn preprocess_image(
|
||||||
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
|
&self,
|
||||||
fn preprocess_image(&self, img: &DynamicImage) -> Result<ndarray::Array4<f32>,ImagePreprocessError> {
|
img: &DynamicImage,
|
||||||
|
) -> Result<ndarray::Array4<f32>, ImagePreprocessError> {
|
||||||
// 1. 获取模型元数据配置
|
// 1. 获取模型元数据配置
|
||||||
let meta = self.runtime.metadata();
|
let meta = self.runtime.metadata();
|
||||||
let norm = &meta.normalization; // 获取归一化器
|
let norm = &meta.normalization; // 获取归一化器
|
||||||
@@ -255,30 +253,10 @@ impl<'a> Ocr<'a> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(array4)
|
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(¤t_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
|
// 这段代码未来直接放入 ddddocr-core
|
||||||
fn process_model_output(&self, output: OcrOutput) -> Result<OcrResult,TensorError> {
|
fn process_model_output(&self, output: OcrOutput) -> Result<OcrResult, TensorError> {
|
||||||
match output {
|
match output {
|
||||||
OcrOutput::Indices(array1) => {
|
OcrOutput::Indices(array1) => {
|
||||||
// 对应你原来的 process_i64_tensor
|
// 对应你原来的 process_i64_tensor
|
||||||
@@ -340,8 +318,7 @@ impl<'a> Ocr<'a> {
|
|||||||
None => true,
|
None => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印
|
/// 返回当前生效的可用 token 列表。
|
||||||
/// 这里的 &str 完美借用了自 tokens,依然是彻底的零拷贝!
|
|
||||||
pub fn valid_tokens(&self) -> Vec<&str> {
|
pub fn valid_tokens(&self) -> Vec<&str> {
|
||||||
let charset = &self.runtime.metadata().charset;
|
let charset = &self.runtime.metadata().charset;
|
||||||
let tokens = &charset.tokens;
|
let tokens = &charset.tokens;
|
||||||
@@ -360,7 +337,6 @@ impl<'a> Ocr<'a> {
|
|||||||
None => self.runtime.metadata().charset.tokens.len(),
|
None => self.runtime.metadata().charset.tokens.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 变体 B 核心处理器:单次遍历 2D 视图,融合计算 Softmax、Argmax、置信度并输出概率大包
|
|
||||||
fn compute_f32_full_probability(
|
fn compute_f32_full_probability(
|
||||||
&self,
|
&self,
|
||||||
matrix_view: ArrayView2<f32>,
|
matrix_view: ArrayView2<f32>,
|
||||||
@@ -502,7 +478,6 @@ impl<'a> Ocr<'a> {
|
|||||||
// 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.runtime.metadata().charset;
|
let charset = &self.runtime.metadata().charset;
|
||||||
@@ -542,8 +517,7 @@ impl<'a> Ocr<'a> {
|
|||||||
// 5. 字符映射
|
// 5. 字符映射
|
||||||
if let Some(char_str) = tokens.get(u_idx) {
|
if let Some(char_str) = tokens.get(u_idx) {
|
||||||
res.push_str(char_str);
|
res.push_str(char_str);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
warn!("警告: 预测索引 {} 超出字符集范围", u_idx);
|
warn!("警告: 预测索引 {} 超出字符集范围", u_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! OCR 模型元数据:归一化、缩放策略与模型信息。
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 1. 辅助定义的枚举与结构体
|
// 1. 辅助定义的枚举与结构体
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -5,6 +7,7 @@
|
|||||||
use crate::ocr::Charset;
|
use crate::ocr::Charset;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
/// 像素归一化策略。
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Normalization {
|
pub enum Normalization {
|
||||||
/// 映射到 [0.0, 1.0] -> pixel / 255.0
|
/// 映射到 [0.0, 1.0] -> pixel / 255.0
|
||||||
@@ -14,7 +17,6 @@ pub enum Normalization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Normalization {
|
impl Normalization {
|
||||||
/// 统一归一化计算逻辑
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn normalize(&self, pixel: f32) -> f32 {
|
pub fn normalize(&self, pixel: f32) -> f32 {
|
||||||
match self {
|
match self {
|
||||||
@@ -35,17 +37,15 @@ pub enum Resize {
|
|||||||
Square(u32),
|
Square(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// OCR 模型元数据:字符集、缩放策略、通道数与归一化配置。
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ModelMetadata {
|
pub struct ModelMetadata {
|
||||||
/// 字符集管理器
|
|
||||||
pub charset: Charset,
|
pub charset: Charset,
|
||||||
/// 是否为单字识别模型
|
/// 是否为单字识别模型
|
||||||
pub word: bool,
|
pub word: bool,
|
||||||
/// 预处理的缩放策略
|
|
||||||
pub resize: Resize,
|
pub resize: Resize,
|
||||||
/// 图像通道数 (1 或 3)
|
/// 图像通道数(1 或 3)
|
||||||
pub channel: u8,
|
pub channel: u8,
|
||||||
/// 新增:传递给核心业务使用的归一化配置
|
|
||||||
pub normalization: Normalization,
|
pub normalization: Normalization,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ impl ModelMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- 优雅的工厂模式构造器 ---
|
// --- 优雅的工厂模式构造器 ---
|
||||||
/// 通用的静态切片转换构造器
|
/// 从静态字符切片构建元数据并自动生成字符集。
|
||||||
pub fn from_static_slice(
|
pub fn from_static_slice(
|
||||||
slice: &[&'static str],
|
slice: &[&'static str],
|
||||||
word: bool,
|
word: bool,
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
|
//! 字符集限制:按字符属性或索引过滤识别范围。
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
/// 字符集范围限制枚举
|
/// 字符集校验上下文:当前 token 的文本与索引。
|
||||||
pub struct ValidationCtx<'a> {
|
pub struct ValidationCtx<'a> {
|
||||||
pub text: &'a str, // 当前 Token 的文本内容
|
pub text: &'a str, // 当前 Token 的文本内容
|
||||||
pub token_id: usize, // 当前 Token 的 ID 索引
|
pub token_id: usize, // 当前 Token 的 ID 索引
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 统一的约束接口
|
/// 字符集限制接口:决定某个 token 是否放行。
|
||||||
pub trait TokenFilter {
|
pub trait TokenFilter {
|
||||||
fn matches(&self, ctx: &ValidationCtx) -> bool;
|
fn matches(&self, ctx: &ValidationCtx) -> bool;
|
||||||
/// 预估容量提示,帮助精准开辟 Vec 内存
|
/// 预估匹配数量的容量提示。
|
||||||
fn estimated_capacity(&self) -> usize {
|
fn estimated_capacity(&self) -> usize {
|
||||||
128
|
128
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 统一接管全量字符集的密集遍历、CTC Blank放行、去重、排序及空交集退化兜底
|
/// 遍历全量字符集筛选可用索引(放行 CTC blank、排序去重、空交集返回 `None`)。
|
||||||
fn apply_to_charset(&self, tokens: &[Cow<str>]) -> Option<Vec<usize>> {
|
fn apply_to_charset(&self, tokens: &[Cow<str>]) -> Option<Vec<usize>> {
|
||||||
let mut has_any_match = false;
|
let mut has_any_match = false;
|
||||||
let estimated_capacity = self.estimated_capacity();
|
let estimated_capacity = self.estimated_capacity();
|
||||||
@@ -58,6 +60,7 @@ pub trait TokenFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 按字符属性限制:数字、大小写字母或自定义列表。
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum CharRestrict {
|
pub enum CharRestrict {
|
||||||
Digit,
|
Digit,
|
||||||
@@ -84,6 +87,7 @@ impl TokenFilter for CharRestrict {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 按索引限制:前 N 个、索引范围或索引列表。
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum IdRestrict {
|
pub enum IdRestrict {
|
||||||
TopN(usize),
|
TopN(usize),
|
||||||
@@ -128,7 +132,7 @@ impl<'a> TokenFilter for MultiOrRestrict<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 声明式宏:解决组合扩展痛苦
|
/// 组合多个字符限制规则为「或」关系的快捷宏。
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! any_of {
|
macro_rules! any_of {
|
||||||
// 场景 A:如果用户只传了一个规则,免去构建 Vec 的开销,直接返回其引用
|
// 场景 A:如果用户只传了一个规则,免去构建 Vec 的开销,直接返回其引用
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! 滑块验证码匹配:模板匹配与差异比较两种模式。
|
||||||
|
|
||||||
use crate::error::{ImagePreprocessError, Result};
|
use crate::error::{ImagePreprocessError, Result};
|
||||||
use crate::utils::image_convert::{ColorMode, image_to_ndarray};
|
use crate::utils::image_convert::{ColorMode, image_to_ndarray};
|
||||||
use crate::utils::image_processor;
|
use crate::utils::image_processor;
|
||||||
@@ -12,8 +14,11 @@ use imageproc::region_labelling::{Connectivity, connected_components};
|
|||||||
use imageproc::template_matching::{MatchTemplateMethod, match_template};
|
use imageproc::template_matching::{MatchTemplateMethod, match_template};
|
||||||
use ndarray::{ArrayView2, ArrayView3};
|
use ndarray::{ArrayView2, ArrayView3};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
/// 滑块匹配结果:检测中心坐标与置信度。
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SlideResult {
|
pub struct SlideResult {
|
||||||
|
/// 检测中心坐标 `[x, y]`。
|
||||||
pub target: [i32; 2],
|
pub target: [i32; 2],
|
||||||
pub target_x: i32,
|
pub target_x: i32,
|
||||||
pub target_y: i32,
|
pub target_y: i32,
|
||||||
@@ -29,13 +34,14 @@ impl fmt::Display for SlideResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 滑块匹配服务:提供模板匹配与差异比较两种识别模式。
|
||||||
pub struct Slider;
|
pub struct Slider;
|
||||||
|
|
||||||
impl Slider {
|
impl Slider {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self
|
Self
|
||||||
}
|
}
|
||||||
/// 对应 Python: slide_match 滑块匹配接口
|
/// 模板匹配滑块:在背景图中定位滑块中心(对应 Python 的 `slide_match`)。
|
||||||
pub fn slide_match(
|
pub fn slide_match(
|
||||||
&self,
|
&self,
|
||||||
target_image: &DynamicImage,
|
target_image: &DynamicImage,
|
||||||
@@ -48,8 +54,7 @@ impl Slider {
|
|||||||
self.perform_slide_match(target_array.view(), background_array.view(), simple_target)
|
self.perform_slide_match(target_array.view(), background_array.view(), simple_target)
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
/// 对应 Python: slide_comparison 差异比较接口
|
/// 差异比较滑块:对比带坑位的图与原始背景图,定位差异中心(对应 Python 的 `slide_comparison`)。
|
||||||
/// 用于比较带坑位的图片与原始背景图,定位差异点
|
|
||||||
pub fn slide_comparison(
|
pub fn slide_comparison(
|
||||||
&self,
|
&self,
|
||||||
target_image: &DynamicImage,
|
target_image: &DynamicImage,
|
||||||
@@ -63,7 +68,7 @@ impl Slider {
|
|||||||
self.perform_slide_comparison(target_array.view(), background_array.view())
|
self.perform_slide_comparison(target_array.view(), background_array.view())
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
/// 对应 Python: _perform_slide_comparison
|
/// 差异比较核心实现(对应 Python 的 `_perform_slide_comparison`)。
|
||||||
pub fn perform_slide_comparison(
|
pub fn perform_slide_comparison(
|
||||||
&self,
|
&self,
|
||||||
target: ArrayView3<u8>,
|
target: ArrayView3<u8>,
|
||||||
@@ -133,7 +138,7 @@ impl Slider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 对应 Python: _perform_slide_match
|
/// 模板匹配核心实现(对应 Python 的 `_perform_slide_match`)。
|
||||||
// 在 SlideEngine 中修改此入口进行测试
|
// 在 SlideEngine 中修改此入口进行测试
|
||||||
fn perform_slide_match(
|
fn perform_slide_match(
|
||||||
&self,
|
&self,
|
||||||
@@ -180,9 +185,7 @@ impl Slider {
|
|||||||
Ok(self.edge_based_match(target_gray.view(), background_gray.view()))
|
Ok(self.edge_based_match(target_gray.view(), background_gray.view()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 对应 Python: _simple_template_match
|
/// 简单模式模板匹配:直接对灰度图做归一化互相关(对应 Python 的 `_simple_template_match`)。
|
||||||
/// 使用 SAD (Sum of Absolute Differences) 算法
|
|
||||||
/// 核心模板匹配:SAD + 有效像素过滤
|
|
||||||
fn simple_template_match(
|
fn simple_template_match(
|
||||||
&self,
|
&self,
|
||||||
target: ArrayView2<u8>,
|
target: ArrayView2<u8>,
|
||||||
@@ -221,8 +224,7 @@ impl Slider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 对应 Python: _edge_based_match
|
/// 边缘模式模板匹配:基于 Canny 边缘检测后再匹配(对应 Python 的 `_edge_based_match`)。
|
||||||
/// 基于边缘检测的滑块匹配 (对齐 Python _edge_based_match)
|
|
||||||
pub fn edge_based_match(
|
pub fn edge_based_match(
|
||||||
&self,
|
&self,
|
||||||
target: ArrayView2<u8>,
|
target: ArrayView2<u8>,
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
|
//! 推理引擎统一抽象接口。
|
||||||
|
|
||||||
use crate::error::TensorError;
|
use crate::error::TensorError;
|
||||||
use crate::types::{ModelInfo, TensorInfo};
|
use crate::types::{ModelInfo, TensorInfo};
|
||||||
use crate::{DetOutput, ModelMetadata, OcrOutput};
|
use crate::{DetOutput, ModelMetadata, OcrOutput};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// 查询模型输入/输出信息的接口。
|
||||||
pub trait Info {
|
pub trait Info {
|
||||||
fn input_info(&self) -> crate::error::Result<Vec<TensorInfo>>;
|
fn input_info(&self) -> crate::error::Result<Vec<TensorInfo>>;
|
||||||
fn output_info(&self) -> crate::error::Result<Vec<TensorInfo>>;
|
fn output_info(&self) -> crate::error::Result<Vec<TensorInfo>>;
|
||||||
fn model_info(&self) -> crate::error::Result<ModelInfo>;
|
fn model_info(&self) -> crate::error::Result<ModelInfo>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 核心层定义的统一推理引擎接口。
|
/// 推理引擎统一抽象接口,由 ddddocr-tract、ddddocr-ort 等引擎 crate 实现。
|
||||||
/// 未来的 ddddocr-tract 和 ddddocr-ort 都必须实现这个 Trait
|
|
||||||
pub trait InferenceEngine {
|
pub trait InferenceEngine {
|
||||||
/// 关联类型:具体的 Session 需要声明自己到底产出什么枚举
|
/// 引擎产出的输出枚举(OCR 为 [`crate::OcrOutput`],检测为 [`crate::DetOutput`])。
|
||||||
type Output;
|
type Output;
|
||||||
fn inference(
|
fn inference(
|
||||||
&self,
|
&self,
|
||||||
@@ -21,12 +22,15 @@ pub trait InferenceEngine {
|
|||||||
) -> crate::error::Result<Self::Output, TensorError>;
|
) -> crate::error::Result<Self::Output, TensorError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// OCR 引擎接口:输出 [`crate::OcrOutput`],并提供模型元数据。
|
||||||
pub trait OcrEngine: InferenceEngine<Output = OcrOutput> + Info {
|
pub trait OcrEngine: InferenceEngine<Output = OcrOutput> + Info {
|
||||||
fn metadata(&self) -> &ModelMetadata;
|
fn metadata(&self) -> &ModelMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 目标检测引擎接口:输出 [`crate::DetOutput`]。
|
||||||
pub trait DetEngine: InferenceEngine<Output = DetOutput> {}
|
pub trait DetEngine: InferenceEngine<Output = DetOutput> {}
|
||||||
|
|
||||||
|
/// 模型加载器:从本地路径或字节流构建引擎会话。
|
||||||
pub trait Loader {
|
pub trait Loader {
|
||||||
type Session;
|
type Session;
|
||||||
type Error;
|
type Error;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! 图像与张量工具:加载、格式转换、图像处理与归一化。
|
||||||
|
|
||||||
pub mod image_convert;
|
pub mod image_convert;
|
||||||
mod image_helper;
|
mod image_helper;
|
||||||
pub mod image_processor;
|
pub mod image_processor;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
//! 图像格式转换:DynamicImage 与 ndarray 数组互转。
|
||||||
|
|
||||||
use crate::error::{ImagePreprocessError, Result};
|
use crate::error::{ImagePreprocessError, Result};
|
||||||
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
|
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
|
||||||
use ndarray::{Array3, ArrayViewD};
|
use ndarray::{Array3, ArrayViewD};
|
||||||
|
|
||||||
|
/// 图像通道模式。
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ColorMode {
|
pub enum ColorMode {
|
||||||
RGB,
|
RGB,
|
||||||
@@ -9,7 +12,7 @@ pub enum ColorMode {
|
|||||||
L,
|
L,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 封装数组转图像的逻辑,
|
/// 将 ndarray 数组转换为图像(自动识别 HWC 通道数)。
|
||||||
// 对应 Python 版 _numpy_to_pil_image
|
// 对应 Python 版 _numpy_to_pil_image
|
||||||
pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage,ImagePreprocessError> {
|
pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage,ImagePreprocessError> {
|
||||||
let shape = array.shape();
|
let shape = array.shape();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! 图像输入源:从路径、Base64、字节等统一加载图片。
|
||||||
|
|
||||||
use crate::error::{DdddError, Result};
|
use crate::error::{DdddError, Result};
|
||||||
use crate::utils::image_convert::ndarray_to_hwc_image;
|
use crate::utils::image_convert::ndarray_to_hwc_image;
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
@@ -8,8 +10,9 @@ use std::fmt::{Debug, Formatter};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
/// Base64 字符串包装。
|
||||||
pub struct Base64<'a>(pub &'a str);
|
pub struct Base64<'a>(pub &'a str);
|
||||||
/// 专属图像输入源转换器
|
/// 统一图像输入源,包装加载完成的图像。
|
||||||
pub struct ImageSource {
|
pub struct ImageSource {
|
||||||
inner: DynamicImage,
|
inner: DynamicImage,
|
||||||
}
|
}
|
||||||
@@ -21,6 +24,7 @@ impl ImageSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从多种输入类型转换为 [`ImageSource`] 的转换接口。
|
||||||
pub trait TryFromImage<T>: Sized {
|
pub trait TryFromImage<T>: Sized {
|
||||||
// 唯一的转换入口,通过目标类型来调用
|
// 唯一的转换入口,通过目标类型来调用
|
||||||
fn try_from_image(value: T) -> Result<Self>;
|
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)]
|
#[allow(dead_code)]
|
||||||
pub fn load_image_from_input<I>(input: I) -> Result<DynamicImage>
|
pub fn load_image_from_input<I>(input: I) -> Result<DynamicImage>
|
||||||
where
|
where
|
||||||
@@ -119,7 +123,7 @@ where
|
|||||||
Ok(img)
|
Ok(img)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将base64编码的图片转换为 DynamicImage
|
/// 将 Base64 编码的图片转换为 DynamicImage。
|
||||||
pub fn base64_to_image(b64_str: &str) -> Result<DynamicImage> {
|
pub fn base64_to_image(b64_str: &str) -> Result<DynamicImage> {
|
||||||
// 过滤掉可能存在的 base64 前缀,例如 "data:utils/png;base64,"
|
// 过滤掉可能存在的 base64 前缀,例如 "data:utils/png;base64,"
|
||||||
let clean_b64 = if let Some(pos) = b64_str.find(",") {
|
let clean_b64 = if let Some(pos) = b64_str.find(",") {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
//! 图像处理算法:OpenCV 风格的常用函数封装。
|
||||||
|
|
||||||
use image::{imageops::FilterType, DynamicImage, GrayImage, ImageBuffer, Luma};
|
use image::{imageops::FilterType, DynamicImage, GrayImage, ImageBuffer, Luma};
|
||||||
|
|
||||||
use ndarray::{azip, Array2, Array3, ArrayView2, ArrayView3};
|
use ndarray::{azip, Array2, Array3, ArrayView2, ArrayView3};
|
||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min};
|
||||||
|
|
||||||
// 模拟openCV
|
// 模拟openCV
|
||||||
/// 1. 计算两个数组的绝对差值 (对应 cv2.absdiff)
|
/// 计算两个 HWC 数组的绝对差值(对应 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,生成差值的绝对值数组
|
||||||
// 或者直接使用 zip_mut_with 处理以减少内存分配
|
// 或者直接使用 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)) {
|
pub fn min_max_loc(result_map: &ImageBuffer<Luma<f32>, Vec<f32>>) -> (f32, (u32, u32)) {
|
||||||
// 4. 找到最佳匹配位置 (对齐 cv2.minMaxLoc)
|
// 4. 找到最佳匹配位置 (对齐 cv2.minMaxLoc)
|
||||||
let mut max_val: f32 = -1.0;
|
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)
|
(max_val, max_loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 1. 模拟 findContours 并获取最大面积区域的 Label
|
/// 模拟 findContours:返回面积最大的连通域标签,找不到时返回 `None`。
|
||||||
/// 返回 Option<u32>,如果找不到任何区域则返回 None
|
|
||||||
pub fn find_contours_and_max(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>) -> Option<u32> {
|
pub fn find_contours_and_max(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>) -> Option<u32> {
|
||||||
// 统计每个标签出现的频率(即面积)
|
// 统计每个标签出现的频率(即面积)
|
||||||
let mut max_label = 0;
|
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)
|
Some(max_label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 根据目标连通域标签,计算其在图像中的外接矩形边界框(对应 `cv2.boundingRect`)
|
/// 计算指定连通域标签的外接矩形(对应 cv2.boundingRect),返回 `(min_x, min_y, width, height)`。
|
||||||
///
|
|
||||||
/// 返回格式: `(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,
|
||||||
@@ -109,9 +108,7 @@ pub fn calculate_center(top_left: (u32, u32), width: usize, height: usize) -> (i
|
|||||||
(center_x, center_y)
|
(center_x, center_y)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 高性能转换:将 `ndarray` 2D 灰度视图规整为 `image::ImageBuffer` 格式
|
/// 将 2D 灰度 ndarray 视图转换为灰度 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 包装
|
// 技巧:直接将已有的规整连续内存打平转换,或用 from_raw 包装
|
||||||
@@ -128,6 +125,7 @@ pub fn ndarray_to_luma8(array: ArrayView2<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>>
|
|||||||
// 5. 核心高性能图像转换算法 (纯 Rust 编写)
|
// 5. 核心高性能图像转换算法 (纯 Rust 编写)
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
|
/// RGB 像素转换为 OpenCV 风格的 HSV 值。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
|
pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
|
||||||
// 1. 规避高昂的除法,直接转为 f32 进行比对
|
// 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)
|
(h_opencv, s_opencv, v_opencv)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 对应 Python 的 convert_to_grayscale
|
/// 将图像转换为灰度图(L 模式)。
|
||||||
/// 将图像转换为灰度图 (L模式)
|
|
||||||
pub fn convert_to_grayscale(image: &DynamicImage) -> GrayImage {
|
pub fn convert_to_grayscale(image: &DynamicImage) -> GrayImage {
|
||||||
// Rust utils 库的 to_luma8 会根据标准的亮度公式进行转换
|
// Rust utils 库的 to_luma8 会根据标准的亮度公式进行转换
|
||||||
image.to_luma8()
|
image.to_luma8()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 对应 Python 的 resize_image
|
/// 按指定宽高调整图像尺寸。
|
||||||
/// 调整图像尺寸。当前版本仅实现 keep_aspect_ratio=false
|
|
||||||
pub fn resize_image(
|
pub fn resize_image(
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
target_width: u32,
|
target_width: u32,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
//! 张量变换:将异构形状的模型输出规整为统一格式。
|
||||||
|
|
||||||
use crate::OcrOutput;
|
use crate::OcrOutput;
|
||||||
use crate::error::{Result, TensorError};
|
use crate::error::{Result, TensorError};
|
||||||
use ndarray::s;
|
use ndarray::s;
|
||||||
/// 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵
|
|
||||||
|
/// 将异构形状的模型输出规整为标准 `[Steps, Classes]` Logits 矩阵。
|
||||||
pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) -> Result<OcrOutput,TensorError> {
|
pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) -> Result<OcrOutput,TensorError> {
|
||||||
let (steps, classes, data_dyn_view) = match shape.len() {
|
let (steps, classes, data_dyn_view) = match shape.len() {
|
||||||
3 => {
|
3 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user