refactor: 重构 core 包目录结构并消除旧版 mod.rs
- 优化 剥离 models,algo 层并平铺业务模块 - 重构 统一使用现代 filename.rs + 文件夹结构替代旧版 mod.rs
This commit is contained in:
74
ddddocr-core/src/ocr/builder.rs
Normal file
74
ddddocr-core/src/ocr/builder.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::ocr::executor::Ocr;
|
||||
// use ddddocr_tract::session::OcrSession;
|
||||
use crate::ocr::color_filter::ColorFilter;
|
||||
use crate::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,
|
||||
}
|
||||
}
|
||||
}
|
||||
287
ddddocr-core/src/ocr/color_filter.rs
Normal file
287
ddddocr-core/src/ocr/color_filter.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use crate::utils::image_processor::rgb_to_opencv_hsv;
|
||||
use anyhow::anyhow;
|
||||
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| {
|
||||
h >= range.lower.0
|
||||
&& h <= range.upper.0
|
||||
&& s >= range.lower.1
|
||||
&& s <= range.upper.1
|
||||
&& v >= range.lower.2
|
||||
&& v <= range.upper.2
|
||||
})
|
||||
}
|
||||
pub fn apply_to_image(
|
||||
image: &DynamicImage,
|
||||
hsv_ranges: &[HsvRange],
|
||||
) -> anyhow::Result<DynamicImage> {
|
||||
// 1. 统一转换为连续内存的 RGB8 缓冲区 (对应 Python 的 Image 到 RGB/BGR 数组转换)
|
||||
let rgb_img = image.to_rgb8();
|
||||
let (width, height) = rgb_img.dimensions();
|
||||
let mut raw_pixels = rgb_img.into_raw();
|
||||
|
||||
// 2. 密集计算核心:原地流式迭代修改
|
||||
// 每次取出 3 个 u8 字节,分别代表 [R, G, B],无多余掩膜矩阵内存分配
|
||||
for chunk in raw_pixels.chunks_exact_mut(3) {
|
||||
let r = chunk[0];
|
||||
let g = chunk[1];
|
||||
let b = chunk[2];
|
||||
|
||||
// 像素级转换为 OpenCV 标准的 HSV
|
||||
let (h, s, v) = rgb_to_opencv_hsv(r, g, b);
|
||||
|
||||
// 模拟 Python 的多范围 mask bitwise_or 并在 mask == 0 处刷白
|
||||
// 如果该像素没有命中任何一个配置的颜色区间,立刻原地刷白 [255, 255, 255]
|
||||
if !is_pixel_matched(hsv_ranges, h, s, v) {
|
||||
chunk[0] = 255;
|
||||
chunk[1] = 255;
|
||||
chunk[2] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 将扁平字节数组重新打包回 DynamicImage 容器
|
||||
let filtered_buffer = ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(width, height, raw_pixels)
|
||||
.ok_or_else(|| anyhow!("图像缓冲重新组装失败,维度与数据大小不匹配"))?;
|
||||
|
||||
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 {
|
||||
pub const fn new(lower: (u8, u8, u8), upper: (u8, u8, u8)) -> Self {
|
||||
Self { lower, upper }
|
||||
}
|
||||
}
|
||||
impl HsvRange {
|
||||
/// 验证当前 HSV 范围是否合法
|
||||
/// 对应 Python 逻辑:H 在 0-180,S/V 在 0-255,且下界 <= 上界
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
// 1. 校验 H 通道边界 (OpenCV 中 H 范围是 0-180)
|
||||
if self.lower.0 > 180 || self.upper.0 > 180 {
|
||||
return Err("H通道值必须在 0-180 范围内".to_string());
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ColorPreset {
|
||||
Red,
|
||||
Blue,
|
||||
Green,
|
||||
Yellow,
|
||||
Orange,
|
||||
Purple,
|
||||
Cyan,
|
||||
Black,
|
||||
White,
|
||||
Gray,
|
||||
Custom(Vec<HsvRange>),
|
||||
}
|
||||
|
||||
impl ColorPreset {
|
||||
/// 纯裸数据定义,没有任何结构体包装,干净利落
|
||||
/// 返回值:(范围数量, 范围数组)
|
||||
/// 完美的零成本抽象:利用常量提升将数据直接打入只读数据段 (.rodata)
|
||||
pub fn matches(&self) -> &[HsvRange] {
|
||||
match self {
|
||||
ColorPreset::Red => &[
|
||||
HsvRange {
|
||||
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::Green => &[HsvRange {
|
||||
lower: (40, 50, 50),
|
||||
upper: (80, 255, 255),
|
||||
}],
|
||||
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,
|
||||
}
|
||||
}
|
||||
/// 校验逻辑:在这里实现完美的“责任分离”
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
match self {
|
||||
// 1. 快捷变体:完全绕过,根本不校验,0 运行时开销放行!
|
||||
ColorPreset::Custom(ranges) => {
|
||||
// 2. 只有 Custom 变体需要接受严格的参数政审
|
||||
for r in ranges {
|
||||
r.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ColorPreset {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"red" => Ok(ColorPreset::Red),
|
||||
"blue" => Ok(ColorPreset::Blue),
|
||||
"green" => Ok(ColorPreset::Green),
|
||||
"yellow" => Ok(ColorPreset::Yellow),
|
||||
"orange" => Ok(ColorPreset::Orange),
|
||||
"purple" => Ok(ColorPreset::Purple),
|
||||
"cyan" => Ok(ColorPreset::Cyan),
|
||||
"black" => Ok(ColorPreset::Black),
|
||||
"white" => Ok(ColorPreset::White),
|
||||
"gray" => Ok(ColorPreset::Gray),
|
||||
_ => Err(format!("不支持的颜色预设: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 3. 颜色约束特征(Trait)与组合子设计模式
|
||||
// =====================================================================
|
||||
|
||||
pub struct PixelCtx {
|
||||
pub hsv: (u8, u8, u8),
|
||||
}
|
||||
|
||||
/// 统一的颜色约束接口
|
||||
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<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
/// 【新扩展的架构方法】将自身安全的合并到已有的普通容器中,并完成去重和排序
|
||||
/// 完美的责任分离:Builder 不再需要关心怎么分配内存、怎么排序去重
|
||||
fn collect_to_vec(&self) -> Result<Option<Vec<HsvRange>>, String> {
|
||||
// 1. 触发自检
|
||||
self.validate_self()?;
|
||||
|
||||
let total_capacity = self.estimated_count();
|
||||
if total_capacity == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 2. 永远一击必中分配精准内存,不需要再考虑追加和扩容!
|
||||
let mut v = Vec::with_capacity(total_capacity.max(16));
|
||||
|
||||
// 2. 倒入数据
|
||||
self.append_ranges(&mut v);
|
||||
|
||||
// 3. 原地完成排序与去重
|
||||
v.sort_unstable();
|
||||
v.dedup();
|
||||
|
||||
Ok(Some(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorFilter for ColorPreset {
|
||||
fn append_ranges(&self, target: &mut Vec<HsvRange>) {
|
||||
// 直接利用我们第一步写好的 matches() 拿到切片,整块高速拷贝倒入目标容器
|
||||
target.extend_from_slice(self.matches());
|
||||
}
|
||||
fn estimated_count(&self) -> usize {
|
||||
// 直接获取切片长度
|
||||
self.matches().len()
|
||||
}
|
||||
fn validate_self(&self) -> Result<(), String> {
|
||||
// 直接调用我们在第一步中为 ColorPreset 实现的精细化分流校验
|
||||
// 快捷变体在这里会直接返回 Ok(()), 只有 Custom 才会去真正校验
|
||||
self.validate()
|
||||
}
|
||||
}
|
||||
|
||||
/// 多路颜色“或”逻辑组合子(并集网络)
|
||||
pub struct MultiOrColorRestrict<'a> {
|
||||
pub filters: Vec<&'a dyn ColorFilter>,
|
||||
}
|
||||
|
||||
impl<'a> ColorFilter for MultiOrColorRestrict<'a> {
|
||||
fn append_ranges(&self, target: &mut Vec<HsvRange>) {
|
||||
// 管道递延:依次指挥内部每一个子过滤器把数据倒进目标容器
|
||||
for f in &self.filters {
|
||||
f.append_ranges(target);
|
||||
}
|
||||
}
|
||||
fn estimated_count(&self) -> usize {
|
||||
// 数量累加:$O(1)$ 地把所有子过滤器的预估容量加起来
|
||||
self.filters.iter().map(|f| f.estimated_count()).sum()
|
||||
}
|
||||
|
||||
fn validate_self(&self) -> Result<(), String> {
|
||||
// 递归政审:只要其中一个子过滤器校验失败(比如某个 Custom 变体非法),立刻熔断
|
||||
for f in &self.filters {
|
||||
f.validate_self()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 4. 声明式宏:一语定乾坤
|
||||
// =====================================================================
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! color_any_of {
|
||||
($only:expr) => {
|
||||
&$only as &dyn $crate::ColorFilter
|
||||
};
|
||||
($($filter:expr),+ $(,)?) => {
|
||||
&$crate::MultiOrColorRestrict {
|
||||
filters: vec![ $( &$filter as &dyn $crate::ColorFilter ),+ ]
|
||||
}
|
||||
};
|
||||
}
|
||||
537
ddddocr-core/src/ocr/executor.rs
Normal file
537
ddddocr-core/src/ocr/executor.rs
Normal file
@@ -0,0 +1,537 @@
|
||||
use crate::ocr::metadata::Resize;
|
||||
|
||||
use crate::ocr::color_filter::{HsvRange, apply_to_image};
|
||||
// use ddddocr_tract::session::{ModelOutput, OcrSession};
|
||||
use crate::utils::image_io::png_rgba_white_preprocess;
|
||||
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
||||
use anyhow::Result;
|
||||
use image::DynamicImage;
|
||||
use serde::Serialize;
|
||||
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::{OcrEngine, OcrOutput};
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum OcrResult {
|
||||
/// 纯文本分支(对应 probability = false)
|
||||
Text(String),
|
||||
/// 包含全量概率的分支(对应 probability = true)
|
||||
Probability {
|
||||
text: String,
|
||||
/// 满额概率矩阵 [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,
|
||||
OcrResult::Probability { text, .. } => text,
|
||||
OcrResult::Unsupported { message } => {
|
||||
// 作为库,这里可以返回空,或者直接携带错误信息,取决于你的设计
|
||||
format!("Error: {}", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl fmt::Display for OcrResult {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
OcrResult::Text(text) => {
|
||||
// 纯文本分支,直接输出文本内容
|
||||
write!(f, "{}", text)
|
||||
}
|
||||
OcrResult::Probability {
|
||||
text,
|
||||
probabilities,
|
||||
confidence,
|
||||
} => {
|
||||
// 概率分支,友好地展示文本以及百分比形式的置信度
|
||||
// 1. 基本信息
|
||||
write!(f, "{} (置信度: {:.2}%)", text, confidence * 100.0)?;
|
||||
|
||||
// 2. 概率矩阵流式安全打印
|
||||
write!(f, " [概率矩阵预览: ")?;
|
||||
|
||||
let max_steps_to_show = 10;
|
||||
let take_steps = probabilities.iter().take(max_steps_to_show);
|
||||
|
||||
for (i, step_probs) in take_steps.enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
|
||||
// 为了防止单行内部数据过长,单行也做一下截断保护(比如每行最多显示前 3 个概率)
|
||||
let max_classes_to_show = 3;
|
||||
write!(f, "[")?;
|
||||
for (j, prob) in step_probs.iter().take(max_classes_to_show).enumerate() {
|
||||
if j > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{:.4}", prob)?;
|
||||
}
|
||||
if step_probs.len() > max_classes_to_show {
|
||||
write!(f, ", ..")?;
|
||||
}
|
||||
write!(f, "]")?;
|
||||
}
|
||||
|
||||
// 如果总 Step 数量超过 10,末尾追加 .. 表示截断
|
||||
if probabilities.len() > max_steps_to_show {
|
||||
write!(f, ", ..")?;
|
||||
}
|
||||
write!(f, "]")
|
||||
}
|
||||
OcrResult::Unsupported { message } => {
|
||||
// 错误分支,直观输出异常原因
|
||||
write!(f, "未识别成功: {}", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ocr<'a> {
|
||||
pub(crate) session: &'a dyn OcrEngine,
|
||||
pub(crate) png_fix: bool,
|
||||
pub(crate) probability: bool,
|
||||
/// 颜色过滤:保留的颜色列表
|
||||
pub(crate) final_color_ranges: Result<Option<Vec<HsvRange>>, String>,
|
||||
|
||||
/// 字符集范围
|
||||
pub(crate) final_charset_indices: Option<Vec<usize>>,
|
||||
}
|
||||
|
||||
impl<'a> Ocr<'a> {
|
||||
// 初始化任务,设置默认参数
|
||||
|
||||
pub fn new(session: &'a dyn OcrEngine) -> Self {
|
||||
Ocr {
|
||||
session,
|
||||
png_fix: false, // 默认值
|
||||
probability: false,
|
||||
final_color_ranges: Ok(None),
|
||||
final_charset_indices: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
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.final_color_ranges {
|
||||
Err(err_msg) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"颜色过滤器初始化失败,全链路短路: {}",
|
||||
err_msg
|
||||
));
|
||||
}
|
||||
Ok(None) => {
|
||||
// 核心优化点:直接借用原图,不发生任何克隆
|
||||
Cow::Borrowed(image)
|
||||
}
|
||||
Ok(Some(ranges)) => {
|
||||
// 只有真正需要过滤时,才在内部提取像素并生成清洗后的 Owned 新图
|
||||
let filtered_img = apply_to_image(image, ranges)?;
|
||||
Cow::Owned(filtered_img)
|
||||
}
|
||||
};
|
||||
let tensor = self.preprocess_image(&img_cow)?;
|
||||
|
||||
let raw_tensor = self.session.inference(tensor)?;
|
||||
|
||||
// 3. 后处理分流:直接返回 OcrResult
|
||||
// let ocr_output = match raw_tensor.datum_type() {
|
||||
// DatumType::I64 => self.process_i64_tensor(raw_tensor)?,
|
||||
// DatumType::F32 => self.process_f32_tensor(raw_tensor)?,
|
||||
// _ => OcrResult::Unsupported {
|
||||
// message: format!("不支持的模型输出数据类型: {:?}", raw_tensor.datum_type()),
|
||||
// },
|
||||
// };
|
||||
|
||||
// let raw_indices = self.ocr.extract_indices_from_tensor(&raw_tensor)?;
|
||||
// // 步骤 2: 将索引切片 `&[i64]` 传给解码器进行 CTC 去重和字符映射
|
||||
// let final_text = self.ctc_decode_to_string(&raw_indices);
|
||||
let ocr_output = self.process_model_output(raw_tensor);
|
||||
ocr_output
|
||||
}
|
||||
/// 对应 Python 的 _preprocess_image
|
||||
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
|
||||
fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result<ndarray::Array4<f32>> {
|
||||
// 1. 获取模型元数据配置
|
||||
let meta = self.session.metadata();
|
||||
let norm = &meta.normalization; // 获取归一化器
|
||||
|
||||
// A. 修复 PNG 透明背景 (内部逻辑你之前已实现)
|
||||
let current_img = if self.png_fix && img.color().has_alpha() {
|
||||
// 只有满足条件才去触发分配,生成新图
|
||||
Cow::Owned(png_rgba_white_preprocess(img))
|
||||
} else {
|
||||
// 正常情况下,仅仅是再次安全借用,无开销
|
||||
Cow::Borrowed(img)
|
||||
};
|
||||
|
||||
// 3. 管道节点 2: 根据 Resize 策略计算目标宽高并进行缩放
|
||||
let (target_w, target_h) = match meta.resize {
|
||||
Resize::Fixed(w, h) => (w, h),
|
||||
Resize::DynamicWidth(h) => {
|
||||
// 高度固定,宽度根据原始比例动态计算:W_target = W_orig * (H_target / H_orig)
|
||||
let w =
|
||||
(current_img.width() as f32 * (h as f32 / current_img.height() as f32)) as u32;
|
||||
(w, h)
|
||||
}
|
||||
Resize::Square(size) => {
|
||||
// 单字识别模型,直接缩放为正方形
|
||||
(size, size)
|
||||
}
|
||||
};
|
||||
// 执行缩放
|
||||
let resized_img = resize_image(¤t_img, target_w, target_h);
|
||||
|
||||
// 4. 管道节点 3: 颜色通道转换(单通道灰度 vs 三通道 RGB)与 4D 张量填充
|
||||
let array4 = match meta.channel {
|
||||
// --- 情况 A: 单通道(灰度图),对应 Python 的 len(shape) == 2 展开 ---
|
||||
1 => {
|
||||
let gray_img = convert_to_grayscale(&resized_img);
|
||||
|
||||
let array = ndarray::Array4::from_shape_fn(
|
||||
(1, 1, target_h as usize, target_w as usize),
|
||||
|(_, _, y, x)| {
|
||||
let pixel = gray_img.get_pixel(x as u32, y as u32)[0] as f32;
|
||||
// pixel / 255.0 // 严格对齐 Python 归一化 [0.0, 1.0]
|
||||
// (pixel / 255.0 - 0.5) / 0.5
|
||||
norm.normalize(pixel)
|
||||
},
|
||||
);
|
||||
array
|
||||
}
|
||||
|
||||
// --- 情况 B: 三通道(RGB),对应 Python 的 transpose(2, 0, 1) 的 CHW 布局 ---
|
||||
3 => {
|
||||
let rgb_img = resized_img.to_rgb8();
|
||||
|
||||
let array = ndarray::Array4::from_shape_fn(
|
||||
(1, 3, target_h as usize, target_w as usize),
|
||||
|(_, c, y, x)| {
|
||||
let pixel = rgb_img.get_pixel(x as u32, y as u32)[c] as f32;
|
||||
// pixel / 255.0 // 严格对齐 Python 归一化 [0.0, 1.0]
|
||||
// (pixel / 255.0 - 0.5) / 0.5
|
||||
norm.normalize(pixel)
|
||||
},
|
||||
);
|
||||
// Tensor::from(array)
|
||||
array
|
||||
}
|
||||
|
||||
_ => return Err(anyhow::anyhow!("不支持的通道数配置: {}", meta.channel)),
|
||||
};
|
||||
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
|
||||
fn process_model_output(&self, output: OcrOutput) -> anyhow::Result<OcrResult> {
|
||||
match output {
|
||||
OcrOutput::Indices(array1) => {
|
||||
// 对应你原来的 process_i64_tensor
|
||||
let slice = array1
|
||||
.as_slice()
|
||||
.ok_or_else(|| anyhow::anyhow!("内存不连续,无法执行零拷贝解码"))?;
|
||||
let final_text = self.ctc_decode_to_string(slice);
|
||||
|
||||
if self.probability {
|
||||
Ok(OcrResult::Probability {
|
||||
text: final_text,
|
||||
probabilities: vec![],
|
||||
confidence: 1.0,
|
||||
})
|
||||
} else {
|
||||
Ok(OcrResult::Text(final_text))
|
||||
}
|
||||
}
|
||||
OcrOutput::Logits(matrix_view) => {
|
||||
// 对应你原来的 process_f32_tensor
|
||||
// 注意:此时的 matrix_view 已经是干净的标准的 ndarray::Array2<f32>,且保证是 [Steps, Classes] 2D 形状
|
||||
if self.probability {
|
||||
let (probabilities_list, confidence, predicted_indices) =
|
||||
self.compute_f32_full_probability(matrix_view.view());
|
||||
let final_text = self.ctc_decode_to_string(&predicted_indices);
|
||||
Ok(OcrResult::Probability {
|
||||
text: final_text,
|
||||
probabilities: probabilities_list,
|
||||
confidence: confidence as f64,
|
||||
})
|
||||
} else {
|
||||
let predicted_indices: Vec<i64> = matrix_view
|
||||
.outer_iter()
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.enumerate()
|
||||
.max_by(|(_, a), (_, b)| a.total_cmp(b))
|
||||
.map(|(idx, _)| idx as i64)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let final_text = self.ctc_decode_to_string(&predicted_indices);
|
||||
Ok(OcrResult::Text(final_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Ocr<'a> {
|
||||
fn is_valid_indices(&self, idx: usize) -> bool {
|
||||
if idx >= self.session.metadata().charset.size() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &self.final_charset_indices {
|
||||
Some(v) => v.binary_search(&idx).is_ok(),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
/// 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印
|
||||
/// 这里的 &str 完美借用了自 tokens,依然是彻底的零拷贝!
|
||||
pub fn valid_tokens(&self) -> Vec<&str> {
|
||||
let charset = &self.session.metadata().charset;
|
||||
let tokens = &charset.tokens;
|
||||
match &self.final_charset_indices {
|
||||
Some(indices) => indices
|
||||
.iter()
|
||||
.filter_map(|&idx| tokens.get(idx).map(|cow| cow.as_ref()))
|
||||
.collect(),
|
||||
// 如果是 None,现场映射出全量 Token 视图给外部
|
||||
None => tokens.iter().map(|cow| cow.as_ref()).collect(),
|
||||
}
|
||||
}
|
||||
pub fn valid_size(&self) -> usize {
|
||||
match &self.final_charset_indices {
|
||||
Some(indices) => indices.len(),
|
||||
None => self.session.metadata().charset.tokens.len(),
|
||||
}
|
||||
}
|
||||
/// 变体 B 核心处理器:单次遍历 2D 视图,融合计算 Softmax、Argmax、置信度并输出概率大包
|
||||
fn compute_f32_full_probability(
|
||||
&self,
|
||||
matrix_view: ArrayView2<f32>,
|
||||
) -> (Vec<Vec<f32>>, f32, Vec<i64>) {
|
||||
let steps = matrix_view.nrows();
|
||||
let classes = matrix_view.ncols();
|
||||
|
||||
// 1. 预分配满额概率矩阵内存
|
||||
let mut prob_matrix = ndarray::Array2::<f32>::zeros((steps, classes));
|
||||
let mut predicted_indices = Vec::with_capacity(steps);
|
||||
let mut confidence_sum = 0.0f32;
|
||||
|
||||
// 2. 融合单次遍历
|
||||
for (step_idx, row) in matrix_view.outer_iter().enumerate() {
|
||||
// 寻找当前 Step 的最大值和最大值索引 (Argmax)
|
||||
let (row_max_idx, max_logit) = row
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|(_, a), (_, b)| a.total_cmp(b))
|
||||
.map(|(idx, &val)| (idx, val))
|
||||
.unwrap_or((0, 0.0));
|
||||
|
||||
predicted_indices.push(row_max_idx as i64);
|
||||
|
||||
// 计算单行 exp 溢出防范和
|
||||
let mut exp_sum = 0.0f32;
|
||||
for &val in row.iter() {
|
||||
exp_sum += (val - max_logit).exp();
|
||||
}
|
||||
|
||||
// 归一化 Softmax 顺序写入
|
||||
for (class_idx, &val) in row.iter().enumerate() {
|
||||
prob_matrix[[step_idx, class_idx]] = (val - max_logit).exp() / exp_sum;
|
||||
}
|
||||
|
||||
// 当前 Step 最大概率在线累加
|
||||
confidence_sum += 1.0f32 / exp_sum;
|
||||
}
|
||||
|
||||
// 3. 统计全局平均置信度
|
||||
let confidence = if steps > 0 {
|
||||
confidence_sum / steps as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
// 4. 将矩阵转化为标准安全序列化格式 [Steps, Classes]
|
||||
let probabilities_list: Vec<Vec<f32>> =
|
||||
prob_matrix.outer_iter().map(|row| row.to_vec()).collect();
|
||||
|
||||
(probabilities_list, confidence, predicted_indices)
|
||||
}
|
||||
/// 变体 A 专属提取器:直接从 I64 Tensor 零拷贝提取 CTC 文本与初始概率包
|
||||
// fn process_i64_tensor(&self, raw_tensor: Tensor) -> anyhow::Result<OcrResult> {
|
||||
// // 1. 拿到底层的动态维度只读视图
|
||||
// let view = raw_tensor.to_array_view::<i64>()?;
|
||||
//
|
||||
// // 2. 索要底层连续的只读切片引用
|
||||
// let slice = view
|
||||
// .as_slice()
|
||||
// .ok_or_else(|| anyhow::anyhow!("I64 模型输出内存不连续,无法执行零拷贝解码"))?;
|
||||
//
|
||||
// // 3. 直接喂给 CTC 解码器(无任何物理克隆开销)
|
||||
// let final_text = self.ctc_decode_to_string(slice);
|
||||
//
|
||||
// // 4. 组装返回
|
||||
// if self.probability {
|
||||
// Ok(OcrResult::Probability {
|
||||
// text: final_text,
|
||||
// probabilities: vec![], // I64 模型物理上丢失了全量 Logits 分值网,降级处理
|
||||
// confidence: 1.0, // 判定即百分之百置信
|
||||
// })
|
||||
// } else {
|
||||
// Ok(OcrResult::Text(final_text))
|
||||
// }
|
||||
// }
|
||||
// /// 变体二(F32)的总体管线:负责降维,并分流文本和概率
|
||||
// fn process_f32_tensor(&self, raw_tensor: Tensor) -> anyhow::Result<OcrResult> {
|
||||
// let shape = raw_tensor.shape();
|
||||
// println!("模型输出shape数据: {:?}", shape);
|
||||
// let view = raw_tensor.to_array_view::<f32>()?;
|
||||
//
|
||||
// // 1. 极其纯粹的、无拷贝的多维 Shape 压扁清洗
|
||||
// let (steps, classes, data_dyn_view) = match shape.len() {
|
||||
// 3 => {
|
||||
// if shape[1] == 1 {
|
||||
// // 形状: [Steps, 1, Classes] -> 你的原有逻辑
|
||||
// (shape[0], shape[2], view.into_dyn())
|
||||
// } else if shape[0] == 1 {
|
||||
// // 形状: [1, Steps, Classes] -> 另一种常见导出格式
|
||||
// (shape[1], shape[2], view.into_dyn())
|
||||
// } else {
|
||||
// // 默认取第一个 batch: [Batch, Steps, Classes]
|
||||
// // 使用 slice 对应 Python 的 output[0, :, :]
|
||||
// let sliced = view.slice(s![0, .., ..]);
|
||||
// (shape[1], shape[2], sliced.into_dyn())
|
||||
// }
|
||||
// }
|
||||
// // 形状: [Steps, Classes] -> 已经剥离了 Batch 维度
|
||||
// 2 => (shape[0], shape[1], view.into_dyn()),
|
||||
// // 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑)
|
||||
// // 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
|
||||
// 1 => (1, shape[0], view.into_dyn()),
|
||||
// _ => return Err(anyhow::anyhow!("不支持的输出维度: {:?}", shape)),
|
||||
// };
|
||||
// let matrix_cow = data_dyn_view
|
||||
// .to_shape(Ix2(steps, classes))
|
||||
// .map_err(|e| anyhow::anyhow!("转换为2D静态矩阵失败: {:?}", e))?;
|
||||
//
|
||||
// let matrix_view: ArrayView2<f32> = matrix_cow.view();
|
||||
//
|
||||
// // 2. 根据业务参数明确分流
|
||||
// if self.probability {
|
||||
// // 走向 B1:调用刚刚拆分出来的“全量概率计算器”
|
||||
// let (probabilities_list, confidence, predicted_indices) =
|
||||
// self.compute_f32_full_probability(matrix_view);
|
||||
// // 5. 执行 CTC 解码
|
||||
// let final_text = self.ctc_decode_to_string(&predicted_indices);
|
||||
//
|
||||
// Ok(OcrResult::Probability {
|
||||
// text: final_text,
|
||||
// probabilities: probabilities_list,
|
||||
// confidence: confidence as f64,
|
||||
// })
|
||||
// } else {
|
||||
// // 走向 B2:极速免 Softmax 提取纯文本(代码保持原地提取,简单短小不需要再拆)
|
||||
// let predicted_indices: Vec<i64> = matrix_view
|
||||
// .outer_iter()
|
||||
// .map(|row| {
|
||||
// row.iter()
|
||||
// .enumerate()
|
||||
// .max_by(|(_, a), (_, b)| a.total_cmp(b))
|
||||
// .map(|(idx, _)| idx as i64)
|
||||
// .unwrap_or(0)
|
||||
// })
|
||||
// .collect();
|
||||
//
|
||||
// let final_text = self.ctc_decode_to_string(&predicted_indices);
|
||||
// Ok(OcrResult::Text(final_text))
|
||||
// }
|
||||
// }
|
||||
/// 获取有效字符索引列表 (用于外部验证或过滤)
|
||||
fn ctc_decode_to_string(&self, predicted_indices: &[i64]) -> String {
|
||||
println!("indices模型输出原始数据: {:?}", predicted_indices);
|
||||
let charset = &self.session.metadata().charset;
|
||||
let tokens = &charset.tokens;
|
||||
// let valid_indices = &charset.valid_indices;
|
||||
|
||||
// 对应 _ctc_decode_indices 的逻辑:去重、去 blank (0)
|
||||
let mut res = String::new();
|
||||
let mut prev_idx: i64 = -1;
|
||||
|
||||
for &idx in predicted_indices {
|
||||
// 1. CTC 去重:如果是连续重复的,直接跳过
|
||||
if idx == prev_idx {
|
||||
continue;
|
||||
}
|
||||
// 【关键核心】只要不是连续重复,立刻更新 prev_idx 状态,绝对不能被后续的过滤短路!
|
||||
prev_idx = idx;
|
||||
|
||||
// 2. CTC 过滤 Blank (0)
|
||||
if idx == 0 {
|
||||
continue;
|
||||
}
|
||||
// 3. 类型安全转换
|
||||
let u_idx = match usize::try_from(idx) {
|
||||
Ok(u) => u,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// 史诗级加速点:如果是 None,说明没限制,根本不进入分支,直接放行!
|
||||
// 只有当有具体限制(Some)时,才去跑 4-5 次 CPU 寄存器级别的二分查找
|
||||
if let Some(ref indices) = self.final_charset_indices {
|
||||
if indices.binary_search(&u_idx).is_err() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 字符映射
|
||||
if let Some(char_str) = tokens.get(u_idx) {
|
||||
res.push_str(char_str);
|
||||
} else {
|
||||
eprintln!("警告: 预测索引 {} 超出字符集范围", u_idx);
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
200
ddddocr-core/src/ocr/metadata.rs
Normal file
200
ddddocr-core/src/ocr/metadata.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ==========================================
|
||||
// 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. 辅助定义的枚举与结构体
|
||||
// =====================================================================
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")] // 支持 json 中写 "zero_to_one" 或 "minus_one_to_one"
|
||||
pub enum Normalization {
|
||||
/// 映射到 [0.0, 1.0] -> pixel / 255.0
|
||||
ZeroToOne,
|
||||
/// 映射到 [-1.0, 1.0] -> (pixel / 255.0 - 0.5) / 0.5
|
||||
MinusOneToOne,
|
||||
}
|
||||
|
||||
impl Normalization {
|
||||
/// 统一归一化计算逻辑
|
||||
#[inline(always)]
|
||||
pub fn normalize(&self, pixel: f32) -> f32 {
|
||||
match self {
|
||||
Normalization::ZeroToOne => pixel / 255.0,
|
||||
Normalization::MinusOneToOne => (pixel / 255.0 - 0.5) / 0.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 图像缩放策略枚举
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Resize {
|
||||
/// 固定宽高,例如 (64, 64)
|
||||
Fixed(u32, u32),
|
||||
/// 高度固定,宽度根据原始比例动态计算(对应 Python 的 [-1, H])
|
||||
DynamicWidth(u32),
|
||||
/// 单字识别的正方形切图(对应 Python 的 word 为 True 且 [-1, H])
|
||||
Square(u32),
|
||||
}
|
||||
|
||||
/// 仅用于反序列化 JSON 的中间临时结构体(DTO)
|
||||
#[derive(Deserialize)]
|
||||
struct ModelMetadataDto {
|
||||
charset: Vec<String>,
|
||||
word: bool,
|
||||
#[serde(alias = "image")]
|
||||
resize: Vec<i32>,
|
||||
channel: u8,
|
||||
/// 新增:允许在配置文件中指定归一化策略。
|
||||
/// 使用 serde(default) 可以在不配置时提供一个默认值(比如默认 ZeroToOne)
|
||||
#[serde(default = "default_normalization")]
|
||||
normalization: Normalization,
|
||||
}
|
||||
fn default_normalization() -> Normalization {
|
||||
Normalization::ZeroToOne
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModelMetadata {
|
||||
/// 字符集管理器
|
||||
pub charset: Charset,
|
||||
/// 是否为单字识别模型
|
||||
pub word: bool,
|
||||
/// 预处理的缩放策略
|
||||
pub resize: Resize,
|
||||
/// 图像通道数 (1 或 3)
|
||||
pub channel: u8,
|
||||
/// 新增:传递给核心业务使用的归一化配置
|
||||
pub normalization: Normalization,
|
||||
}
|
||||
|
||||
impl ModelMetadata {
|
||||
// --- 优雅的工厂模式构造器 ---
|
||||
/// 通用的静态切片转换构造器
|
||||
pub fn from_static_slice(
|
||||
slice: &[&'static str],
|
||||
word: bool,
|
||||
resize: Resize,
|
||||
channel: u8,
|
||||
normalization: Normalization,
|
||||
) -> Self {
|
||||
let tokens: Vec<Cow<'static, str>> = slice.iter().map(|&s| Cow::Borrowed(s)).collect();
|
||||
Self {
|
||||
charset: Charset::new(tokens),
|
||||
word,
|
||||
resize,
|
||||
channel,
|
||||
normalization,
|
||||
}
|
||||
}
|
||||
pub fn from_json_str(json_str: &str) -> Result<Self> {
|
||||
let dto: ModelMetadataDto = serde_json::from_str(json_str)
|
||||
.map_err(|e| anyhow!("JSON 反序列化失败,请检查字段是否完整: {}", e))?;
|
||||
|
||||
// 1. 将 DTO 的字符串数组转化为强类型的 Charset
|
||||
let tokens: Vec<Cow<'static, str>> =
|
||||
dto.charset.into_iter().map(|s| Cow::Owned(s)).collect();
|
||||
let charset = Charset::new(tokens);
|
||||
|
||||
// 2. 解析 resize 策略(重现 Python 的复杂条件判断)
|
||||
if dto.resize.len() != 2 {
|
||||
return Err(anyhow!(
|
||||
"'resize (or image)' 字段必须是包含两个元素的数组,例如 [-1, 64]"
|
||||
));
|
||||
}
|
||||
let r0 = dto.resize[0];
|
||||
let r1 = dto.resize[1];
|
||||
|
||||
let resize = if r0 == -1 {
|
||||
if dto.word {
|
||||
// 如果 word 为 true,且包含 -1,Python 里是 resize 为 (r1, r1) 的正方形
|
||||
Resize::Square(r1 as u32)
|
||||
} else {
|
||||
// 如果 word 为 false,且包含 -1,Python 里是高度固定为 r1,宽度按原图比例缩放
|
||||
Resize::DynamicWidth(r1 as u32)
|
||||
}
|
||||
} else {
|
||||
// 正常的固定宽高
|
||||
Resize::Fixed(r0 as u32, r1 as u32)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
charset,
|
||||
word: dto.word,
|
||||
resize,
|
||||
channel: dto.channel,
|
||||
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)
|
||||
}
|
||||
}
|
||||
146
ddddocr-core/src/ocr/token_filter.rs
Normal file
146
ddddocr-core/src/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 ),+ ]
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user