8 Commits

Author SHA1 Message Date
7f1ce04f50 refactor(slide,det): 重构目标检测引擎并统一图像输入类型为 DynamicImage以及滑块匹配与比较引擎为 Rust 实现
- 统一 `predict` 和 `get_bbox` 接口参数为 `&DynamicImage`,消除多步处理时的重复图像解码开销。
- 引入轻量级 `DetectionResult` 结构体和固定大小数组 `[f32; 6]` 替代旧的嵌套 `Vec`,彻底消除后处理中的内存碎片。
- 优化 `preproc` 预处理逻辑,使用连续内存切片批量操作替代原有的逐像素迭代遍历。
- 移除多余的 `multiclass_nms_class_agnostic` 转发层,合并并精简 NMS 聚合函数。
- 优化 `calculate_center` 几何中心点计算函数,提高泛型语义并复用于两种匹配模式
- 在执行核心算法前增加尺寸与通道边界守卫(Guard Clauses),提升库的防防御性编程能力与崩溃安全性
- 移除多余的错误二次包装(map_err),改由 Rust 原生 Result 错误传播机制直接向上层抛出
2026-07-03 17:51:28 +08:00
22cc9709ad refactor(predict): 重构预测流水线并优化模型元数据与输出架构
- 优化 `predict` 核心方法:移除冗余日志与深层嵌套,将流程重塑为线性流水线。
- 重构 `compute_f32_full_probability`:解耦逻辑与外部状态,消除并发隐患与生命周期冲突。
- 增强 `ModelMetadata`:引入动态归一化配置并支持 Serde 序列化,解决特定模型漏字问题。
- 升级 `OcrOutput`:
  - 增加 `Unsupported` 变体以支持非致命异常的优雅降级。
  - 实现 `into_text(self)` 方法与 `Display` 特征(应用双重截断保护,防止日志刷屏)。

BREAKING CHANGE: `predict` 返回值由 `anyhow::Result<String>` 改为 `anyhow::Result<OcrOutput>`,将后处理和控制权移交上层。
2026-07-02 20:24:52 +08:00
b352fc344f refactor(ocr): 优化 preprocess_image 逻辑实现,修复部分BUG
- 优化 `OcrBuilder` 重名名为 `OcrPredictor`
 - 优化 `OcrPredictor` 的 `preprocess_image` 支持多种图像管道。
 - 修复 `OcrPredictor` 引发的并发BUG。
2026-07-01 20:22:17 +08:00
48c2cbedb0 refactor(ocr): 优化并精简 color_filter 架构设计
- 重构 `OcrBuilder`,将图像矩阵过滤与像素比对等执行层逻辑彻底剥离解耦。
 - 优化 `OcrBuilder` 的 `color_filter` 链式调用,将其改造为无心智负担的单次覆盖(Overwrite)逻辑。
 - 扩展 `ColorFilter` 特征,新增 `collect_to_vec` 方法,实现底层规则的高内聚收集、精准内存开辟与原地去重排序。
2026-06-29 17:19:01 +08:00
2f86694c54 refactor(ocr): 优化 color_filter.rs
- 重构 `OcrBuilder` 移除is_pixel_matched,filter_image。
 - 优化 `OcrBuilder` 的color_filter方法(部分逻辑转移给merge_to_vec) 。
 - 新增 `ColorFilter` 特征增加merge_to_vec方法。
2026-06-25 20:25:49 +08:00
62d5e7a0ca refactor(ocr): 优化 HSV 颜色过滤架构,实现快捷预设免检与大一统 Custom 变体
- 重构 `ColorPreset` 枚举,新增 `Custom(Vec<HsvRange>)` 变体。
 - 优化 `ColorFilter` 特征兼容多路组合宏。
 - 新增 `validate_self` 特征多态方法,实现责任分离:库担保的快捷预设 0 运行时开销免检放行,仅对 `Custom` 动态数据进行严格自检。
 - 优化 `OcrBuilder::color_filter` 接收 `&dyn ColorFilter` 特征对象,完美兼容原有声明式宏与链式调用熔断机制。
 - 借鉴 `reqwest` 的延迟错误处理模式,完善 `OcrBuilder` 的链式调用熔断(毒化)状态机。
2026-06-18 17:40:29 +08:00
189f2bd697 refactor(ocr): 拆分字符集限制枚举并引入声明式多路组合子宏
- 将原本臃肿的 CharsetRestrict 拆分为 CharRestrict(内容过滤)和 IdRestrict(索引过滤),实现职责解耦
 - 引入 any_of! 声明式宏实现无 Box、无堆内存分配的栈上多路组合,规避孤儿规则
 - 完善 estimated_capacity 容量预估函数,实现真正的 O(1) 精准内存开辟
2026-06-16 09:37:15 +08:00
b7146831f7 refactor: 优化 OcrBuilder 新增通过索引范围控制有效字符集
- 修改 `charset_restrict`类型修改为Option<Vec<usize>> 并重构同名方法数据处理逻辑
- 优化 `ctc_decode_to_string` 内部复用策略计算,通过 `Option` 结构实现无限制请求的全量免检短路加速
- 新增 `CharsetRestrict`枚举新增变体`TopN(usize)` 实现通过索引范围控制有效字符集
2026-06-13 17:13:00 +08:00
11 changed files with 1148 additions and 382 deletions

View File

@@ -517,81 +517,152 @@ pub const CHARSET_BETA: &[&str] = &[
pub const CHARSET_OLD: &[&str] = &["", "", "", "", ""]; pub const CHARSET_OLD: &[&str] = &["", "", "", "", ""];
pub fn get_default_charset() -> Vec<String> {
CHARSET_BETA.iter().map(|&s| s.to_string()).collect()
}
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::ops::{Add, Deref}; /// 字符集范围限制枚举
// 字符集范围类型 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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum CharsetRestrict { pub enum CharRestrict {
/// 纯整数 0-9
Digit, Digit,
/// 纯小写字母 a-z
Lowercase, Lowercase,
/// 纯大写字母 A-Z
Uppercase, Uppercase,
// /// 过滤模式:删除所有 ASCII 字母和数字(通常用于仅保留汉字、特殊标点)
// // ExcludeAlphanumeric,
// /// 自定义单字字符集,例如 "0123456789+-x/="
// Single(String),
/// 直接设置完整的 Token 白名单(支持多字 Token例如 vec!["html".to_string()]
CustomList(Vec<String>), CustomList(Vec<String>),
}
/// 核心组合子:满足左边或右边任意一个条件即可(即 A + B 的并集逻辑) impl TokenFilter for CharRestrict {
/// 使用 Box 打破 Rust 编译期对递归枚举的无限大小限制 fn matches(&self, ctx: &ValidationCtx) -> bool {
Or(Box<CharsetRestrict>, Box<CharsetRestrict>),
}
impl From<i32> for CharsetRestrict {
fn from(value: i32) -> Self {
match value {
0 => Self::Digit,
1 => Self::Lowercase,
2 => Self::Uppercase,
// 3 => Self::LowercaseUppercase,
// 4 => Self::LowercaseDigit,
// 5 => Self::UppercaseDigit,
// 6 => Self::LowercaseUppercaseDigit,
// 7 => Self::DefaultCharsetLowercaseUppercaseDigit,
_ => panic!("invalid charset range: {}", value),
}
}
}
impl CharsetRestrict {
/// 💡 辅助构造函数:直接在源头把用户的长字符串切碎,伪装成基础积木
pub fn from_chars(custom_str: &str) -> Self {
let tokens = custom_str.chars().map(|c| c.to_string()).collect();
CharsetRestrict::CustomList(tokens)
}
// 内部递归收集器:利用硬编码切片快速无损展开
pub(crate) fn matches(&self, s: &str) -> bool {
match self { match self {
CharsetRestrict::Digit => s.len() == 1 && s.as_bytes()[0].is_ascii_digit(), Self::Digit => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_digit(),
CharsetRestrict::Lowercase => s.len() == 1 && s.as_bytes()[0].is_ascii_lowercase(), Self::Lowercase => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_lowercase(),
CharsetRestrict::Uppercase => s.len() == 1 && s.as_bytes()[0].is_ascii_uppercase(), Self::Uppercase => ctx.text.len() == 1 && ctx.text.as_bytes()[0].is_ascii_uppercase(),
CharsetRestrict::CustomList(vec) => vec.iter().any(|t| t == s), Self::CustomList(vec) => vec.iter().any(|t| t == ctx.text),
CharsetRestrict::Or(left, right) => left.matches(s) || right.matches(s), }
}
fn estimated_capacity(&self) -> usize {
match self {
Self::Digit => 16,
Self::Lowercase | Self::Uppercase => 32,
Self::CustomList(vec) => vec.len() + 1,
} }
} }
} }
// =====================================================================
// 5. 优雅的魔法:重载 + 运算符 (实现 std::ops::Add)
// =====================================================================
/// 支持 `CharsetRestrict::Digit + CharsetRestrict::Lowercase` #[derive(Debug, Clone, PartialEq, Eq)]
impl Add for CharsetRestrict { pub enum IdRestrict {
type Output = Self; TopN(usize),
IdRange(std::ops::Range<usize>),
IdList(Vec<usize>),
}
fn add(self, rhs: Self) -> Self::Output { impl TokenFilter for IdRestrict {
CharsetRestrict::Or(Box::new(self), Box::new(rhs)) 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. IdListVec 里的元素个数
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 ),+ ]
}
};
} }
// ========================================== // ==========================================
@@ -639,14 +710,12 @@ impl Charset {
self.tokens.get(index).map(|cow| cow.as_ref()) self.tokens.get(index).map(|cow| cow.as_ref())
} }
pub fn is_valid_char(&self, char_str: &str) -> bool { pub fn is_valid_char(&self, char_str: &str) -> bool {
self.char_to_idx.get(char_str).is_some() self.char_to_idx.get(char_str).is_some()
} }
pub fn size(&self) -> usize { pub fn size(&self) -> usize {
self.tokens.len() self.tokens.len()
} }
} }
// ========================================== // ==========================================
@@ -654,10 +723,6 @@ impl Charset {
// ========================================== // ==========================================
impl std::fmt::Display for Charset { impl std::fmt::Display for Charset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(f, "Charset [Total Size: {}", self.size(),)
f,
"Charset [Total Size: {}",
self.size(),
)
} }
} }

View File

@@ -1,20 +1,21 @@
mod charset; mod charset;
mod model_metadata;
pub mod models; pub mod models;
pub mod utils; pub mod utils;
mod model_metadata;
use anyhow::Result; use anyhow::{Result, anyhow};
use image::DynamicImage; use image::DynamicImage;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
// 关键点:直接使用 tract 重导出的 ndarray // 关键点:直接使用 tract 重导出的 ndarray
use crate::charset::get_default_charset; use crate::charset::CharRestrict;
use crate::models::ocr::ColorRange; use crate::model_metadata::ModelMetadata;
use crate::models::det::DetectionResult;
use crate::utils::color_filter::{ColorPreset, HsvRange};
use models::det::Det; use models::det::Det;
use models::loader::ModelSession; use models::loader::ModelSession;
use models::ocr::Ocr; use models::ocr::Ocr;
use crate::model_metadata::ModelMetadata;
pub enum ModelSpec { pub enum ModelSpec {
/// 默认 OCR (使用内置路径) /// 默认 OCR (使用内置路径)
@@ -64,7 +65,10 @@ impl DdddOcrBuilder {
/// 设置自定义 OCR 路径 /// 设置自定义 OCR 路径
pub fn custom_ocr(mut self, path: String, model_metadata: ModelMetadata) -> Self { pub fn custom_ocr(mut self, path: String, model_metadata: ModelMetadata) -> Self {
// 直接重写枚举,替换掉之前的 Ocr 或 Det // 直接重写枚举,替换掉之前的 Ocr 或 Det
self.mode = ModelSpec::CustomOcrModel { path, model_metadata }; self.mode = ModelSpec::CustomOcrModel {
path,
model_metadata,
};
self self
} }
@@ -76,7 +80,10 @@ impl DdddOcrBuilder {
ModelMetadata::from_builtin_beta(), ModelMetadata::from_builtin_beta(),
)?), )?),
ModelSpec::DetModel => Runtime::Det(Det::new(ModelSpec::DEFAULT_DET_PATH.into())?), ModelSpec::DetModel => Runtime::Det(Det::new(ModelSpec::DEFAULT_DET_PATH.into())?),
ModelSpec::CustomOcrModel { path, model_metadata } => Runtime::Ocr(Ocr::new(path, model_metadata)?), ModelSpec::CustomOcrModel {
path,
model_metadata,
} => Runtime::Ocr(Ocr::new(path, model_metadata)?),
}; };
Ok(DdddOcr { runtime }) Ok(DdddOcr { runtime })
@@ -96,11 +103,30 @@ impl Display for DdddOcr {
impl DdddOcr { impl DdddOcr {
pub fn classification(&self, img: &DynamicImage) -> Result<String> { pub fn classification(&self, img: &DynamicImage) -> Result<String> {
match &self.runtime { match &self.runtime {
Runtime::Ocr(s) => s.predict(img).run(), // Runtime::Ocr(s) => s.predict(img).run(),
// Runtime::Ocr(s) => s.predictor().probability(false).predict(img),
// Runtime::Ocr(s) => {
// let predictor = s.predictor();
// let restricted = predictor.charset_restrict(&CharRestrict::Lowercase);
// let a = restricted.valid_tokens();
// println!("{:?}", a);
// Ok("".to_string())
// }
Runtime::Ocr(s) => {
let res = s.predictor().probability(true).predict(img)?;
println!("{}", res);
Ok(res.to_string())
}
// Runtime::Ocr(s) => s.predictor().charset_restrict(&CharRestrict::Digit).predict(img),
// Runtime::Ocr(s) => s.predictor().color_filter(&ColorPreset::Custom(vec![
// // 错误:下界 (82, 221, 14) 没问题
// // 但上界的 H 通道写成了 240超过了 180 的法定上限!
// HsvRange::new((82, 221, 14), (240, 203, 82)),
// ])).predict(img),
Runtime::Det(_) => Err(anyhow::anyhow!("当前模型是检测模型,无法执行 OCR")), Runtime::Det(_) => Err(anyhow::anyhow!("当前模型是检测模型,无法执行 OCR")),
} }
} }
pub fn detection(&self, img: &[u8]) -> Result<Vec<Vec<i32>>> { pub fn detection(&self, img: &DynamicImage) -> Result<Vec<DetectionResult>> {
match &self.runtime { match &self.runtime {
Runtime::Det(s) => s.predict(img), Runtime::Det(s) => s.predict(img),
Runtime::Ocr(_) => Err(anyhow::anyhow!("当前模型是 OCR 模型,无法执行检测")), Runtime::Ocr(_) => Err(anyhow::anyhow!("当前模型是 OCR 模型,无法执行检测")),
@@ -108,39 +134,39 @@ impl DdddOcr {
} }
} }
struct Classification {} // struct Classification {}
#[derive(Debug)] // #[derive(Debug)]
struct ClassificationBuilder { // struct ClassificationBuilder {
img: DynamicImage, // img: DynamicImage,
png_fix: bool, // png_fix: bool,
color_filter_colors: Option<Vec<ColorRange>>, // color_filter_colors: Option<Vec<ColorRange>>,
color_filter_custom_ranges: Option<Vec<ColorRange>>, // color_filter_custom_ranges: Option<Vec<ColorRange>>,
} // }
impl ClassificationBuilder { // impl ClassificationBuilder {
pub fn new(img: DynamicImage) -> Self { // pub fn new(img: DynamicImage) -> Self {
ClassificationBuilder { // ClassificationBuilder {
img, // img,
png_fix: false, // png_fix: false,
color_filter_colors: None, // color_filter_colors: None,
color_filter_custom_ranges: None, // color_filter_custom_ranges: None,
} // }
} // }
pub fn png_fix(mut self, value: bool) -> Self { // pub fn png_fix(mut self, value: bool) -> Self {
self.png_fix = value; // self.png_fix = value;
self // self
} // }
pub fn color_filter_colors(mut self, value: Vec<ColorRange>) -> Self { // pub fn color_filter_colors(mut self, value: Vec<ColorRange>) -> Self {
self.color_filter_colors = Some(value); // self.color_filter_colors = Some(value);
self // self
} // }
pub fn color_filter_custom_ranges(mut self, value: Vec<ColorRange>) -> Self { // pub fn color_filter_custom_ranges(mut self, value: Vec<ColorRange>) -> Self {
self.color_filter_custom_ranges = Some(value); // self.color_filter_custom_ranges = Some(value);
self // self
} // }
pub fn build(self) -> Classification { // pub fn build(self) -> Classification {
Classification {} // Classification {}
} // }
} // }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@@ -1,4 +1,4 @@
use crate::charset::{CHARSET_BETA, CHARSET_OLD, Charset, CharsetRestrict}; use crate::charset::{CHARSET_BETA, CHARSET_OLD, Charset};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use serde::Deserialize; use serde::Deserialize;
use std::borrow::Cow; use std::borrow::Cow;
@@ -10,6 +10,26 @@ use std::path::Path;
// 1. 辅助定义的枚举与结构体 // 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Resize { pub enum Resize {
@@ -29,6 +49,13 @@ struct ModelMetadataDto {
#[serde(alias = "image")] #[serde(alias = "image")]
resize: Vec<i32>, resize: Vec<i32>,
channel: u8, channel: u8,
/// 新增:允许在配置文件中指定归一化策略。
/// 使用 serde(default) 可以在不配置时提供一个默认值(比如默认 ZeroToOne
#[serde(default = "default_normalization")]
normalization: Normalization,
}
fn default_normalization() -> Normalization {
Normalization::ZeroToOne
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -41,18 +68,32 @@ pub struct ModelMetadata {
pub resize: Resize, pub resize: Resize,
/// 图像通道数 (1 或 3) /// 图像通道数 (1 或 3)
pub channel: u8, pub channel: u8,
/// 新增:传递给核心业务使用的归一化配置
pub normalization: Normalization,
} }
impl ModelMetadata { impl ModelMetadata {
// --- 优雅的工厂模式构造器 --- // --- 优雅的工厂模式构造器 ---
/// 从预设的旧版字符集创建 /// 从预设的旧版字符集创建
pub fn from_builtin_old() -> Self { pub fn from_builtin_old() -> Self {
Self::from_static_slice(CHARSET_OLD, false, Resize::Fixed(64, 64), 1) Self::from_static_slice(
CHARSET_OLD,
false,
Resize::DynamicWidth(64),
1,
Normalization::ZeroToOne,
)
} }
/// 从预设的 Beta 版字符集创建 /// 从预设的 Beta 版字符集创建
pub fn from_builtin_beta() -> Self { pub fn from_builtin_beta() -> Self {
Self::from_static_slice(CHARSET_BETA, false, Resize::Fixed(64, 64), 1) Self::from_static_slice(
CHARSET_BETA,
false,
Resize::DynamicWidth(64),
1,
Normalization::MinusOneToOne,
)
} }
/// 通用的静态切片转换构造器 /// 通用的静态切片转换构造器
@@ -61,6 +102,7 @@ impl ModelMetadata {
word: bool, word: bool,
resize: Resize, resize: Resize,
channel: u8, channel: u8,
normalization: Normalization,
) -> Self { ) -> Self {
let tokens: Vec<Cow<'static, str>> = slice.iter().map(|&s| Cow::Borrowed(s)).collect(); let tokens: Vec<Cow<'static, str>> = slice.iter().map(|&s| Cow::Borrowed(s)).collect();
Self { Self {
@@ -68,6 +110,7 @@ impl ModelMetadata {
word, word,
resize, resize,
channel, channel,
normalization,
} }
} }
@@ -117,6 +160,7 @@ impl ModelMetadata {
word: dto.word, word: dto.word,
resize, resize,
channel: dto.channel, channel: dto.channel,
normalization: dto.normalization,
}) })
} }
} }

View File

@@ -4,6 +4,20 @@ use image::{DynamicImage, GenericImageView, imageops::FilterType};
use tract_onnx::prelude::tract_ndarray::{Array2, Array3, Array4, Axis, prelude::*, s}; use tract_onnx::prelude::tract_ndarray::{Array2, Array3, Array4, Axis, prelude::*, s};
use tract_onnx::prelude::{Graph, RunnableModel, Tensor, TypedFact, TypedOp, tvec}; use tract_onnx::prelude::{Graph, RunnableModel, Tensor, TypedFact, TypedOp, tvec};
#[derive(Debug, Clone, Copy)]
pub struct DetectionResult {
pub x1: i32,
pub y1: i32,
pub x2: i32,
pub y2: i32,
pub score: f32,
pub class_id: u32,
}
pub struct Det { pub struct Det {
session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>, session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
} }
@@ -20,14 +34,14 @@ impl Det {
let session = ModelLoader::load_model(&model_path)?.session; let session = ModelLoader::load_model(&model_path)?.session;
Ok(Self { session }) Ok(Self { session })
} }
pub fn predict(&self, image_bytes: &[u8]) -> Result<Vec<Vec<i32>>> { pub fn predict(&self, image: &DynamicImage) -> Result<Vec<DetectionResult>> {
// Rust 中通常在调用层处理文件/PIL转换这里直接进入核心逻辑 // Rust 中通常在调用层处理文件/PIL转换这里直接进入核心逻辑
self.get_bbox(image_bytes) self.get_bbox(image)
} }
/// 2. preproc: 纯 Rust 实现 (替代 OpenCV) /// 2. preproc: 纯 Rust 实现 (替代 OpenCV)
fn preproc(&self, img: &DynamicImage, input_size: (u32, u32)) -> Result<(Tensor, f32)> { fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> Result<(Tensor, f32)> {
let (target_h, target_w) = input_size; let (target_h, target_w) = input_size;
let (img_w, img_h) = img.dimensions(); let (img_w, img_h) = image.dimensions();
// 计算缩放比例 (Letterbox) // 计算缩放比例 (Letterbox)
let r = (target_h as f32 / img_h as f32).min(target_w as f32 / img_w as f32); let r = (target_h as f32 / img_h as f32).min(target_w as f32 / img_w as f32);
@@ -35,7 +49,7 @@ impl Det {
let new_w = (img_w as f32 * r) as u32; let new_w = (img_w as f32 * r) as u32;
// Resize 图像 // Resize 图像
let resized = img.resize_exact(new_w, new_h, FilterType::Triangle); let resized = image.resize_exact(new_w, new_h, FilterType::Triangle);
// 2. 关键:将 DynamicImage 显式转换为 RgbImage (Rgb<u8>) // 2. 关键:将 DynamicImage 显式转换为 RgbImage (Rgb<u8>)
let resized_rgb = resized.to_rgb8(); let resized_rgb = resized.to_rgb8();
// 创建 114 灰度填充的背景 // 创建 114 灰度填充的背景
@@ -45,21 +59,24 @@ impl Det {
// 将 resize 后的图像覆盖到左上角 (类似于原始代码中的 padded_img[:h, :w]) // 将 resize 后的图像覆盖到左上角 (类似于原始代码中的 padded_img[:h, :w])
image::imageops::overlay(&mut base_img, &resized_rgb, 0, 0); image::imageops::overlay(&mut base_img, &resized_rgb, 0, 0);
// 优化:直接获取底层的扁平 raw buffer比 enumerate_pixels() 快得多
let raw_samples = base_img.as_flat_samples();
let slice = raw_samples.as_slice();
// 构造 NCHW Tensor // 构造 NCHW Tensor
let mut array = Array4::<f32>::zeros((1, 3, target_h as usize, target_w as usize)); let mut array = Array4::<f32>::zeros((1, 3, target_h as usize, target_w as usize));
for (x, y, pixel) in base_img.enumerate_pixels() {
let x = x as usize; // 用连续的 stride 步长进行写入,提高 CPU 缓存利用率
let y = y as usize; for y in 0..target_h as usize {
// 核心对标 Python 的 BGR 逻辑: for x in 0..target_w as usize {
// pixel[0] 是 R, pixel[1] 是 G, pixel[2] 是 B let idx = (y * target_w as usize + x) * 3;
// 如果模型需要 BGR // BGR 赋值
// array[[0, 0, y as usize, x as usize]] = pixel[0] as f32; array[[0, 0, y, x]] = slice[idx + 2] as f32; // B
// array[[0, 1, y as usize, x as usize]] = pixel[1] as f32; array[[0, 1, y, x]] = slice[idx + 1] as f32; // G
// array[[0, 2, y as usize, x as usize]] = pixel[2] as f32; array[[0, 2, y, x]] = slice[idx] as f32; // R
array[[0, 0, y, x]] = pixel[2] as f32; // B
array[[0, 1, y, x]] = pixel[1] as f32; // G
array[[0, 2, y, x]] = pixel[0] as f32; // R
} }
}
Ok((array.into(), r)) Ok((array.into(), r))
} }
@@ -161,13 +178,14 @@ impl Det {
} }
/// 5. multiclass_nms /// 5. multiclass_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 格式
scores: &Array2<f32>, // [25200, 80] -> 已经乘以 objectness 的得分 scores: &Array2<f32>, // [25200, 80] -> 已经乘以 objectness 的得分
nms_thr: f32, nms_thr: f32,
score_thr: f32, score_thr: f32,
) -> Vec<Vec<f32>> { ) -> Vec<[f32; 6]> {
let mut candidates = Vec::new(); let mut candidates = Vec::new();
// 1. 筛选高分框 (单次遍历完成 Argmax 和 Threshold 过滤) // 1. 筛选高分框 (单次遍历完成 Argmax 和 Threshold 过滤)
@@ -213,17 +231,17 @@ impl Det {
.map(|k_idx| { .map(|k_idx| {
let (orig_idx, score, cls_id) = candidates[k_idx]; let (orig_idx, score, cls_id) = candidates[k_idx];
let b = boxes.row(orig_idx); let b = boxes.row(orig_idx);
vec![b[0], b[1], b[2], b[3], score, cls_id as f32] [b[0], b[1], b[2], b[3], score, cls_id as f32]
}) })
.collect() .collect()
} }
/// 6. get_bbox (完全解耦 OpenCV) /// 6. get_bbox (完全解耦 OpenCV)
pub fn get_bbox(&self, image_bytes: &[u8]) -> Result<Vec<Vec<i32>>> { pub fn get_bbox(&self, dynamic_img: &DynamicImage) -> Result<Vec<DetectionResult>> {
// 使用 utils crate 解码 // 使用 utils crate 解码
let dynamic_img = image::load_from_memory(image_bytes).context("Failed to decode utils")?; // let dynamic_img = image::load_from_memory(image_bytes).context("Failed to decode utils")?;
let (orig_w, orig_h) = dynamic_img.dimensions(); let (orig_w, orig_h) = dynamic_img.dimensions();
let (input_tensor, ratio) = self.preproc(&dynamic_img, (416, 416))?; let (input_tensor, ratio) = self.preproc(dynamic_img, (416, 416))?;
// tract 推理 // tract 推理
let outputs = self.session.run(tvec!(input_tensor.into()))?; let outputs = self.session.run(tvec!(input_tensor.into()))?;
@@ -236,7 +254,13 @@ impl Det {
let pred = predictions.slice(s![0, .., ..]); let pred = predictions.slice(s![0, .., ..]);
let boxes = pred.slice(s![.., 0..4]); let boxes = pred.slice(s![.., 0..4]);
let scores = &pred.slice(s![.., 4..5]) * &pred.slice(s![.., 5..]); let obj_conf = pred.slice(s![.., 4..5]);
let cls_conf = pred.slice(s![.., 5..]);
let obj_broadcast = obj_conf
.broadcast(cls_conf.dim())
.context("ndarray broadcasting failed for scores calculation")?;
let scores = &obj_broadcast * &cls_conf;
// let scores = &pred.slice(s![.., 4..5]) * &pred.slice(s![.., 5..]);
let mut boxes_xyxy = Array2::<f32>::zeros(boxes.raw_dim()); let mut boxes_xyxy = Array2::<f32>::zeros(boxes.raw_dim());
for i in 0..boxes.nrows() { for i in 0..boxes.nrows() {
@@ -247,17 +271,19 @@ impl Det {
} }
let detections = self.multiclass_nms(&boxes_xyxy, &scores, 0.45, 0.1); let detections = self.multiclass_nms(&boxes_xyxy, &scores, 0.45, 0.1);
let final_results = detections
Ok(detections
.into_iter() .into_iter()
.map(|d| { .map(|d| {
vec![ DetectionResult{
(d[0] as i32).max(0).min(orig_w as i32), x1: (d[0] as i32).max(0).min(orig_w as i32),
(d[1] as i32).max(0).min(orig_h as i32), y1: (d[1] as i32).max(0).min(orig_h as i32),
(d[2] as i32).max(0).min(orig_w as i32), x2: (d[2] as i32).max(0).min(orig_w as i32),
(d[3] as i32).max(0).min(orig_h as i32), y2: (d[3] as i32).max(0).min(orig_h as i32),
] score: d[4],
class_id: d[5] as u32,
}
}) })
.collect()) .collect();
Ok(final_results )
} }
} }

View File

@@ -1,19 +1,108 @@
use crate::charset::CharsetRestrict; use crate::charset::{TokenFilter, ValidationCtx};
use crate::model_metadata::ModelMetadata; use crate::model_metadata::{ModelMetadata, Resize};
use crate::models::base::ModelArgs; use crate::models::base::ModelArgs;
use crate::models::loader::{ModelLoader, ModelSession, ModelType}; use crate::models::loader::{ModelLoader, ModelSession, ModelType};
use crate::utils::color_filter::{ColorFilter, HsvRange, filter_image};
use crate::utils::image_io::png_rgba_white_preprocess; use crate::utils::image_io::png_rgba_white_preprocess;
use crate::utils::image_processor::{convert_to_grayscale, resize_image}; use crate::utils::image_processor::{convert_to_grayscale, resize_image};
use anyhow::Context; use anyhow::Context;
use image::DynamicImage; use anyhow::{Result, anyhow};
use image::{DynamicImage, ImageBuffer, Rgb};
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashSet; use std::collections::HashSet;
use tract_onnx::prelude::tract_ndarray::s; use std::fmt;
use tract_onnx::prelude::tract_ndarray::{ArrayView2, Ix2, s};
use tract_onnx::prelude::{ use tract_onnx::prelude::{
DatumType, Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp, tract_ndarray, tvec, DatumType, Graph, IntoTensor, RunnableModel, Tensor, TypedFact, TypedOp, tract_ndarray, tvec,
}; };
// 引入 cv_ops 模块中的 OpenCV HSV 转换算子
use crate::utils::cv_ops::rgb_to_opencv_hsv;
// 颜色过滤的自定义范围:(低值RGB, 高值RGB) /// 推理最终输出的强类型外壳(完全 Owned无任何生命周期可直接转 JSON
pub type ColorRange = ((u8, u8, u8), (u8, u8, u8)); #[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 { pub struct Ocr {
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>, pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
@@ -21,7 +110,7 @@ pub struct Ocr {
} }
impl ModelSession for Ocr { impl ModelSession for Ocr {
fn get_model_type(&self) -> ModelType { fn get_model_type(&self) -> ModelType {
todo!() todo!("使用thiserror作为错误处理的库,thiserror 专门用于开发库Library");
} }
fn desc(&self) -> String { fn desc(&self) -> String {
"Ocr Model 加载成功".to_string() "Ocr Model 加载成功".to_string()
@@ -29,43 +118,51 @@ impl ModelSession for Ocr {
} }
impl Ocr { impl Ocr {
pub fn new(model_path: String, model_metadata: ModelMetadata) -> Result<Self, anyhow::Error> { pub fn new(model_path: String, model_metadata: ModelMetadata) -> Result<Self, anyhow::Error> {
let session = ModelLoader::load_model(&model_path)?.session; let session = ModelLoader::load_model(&model_path)?.session;
Ok(Self { Ok(Self {
session, session,
model_metadata, model_metadata,
}) })
} }
pub fn predict<'a>(&'a self, image: &'a DynamicImage) -> OcrBuilder<'a> { /// 对应 Python 的 _inference
OcrBuilder::new(self, image) fn inference(&self, tensor: Tensor) -> anyhow::Result<Tensor> {
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出
// let result = self.session.run(tvec!(tensor.into()))?;
let mut result = self
.session
.run(tvec!(tensor.into()))
.context("执行模型推理失败")?;
println!("模型输出原始数据: {:?}", result);
Ok(result.swap_remove(0).into_tensor())
}
pub fn predictor(&'_ self) -> OcrPredictor<'_> {
OcrPredictor::new(self)
} }
} }
pub struct OcrBuilder<'a> { pub struct OcrPredictor<'a> {
ocr: &'a Ocr, ocr: &'a Ocr,
image: &'a DynamicImage,
/// 是否修复PNG格式问题 /// 是否修复PNG格式问题
png_fix: bool, png_fix: bool,
/// 是否返回概率信息 /// 是否返回概率信息
#[allow(dead_code)]
probability: bool, probability: bool,
/// 颜色过滤:保留的颜色列表 /// 颜色过滤:保留的颜色列表
color_filter_colors: Option<Vec<ColorRange>>, color_filter: Result<Option<Vec<HsvRange>>, String>,
/// 颜色过滤自定义RGB范围
color_filter_custom_ranges: Option<Vec<ColorRange>>,
/// 字符集范围 /// 字符集范围
charset_restrict: Option<CharsetRestrict>, charset_restrict: Option<Vec<usize>>,
} }
impl<'a> OcrBuilder<'a> { impl<'a> OcrPredictor<'a> {
// 初始化任务,设置默认参数 // 初始化任务,设置默认参数
pub fn new(ocr: &'a Ocr, image: &'a DynamicImage) -> Self { pub fn new(ocr: &'a Ocr) -> Self {
Self { Self {
ocr, ocr,
image,
png_fix: false, // 默认值 png_fix: false, // 默认值
probability: false, probability: false,
color_filter_colors: None, color_filter: Ok(None),
color_filter_custom_ranges: None,
charset_restrict: None, charset_restrict: None,
} }
} }
@@ -73,92 +170,280 @@ impl<'a> OcrBuilder<'a> {
self.png_fix = value; self.png_fix = value;
self self
} }
pub fn color_filter_colors(mut self, value: Vec<ColorRange>) -> Self { pub fn probability(mut self, value: bool) -> Self {
self.color_filter_colors = Some(value); self.probability = value;
self
}
pub fn color_filter_custom_ranges(mut self, value: Vec<ColorRange>) -> Self {
self.color_filter_custom_ranges = Some(value);
self
}
pub fn charset_restrict(mut self, restrict: CharsetRestrict) -> Self {
self.charset_restrict = Some(restrict);
self self
} }
pub fn run(&self) -> anyhow::Result<String> { pub fn color_filter(mut self, filter: &dyn ColorFilter) -> Self {
let tensor = self.preprocess_image(self.image, self.png_fix)?; // 一句话把活全包了!错误信息无缝传递,完美熔断
match filter.collect_to_vec() {
let raw_tensor = self.inference(tensor)?; Ok(new_ranges) => self.color_filter = Ok(new_ranges),
let raw_indices = self.extract_indices_from_tensor(&raw_tensor)?; Err(err_msg) => self.color_filter = Err(err_msg), // 校验失败Builder 正式中毒
// 步骤 2: 将索引切片 `&[i64]` 传给解码器进行 CTC 去重和字符映射
let final_text = self.ctc_decode_to_string(&raw_indices);
println!("最终识别出的验证码是: {}", final_text);
Ok(final_text)
} }
/// 对应 Python 的 _preprocess_image self
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换 }
fn preprocess_image(&self, img: &DynamicImage, png_fix: bool) -> anyhow::Result<Tensor> {
// A. 修复 PNG 透明背景 (内部逻辑你之前已实现) pub fn charset_restrict(mut self, restrict: &dyn TokenFilter) -> Self {
let _ = if png_fix && img.color().has_alpha() { let charset = &self.ocr.model_metadata.charset;
png_rgba_white_preprocess(img) let tokens = &charset.tokens;
} else { self.charset_restrict = restrict.apply_to_charset(tokens);
img.clone() self
}
}
impl<'a> OcrPredictor<'a> {
pub fn predict(self, image: &DynamicImage) -> anyhow::Result<OcrResult> {
println!("当前颜色过滤器状态: {:?}", self.color_filter);
// =====================================================================
// 管道节点 1: 颜色过滤流水线
// 使用 Cow (Copy-On-Write) 智能指针。
// 如果未开启过滤img_cow 内部只是持有原图的【只读借用】,发生【零内存分配】!
// =====================================================================
let img_cow = match &self.color_filter {
Err(err_msg) => {
return Err(anyhow::anyhow!(
"颜色过滤器初始化失败,全链路短路: {}",
err_msg
));
}
Ok(None) => {
// 核心优化点:直接借用原图,不发生任何克隆
Cow::Borrowed(image)
}
Ok(Some(ranges)) => {
// 只有真正需要过滤时,才在内部提取像素并生成清洗后的 Owned 新图
let filtered_img = filter_image(image, ranges)?;
Cow::Owned(filtered_img)
}
};
let tensor = self.preprocess_image(&img_cow)?;
let raw_tensor = self.ocr.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 h = 64u32; // let raw_indices = self.ocr.extract_indices_from_tensor(&raw_tensor)?;
let w = (img.width() as f32 * (h as f32 / img.height() as f32)) as u32; // // 步骤 2: 将索引切片 `&[i64]` 传给解码器进行 CTC 去重和字符映射
let gray_img = convert_to_grayscale(img); // let final_text = self.ctc_decode_to_string(&raw_indices);
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 构造,避免版本冲突 Ok(ocr_output)
let array = }
tract_ndarray::Array4::from_shape_fn((1, 1, h as usize, w as usize), |(_, _, y, x)| { /// 对应 Python 的 _preprocess_image
let pixel = resized.get_pixel(x as u32, y as u32)[0] as f32; /// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
(pixel / 255.0 - 0.5) / 0.5 fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result<Tensor> {
}); // 1. 获取模型元数据配置
let meta = &self.ocr.model_metadata;
let norm = &meta.normalization; // 获取归一化器
let tensor = Tensor::from(array); // 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(&current_img, target_w, target_h);
// 4. 管道节点 3: 颜色通道转换(单通道灰度 vs 三通道 RGB与 4D 张量填充
let tensor = match meta.channel {
// --- 情况 A: 单通道(灰度图),对应 Python 的 len(shape) == 2 展开 ---
1 => {
let gray_img = convert_to_grayscale(&resized_img);
let array = tract_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)
},
);
Tensor::from(array)
}
// --- 情况 B: 三通道RGB对应 Python 的 transpose(2, 0, 1) 的 CHW 布局 ---
3 => {
let rgb_img = resized_img.to_rgb8();
let array = tract_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)
}
_ => return Err(anyhow::anyhow!("不支持的通道数配置: {}", meta.channel)),
};
Ok(tensor) 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)
} }
/// 对应 Python 的 _inference }
fn inference(&self, tensor: Tensor) -> anyhow::Result<Tensor> { impl<'a> OcrPredictor<'a> {
// tract 的 run 会返回一个 Vec<TValue>,我们通常只需要第一个输出 fn is_valid_indices(&self, idx: usize) -> bool {
// let result = self.session.run(tvec!(tensor.into()))?; if idx >= self.ocr.model_metadata.charset.size() {
let mut result = self return false;
.ocr
.session
.run(tvec!(tensor.into()))
.context("执行模型推理失败")?;
println!("模型输出原始数据: {:?}", result);
Ok(result.remove(0).into_tensor())
} }
/// 核心解析逻辑:将模型输出的各种维度/类型的 Tensor 转为字符索引序列 match &self.charset_restrict {
fn extract_indices_from_tensor(&self, raw_tensor: &Tensor) -> anyhow::Result<Vec<i64>> { Some(v) => v.binary_search(&idx).is_ok(),
None => true,
}
}
/// 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印
/// 这里的 &str 完美借用了自 tokens依然是彻底的零拷贝
pub fn valid_tokens(&self) -> Vec<&str> {
let charset = &self.ocr.model_metadata.charset;
let tokens = &charset.tokens;
match &self.charset_restrict {
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.charset_restrict {
Some(indices) => indices.len(),
None => self.ocr.model_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 = tract_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(); let shape = raw_tensor.shape();
println!("模型输出shape数据: {:?}", shape); println!("模型输出shape数据: {:?}", shape);
let datum_type = raw_tensor.datum_type();
println!("模型输出datum_type数据: {:?}", datum_type);
match raw_tensor.datum_type() {
// 情况 1: huashi666 式模型,直接输出 i64 索引 (通常是模型内部做好了 Argmax)
DatumType::I64 => {
let view = raw_tensor.to_array_view::<i64>()?;
Ok(view.iter().cloned().collect())
}
// 情况 2: sml2h3 原版模型,输出 F32 概率矩阵
DatumType::F32 => {
let view = raw_tensor.to_array_view::<f32>()?; let view = raw_tensor.to_array_view::<f32>()?;
let (steps, classes, data_view) = match shape.len() {
// 1. 极其纯粹的、无拷贝的多维 Shape 压扁清洗
let (steps, classes, data_dyn_view) = match shape.len() {
3 => { 3 => {
if shape[1] == 1 { if shape[1] == 1 {
// 形状: [Steps, 1, Classes] -> 你的原有逻辑 // 形状: [Steps, 1, Classes] -> 你的原有逻辑
@@ -173,102 +458,56 @@ impl<'a> OcrBuilder<'a> {
(shape[1], shape[2], sliced.into_dyn()) (shape[1], shape[2], sliced.into_dyn())
} }
} }
2 => {
// 形状: [Steps, Classes] -> 已经剥离了 Batch 维度 // 形状: [Steps, Classes] -> 已经剥离了 Batch 维度
(shape[0], shape[1], view.into_dyn()) 2 => (shape[0], shape[1], view.into_dyn()),
}
// 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑) // 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑)
// 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑 // 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
1 => (1, shape[0], view.into_dyn()), 1 => (1, shape[0], view.into_dyn()),
_ => return Err(anyhow::anyhow!("不支持的输出维度: {:?}", shape)), _ => return Err(anyhow::anyhow!("不支持的输出维度: {:?}", shape)),
}; };
let array_2d = data_view.to_shape((steps, classes))?; let matrix_cow = data_dyn_view
// .to_shape(Ix2(steps, classes))
// 对每一行执行 Argmax (寻找概率最大的字符索引) .map_err(|e| anyhow::anyhow!("转换为2D静态矩阵失败: {:?}", e))?;
let indices = array_2d
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() .outer_iter()
.map(|row| { .map(|row| {
row.iter() row.iter()
.enumerate() .enumerate()
.max_by(|(_, a), (_, b)| { .max_by(|(_, a), (_, b)| a.total_cmp(b))
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(idx, _)| idx as i64) .map(|(idx, _)| idx as i64)
.unwrap_or(0) .unwrap_or(0)
}) })
.collect(); .collect();
Ok(indices)
} let final_text = self.ctc_decode_to_string(&predicted_indices);
_ => Err(anyhow::anyhow!( Ok(OcrResult::Text(final_text))
"不支持的模型输出数据类型: {:?}",
raw_tensor.datum_type()
)),
} }
} }
/// 获取有效字符索引列表 (用于外部验证或过滤) /// 获取有效字符索引列表 (用于外部验证或过滤)
pub fn get_valid_indices(&self) -> HashSet<usize> {
let (_, valid_indices) = self.valid_indices();
valid_indices
}
fn valid_indices(&self) -> (bool, HashSet<usize>) {
let charset = &self.ocr.model_metadata.charset;
let tokens = &charset.tokens;
/// 根据传入的 CharsetRestrict 枚举策略,动态更新有效索引
// 1. 🧠 零克隆防御战:在局部判断并动态构建专属的白名单判定表
let mut valid_indices = HashSet::new();
let mut has_any_match = false;
if let Some(ref policy) = self.charset_restrict {
// 🧠 性能精算根据限制策略的类型智能分配合适的初始容量1个字节都不浪费
let estimated_capacity = match policy {
CharsetRestrict::Digit => 16,
CharsetRestrict::Lowercase | CharsetRestrict::Uppercase => 32,
CharsetRestrict::CustomList(vec) => vec.len() + 1, // 动态匹配列表大小
_ => 128, // 组合子Or等复杂情况给个 128 黄金保底值
};
// 🚀 精准开辟内存,完美避开 8120 个槽位的巨大空置浪费
valid_indices = HashSet::with_capacity(estimated_capacity);
for (idx, token) in tokens.iter().enumerate() {
let token_str = token.as_ref();
// CTC Blank 空字符串无条件放行,其余交给超高性能的 matches
if token_str.is_empty() {
valid_indices.insert(idx);
} else if policy.matches(token_str) {
valid_indices.insert(idx);
has_any_match = true;
}
}
// 终极防御:如果除了 Blank 之外,没有任何一个字符被匹配到
if !has_any_match {
valid_indices = (0..tokens.len()).collect();
println!("警告:当前限制策略与模型字符集完全没有交集!已自动恢复全量识别。");
}
}
(has_any_match, valid_indices)
}
/// 🌟 【按需延迟打印】:当用户真的需要“知道当前有哪些限制字符”时,一秒反查并打印
/// 这里的 &str 完美借用了自 tokens依然是彻底的零拷贝
pub fn get_valid_tokens(&self) -> Vec<&str> {
let charset = &self.ocr.model_metadata.charset;
let tokens = &charset.tokens;
self.get_valid_indices()
.iter()
.map(|&idx| tokens[idx].as_ref())
.collect()
}
pub fn valid_size(&self) -> usize {
self.get_valid_indices().len()
}
fn ctc_decode_to_string(&self, predicted_indices: &[i64]) -> String { fn ctc_decode_to_string(&self, predicted_indices: &[i64]) -> String {
println!("indices模型输出原始数据: {:?}", predicted_indices); println!("indices模型输出原始数据: {:?}", predicted_indices);
let charset = &self.ocr.model_metadata.charset; let charset = &self.ocr.model_metadata.charset;
let tokens = &charset.tokens; let tokens = &charset.tokens;
// let valid_indices = &charset.valid_indices; // let valid_indices = &charset.valid_indices;
let (has_any_match, valid_indices) = self.valid_indices();
// 对应 _ctc_decode_indices 的逻辑:去重、去 blank (0) // 对应 _ctc_decode_indices 的逻辑:去重、去 blank (0)
let mut res = String::new(); let mut res = String::new();
let mut prev_idx: i64 = -1; let mut prev_idx: i64 = -1;
@@ -291,11 +530,11 @@ impl<'a> OcrBuilder<'a> {
Err(_) => continue, Err(_) => continue,
}; };
// 4. 终极性能:既然你的有效索引库必然有全量或部分数据,这里直接进行 O(1) 包含校验 // 史诗级加速点:如果是 None说明没限制根本不进入分支直接放行
// 注意:去掉原本错误的 `!` // 只有当有具体限制Some才去跑 4-5 次 CPU 寄存器级别的二分查找
if has_any_match { if let Some(ref indices) = self.charset_restrict {
if !valid_indices.contains(&u_idx) { if indices.binary_search(&u_idx).is_err() {
continue; // 不在有效字符集内,安全跳过 continue;
} }
} }

View File

@@ -27,7 +27,7 @@ impl Slide {
Self Self
} }
/// 对应 Python: slide_match /// 对应 Python: slide_match 滑块匹配接口
pub fn slide_match( pub fn slide_match(
&self, &self,
target_image: &DynamicImage, target_image: &DynamicImage,
@@ -38,9 +38,8 @@ impl Slide {
let background_array = image_to_ndarray(background_image); let background_array = image_to_ndarray(background_image);
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(|e| anyhow!("滑块匹配失败: {}", e))
} }
/// 对应 Python: slide_comparison /// 对应 Python: slide_comparison 差异比较接口
/// 用于比较带坑位的图片与原始背景图,定位差异点 /// 用于比较带坑位的图片与原始背景图,定位差异点
pub fn slide_comparison( pub fn slide_comparison(
&self, &self,
@@ -53,7 +52,6 @@ impl Slide {
// 2. 执行比较逻辑 (对应 _perform_slide_comparison) // 2. 执行比较逻辑 (对应 _perform_slide_comparison)
self.perform_slide_comparison(target_array.view(), background_array.view()) self.perform_slide_comparison(target_array.view(), background_array.view())
.map_err(|e| anyhow!("滑块比较执行失败: {}", e))
} }
/// 对应 Python: _perform_slide_comparison /// 对应 Python: _perform_slide_comparison
pub fn perform_slide_comparison( pub fn perform_slide_comparison(
@@ -61,7 +59,7 @@ impl Slide {
target: ArrayView3<u8>, target: ArrayView3<u8>,
background: ArrayView3<u8>, background: ArrayView3<u8>,
) -> Result<SlideResult> { ) -> Result<SlideResult> {
let (h, w, _) = target.dim(); // let (h, w, _) = target.dim();
// 1. 计算图像差异并灰度化 (对应 cv2.absdiff + cv2.cvtColor) // 1. 计算图像差异并灰度化 (对应 cv2.absdiff + cv2.cvtColor)
// 使用 OpenCV 标准权重公式0.299R + 0.587G + 0.114B // 使用 OpenCV 标准权重公式0.299R + 0.587G + 0.114B
@@ -77,6 +75,26 @@ impl Slide {
// } // }
// } // }
// 1. 计算差异数组 (复用 cv2::absdiff) // 1. 计算差异数组 (复用 cv2::absdiff)
let (th, tw, tc) = target.dim();
let (bh, bw, bc) = background.dim();
// 1. 比较模式下的严格尺寸校验
if th != bh || tw != bw || tc != bc {
return Err(anyhow!(
"比较模式要求两张图分辨率与通道数完全一致Target: [{}x{}x{}], Background: [{}x{}x{}]",
tw,
th,
tc,
bw,
bh,
bc
));
}
if th == 0 || tw == 0 {
return Err(anyhow!("输入图像尺寸不能为0"));
}
let diff_array = abs_diff(&target, &background); let diff_array = abs_diff(&target, &background);
// 2. 转换为灰度数组 (复用你的 cv2.cvtColor) // 2. 转换为灰度数组 (复用你的 cv2.cvtColor)
@@ -130,6 +148,30 @@ impl Slide {
background: ArrayView3<u8>, background: ArrayView3<u8>,
simple_target: bool, // 增加这个参数 simple_target: bool, // 增加这个参数
) -> Result<SlideResult> { ) -> Result<SlideResult> {
let (th, tw, tc) = target.dim();
let (bh, bw, bc) = background.dim();
// 1. 严格的鲁棒性校验(防止底层的 imageproc 算子崩溃)
if th == 0 || tw == 0 || bh == 0 || bw == 0 {
return Err(anyhow!("输入图像的宽度或高度不能为0"));
}
if th > bh || tw > bw {
return Err(anyhow!(
"尺寸不匹配:滑块模板(target)尺寸 [{}x{}] 不能大于背景图(background) [{}x{}]",
tw,
th,
bw,
bh
));
}
if tc != bc {
return Err(anyhow!(
"目标图与背景图的通道数不一致 (target: {}, bg: {})",
tc,
bc
));
}
// 1. 统一灰度化 // 1. 统一灰度化
let target_gray = rgb_to_gray(target); let target_gray = rgb_to_gray(target);
let background_gray = rgb_to_gray(background); let background_gray = rgb_to_gray(background);

249
src/utils/color_filter.rs Normal file
View File

@@ -0,0 +1,249 @@
use std::str::FromStr;
use anyhow::anyhow;
use image::{DynamicImage, ImageBuffer, Rgb};
use crate::utils::cv_ops::rgb_to_opencv_hsv;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct HsvRange {
pub lower: (u8, u8, u8), // (H, S, V)
pub upper: (u8, u8, u8), // (H, S, V)
}
/// 核心区间判定辅助函数
#[inline(always)]
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 filter_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))
}
impl HsvRange {
pub const fn new(lower: (u8, u8, u8), upper: (u8, u8, u8)) -> Self {
Self { lower, upper }
}
}
impl HsvRange {
/// 验证当前 HSV 范围是否合法
/// 对应 Python 逻辑H 在 0-180S/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 ),+ ]
}
};
}

View File

@@ -1,6 +1,6 @@
use std::cmp::{max, min};
use image::{ImageBuffer, Luma}; use image::{ImageBuffer, Luma};
use tract_onnx::prelude::tract_ndarray::{azip, Array2, Array3, ArrayView2, ArrayView3}; use std::cmp::{max, min};
use tract_onnx::prelude::tract_ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip};
/// 1. 计算两个数组的绝对差值 (对应 cv2.absdiff) /// 1. 计算两个数组的绝对差值 (对应 cv2.absdiff)
pub fn abs_diff(a: &ArrayView3<u8>, b: &ArrayView3<u8>) -> Array3<u8> { pub fn abs_diff(a: &ArrayView3<u8>, b: &ArrayView3<u8>) -> Array3<u8> {
@@ -13,7 +13,6 @@ pub fn abs_diff(a: &ArrayView3<u8>, b: &ArrayView3<u8>) -> Array3<u8> {
diff diff
} }
/// RGB 到灰度转换 /// RGB 到灰度转换
pub fn rgb_to_gray(rgb: ArrayView3<u8>) -> Array2<u8> { pub fn rgb_to_gray(rgb: ArrayView3<u8>) -> Array2<u8> {
let (h, w, _) = rgb.dim(); let (h, w, _) = rgb.dim();
@@ -67,9 +66,16 @@ pub fn find_contours_and_max(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>) -> Opt
max_label = label; max_label = label;
} }
} }
if max_label == 0 { None } else { Some(max_label) } if max_label == 0 {
None
} else {
Some(max_label)
}
} }
pub fn bounding_rect(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>,max_label: u32) -> (u32, u32, u32, u32) { pub fn bounding_rect(
labelled: &ImageBuffer<Luma<u32>, Vec<u32>>,
max_label: u32,
) -> (u32, u32, u32, u32) {
// 5. 计算最大区域的边界框 (对应 cv2.boundingRect) // 5. 计算最大区域的边界框 (对应 cv2.boundingRect)
let mut min_x = labelled.width(); let mut min_x = labelled.width();
let mut max_x = 0; let mut max_x = 0;
@@ -85,14 +91,13 @@ pub fn bounding_rect(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>,max_label: u32)
} }
} }
let w = max_x - min_x; let w = max_x - min_x;
let h = max_y - min_y; let h = max_y - min_y;
(min_x, min_y, w, h) (min_x, min_y, w, h)
} }
pub fn calculate_center(max_loc: (u32, u32), tw: usize, th: usize) -> (i32, i32) { pub fn calculate_center(top_left: (u32, u32), width: usize, height: usize) -> (i32, i32) {
let center_x = max_loc.0 as i32 + (tw as i32 / 2); let center_x = top_left.0 as i32 + (width as i32 / 2);
let center_y = max_loc.1 as i32 + (th as i32 / 2); let center_y = top_left.1 as i32 + (height as i32 / 2);
(center_x, center_y) (center_x, center_y)
} }
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>> {
@@ -105,3 +110,52 @@ pub fn ndarray_to_luma8(array: ArrayView2<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>>
} }
buffer buffer
} }
// =====================================================================
// 5. 核心高性能图像转换算法 (纯 Rust 编写)
// =====================================================================
#[inline(always)]
pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
// 1. 规避高昂的除法,直接转为 f32 进行比对
let r_f = r as f32;
let g_f = g as f32;
let b_f = b as f32;
let max = r_f.max(g_f).max(b_f);
let min = r_f.min(g_f).min(b_f);
let delta = max - min;
// 2. 计算 H (色调) - 移除负数取余陷阱,改用平铺分支
let mut h = if delta == 0.0 {
0.0
} else if max == r_f {
let mut diff = (g_f - b_f) / delta;
if diff < 0.0 {
diff += 6.0; // 规避 Rust f32 % 负数的行为
}
60.0 * diff
} else if max == g_f {
60.0 * (((b_f - r_f) / delta) + 2.0)
} else {
60.0 * (((r_f - g_f) / delta) + 4.0)
};
// OpenCV 的 H 量化H / 2
// 注意OpenCV 底层使用截断还是四舍五入与特定版本有关,
// 标准的 cvtColor 内部实现通常是: h * (180.0 / 360.0) -> h * 0.5
// 这里使用强转(截断)若单测对齐发现差1可改为 (h * 0.5 + 0.5) 或 round()
let h_opencv = (h * 0.5) as u8;
// 3. 计算 S (饱和度)
// OpenCV 公式: S = max == 0 ? 0 : 255 * delta / max
let s_opencv = if max == 0.0 {
0
} else {
((255.0 * delta) / max) as u8
};
// 4. 计算 V (明度)
let v_opencv = max as u8;
(h_opencv, s_opencv, v_opencv)
}

View File

@@ -11,17 +11,27 @@ pub fn convert_to_grayscale(image: &DynamicImage) -> GrayImage {
/// 对应 Python 的 resize_image /// 对应 Python 的 resize_image
/// 调整图像尺寸。当前版本仅实现 keep_aspect_ratio=false /// 调整图像尺寸。当前版本仅实现 keep_aspect_ratio=false
pub fn resize_image( pub fn resize_image(
image: &GrayImage, image: &DynamicImage,
target_width: u32, target_width: u32,
target_height: u32, target_height: u32,
// resample 参数我们直接使用 FilterTypeLanczos3 是最接近 Python LANCZOS 的 // resample 参数我们直接使用 FilterTypeLanczos3 是最接近 Python LANCZOS 的
) -> GrayImage { ) -> DynamicImage {
// 使用 resize 算法进行精确缩放
image::imageops::resize(
image,
target_width,
target_height,
FilterType::Lanczos3
)
}
// image::imageops::resize 的最高层封装
// FilterType::Lanczos3 与 Python Pillow 的 Image.LANCZOS 算法完全对齐,缩放质量最高
image.resize_exact(target_width, target_height, FilterType::Lanczos3)
}
// pub fn resize_image(
// image: &GrayImage,
// target_width: u32,
// target_height: u32,
// // resample 参数我们直接使用 FilterTypeLanczos3 是最接近 Python LANCZOS 的
// ) -> GrayImage {
// // 使用 resize 算法进行精确缩放
// image::imageops::resize(
// image,
// target_width,
// target_height,
// FilterType::Lanczos3
// )
// }

View File

@@ -1,3 +1,4 @@
pub mod image_io; pub mod image_io;
pub mod image_processor; pub mod image_processor;
pub mod cv_ops; pub mod cv_ops;
pub mod color_filter;

View File

@@ -1,8 +1,10 @@
use ddddocr_rs::models::slide::Slide; use ddddocr_rs::models::slide::Slide;
use ddddocr_rs::{DdddOcr, DdddOcrBuilder}; // 假设你的包名是这个 use ddddocr_rs::{DdddOcr, DdddOcrBuilder}; // 假设你的包名是这个
use image::Rgb; use image::{DynamicImage, Rgb};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use ddddocr_rs::models::det::DetectionResult;
fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> { fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
// 1. 先将泛型转为具体的 &Path 引用 // 1. 先将泛型转为具体的 &Path 引用
let path_ref = path.as_ref(); let path_ref = path.as_ref();
@@ -15,27 +17,26 @@ fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
} }
/// 将检测结果绘制在图像上并保存 /// 将检测结果绘制在图像上并保存
fn save_debug_image( fn save_debug_image(
image_bytes: &[u8], dynamic_img: &DynamicImage, // 【优化点 1】直接传入解码好的引用拒绝重复解码
bboxes: &Vec<Vec<i32>>, bboxes: &[DetectionResult], // 【修改点 1】类型改为自定义结构体切片
output_path: &str, output_path: &str,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let dynamic_img = image::load_from_memory(image_bytes)?; // 删除了原本的 let dynamic_img = image::load_from_memory(image_bytes)?;
let mut img = dynamic_img.to_rgb8(); let mut img = dynamic_img.to_rgb8();
let (width, height) = img.dimensions(); let (width, height) = img.dimensions();
let red = Rgb([255u8, 0, 0]); let red = Rgb([255u8, 0, 0]);
for bbox in bboxes { for bbox in bboxes {
// 基础边界检查 // 【修改点 2】将原来的索引 bbox[0].. 改为结构体字段访问 .x1, .y1 ..
let x1 = bbox[0].max(0).min(width as i32 - 1) as u32; let x1 = bbox.x1.max(0).min(width as i32 - 1) as u32;
let y1 = bbox[1].max(0).min(height as i32 - 1) as u32; let y1 = bbox.y1.max(0).min(height as i32 - 1) as u32;
let x2 = bbox[2].max(0).min(width as i32 - 1) as u32; let x2 = bbox.x2.max(0).min(width as i32 - 1) as u32;
let y2 = bbox[3].max(0).min(height as i32 - 1) as u32; let y2 = bbox.y2.max(0).min(height as i32 - 1) as u32;
// 绘制横向线条 // 绘制横向线条
for x in x1..=x2 { for x in x1..=x2 {
img.put_pixel(x, y1, red); img.put_pixel(x, y1, red);
img.put_pixel(x, y2, red); img.put_pixel(x, y2, red);
// 如果要加粗,多画一行
if y1 + 1 < height { if y1 + 1 < height {
img.put_pixel(x, y1 + 1, red); img.put_pixel(x, y1 + 1, red);
} }
@@ -47,7 +48,6 @@ fn save_debug_image(
for y in y1..=y2 { for y in y1..=y2 {
img.put_pixel(x1, y, red); img.put_pixel(x1, y, red);
img.put_pixel(x2, y, red); img.put_pixel(x2, y, red);
// 如果要加粗,多画一列
if x1 + 1 < width { if x1 + 1 < width {
img.put_pixel(x1 + 1, y, red); img.put_pixel(x1 + 1, y, red);
} }
@@ -66,7 +66,7 @@ fn test_full_classification() {
let ocr = DdddOcrBuilder::new().build().expect("模型加载失败"); let ocr = DdddOcrBuilder::new().build().expect("模型加载失败");
// 2. 加载测试图片 // 2. 加载测试图片
let img = image::open("samples/code3.png").expect("测试图片不存在"); let img = image::open("samples/code2.png").expect("测试图片不存在");
// 3. 执行识别 // 3. 执行识别
let result = ocr.classification(&img).expect("识别过程出错"); let result = ocr.classification(&img).expect("识别过程出错");
@@ -82,17 +82,27 @@ fn test_det_load() -> anyhow::Result<()> {
fs::read(image_path).map_err(|e| anyhow::anyhow!("无法读取图片 {}: {}", image_path, e))?; fs::read(image_path).map_err(|e| anyhow::anyhow!("无法读取图片 {}: {}", image_path, e))?;
println!("图片读取成功,字节大小: {}", image_bytes.len()); println!("图片读取成功,字节大小: {}", image_bytes.len());
let bboxes = det.detection(&image_bytes)?;
// 【修改点 1】将字节流解码为统一的 DynamicImage
let img = image::load_from_memory(&image_bytes)
.map_err(|e| anyhow::anyhow!("图片解码失败: {}", e))?;
// 【修改点 2】传入统一的 &DynamicImage 引用
let bboxes = det.detection(&img)?;
println!(":?{}", det); println!(":?{}", det);
println!("检测到的目标数量: {}", bboxes.len()); println!("检测到的目标数量: {}", bboxes.len());
if bboxes.is_empty() { if bboxes.is_empty() {
println!("未检测到任何目标。"); println!("未检测到任何目标。");
} else { } else {
save_debug_image(&image_bytes, &bboxes, "samples/result.jpg")?; // 如果 save_debug_image 报错,记得去把它的入参类型和内部访问也改为 DetectionResult
save_debug_image(&img, &bboxes, "samples/result.jpg")?;
for (i, bbox) in bboxes.iter().enumerate() { for (i, bbox) in bboxes.iter().enumerate() {
// 【修改点 3】将原来的 bbox[0].. 索引访问改为结构体字段访问
println!( println!(
"目标 [{}]: x1={}, y1={}, x2={}, y2={}", "目标 [{}]: x1={}, y1={}, x2={}, y2={}, 分数={:.4}, 类别ID={}",
i, bbox[0], bbox[1], bbox[2], bbox[3] i, bbox.x1, bbox.y1, bbox.x2, bbox.y2, bbox.score, bbox.class_id
); );
} }
} }