refactor(ocr): 优化并精简 color_filter 架构设计

- 重构 `OcrBuilder`,将图像矩阵过滤与像素比对等执行层逻辑彻底剥离解耦。
 - 优化 `OcrBuilder` 的 `color_filter` 链式调用,将其改造为无心智负担的单次覆盖(Overwrite)逻辑。
 - 扩展 `ColorFilter` 特征,新增 `collect_to_vec` 方法,实现底层规则的高内聚收集、精准内存开辟与原地去重排序。
This commit is contained in:
2026-06-29 17:19:01 +08:00
parent 2f86694c54
commit 48c2cbedb0
3 changed files with 60 additions and 97 deletions

View File

@@ -533,6 +533,49 @@ pub trait TokenFilter {
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)]

View File

@@ -1,14 +1,14 @@
use std::borrow::Cow;
use crate::charset::{TokenFilter, ValidationCtx};
use crate::model_metadata::ModelMetadata;
use crate::models::base::ModelArgs;
use crate::models::loader::{ModelLoader, ModelSession, ModelType};
use crate::utils::color_filter::{filter_image, ColorFilter, HsvRange};
use crate::utils::color_filter::{ColorFilter, HsvRange, filter_image};
use crate::utils::image_io::png_rgba_white_preprocess;
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
use anyhow::Context;
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use image::{DynamicImage, ImageBuffer, Rgb};
use std::borrow::Cow;
use std::collections::HashSet;
use tract_onnx::prelude::tract_ndarray::s;
use tract_onnx::prelude::{
@@ -158,48 +158,13 @@ impl<'a> OcrBuilder<'a> {
// 反复调用color_filter怎么处理
pub fn color_filter(mut self, filter: &dyn ColorFilter) -> Self {
// self.color_filter = Some(value);
// 利用组合子预估能力,获取精准分配槽位数
let total_capacity = filter.estimated_count();
if total_capacity == 0 {
return self;
// 一句话把活全包了!错误信息无缝传递,完美熔断
match filter.collect_to_vec() {
Ok(new_ranges) => self.color_filter = Ok(new_ranges),
Err(err_msg) => self.color_filter = Err(err_msg), // 校验失败Builder 正式中毒
}
match self.color_filter {
Ok(mut ranges) => {
// 2. 触发特征多态自检快捷预设秒过Custom 变体严格政审
if let Err(err_msg) = filter.validate_self() {
// 校验失败Builder 正式中毒,熔断器闭合
self.color_filter = Err(err_msg);
return self;
}
match &mut ranges {
None => {
// 情况 A这是用户第一次配置直接一击必中分配精准内存
let mut v = Vec::with_capacity(total_capacity);
// 尝试倒入如果出错Builder 正式中毒
filter.append_ranges(&mut v);
ranges = Some(v);
}
Some(existing_vec) => {
// 情况 B用户之前配置过现在追加配置。
// 绝不让它动态乱涨!我们利用 reserve 方法,【仅此一次】精准追加所需的扩容空间!
existing_vec.reserve(total_capacity);
// 尝试追加倒入
filter.append_ranges(existing_vec);
}
}
if let Some(v) = &mut ranges {
v.sort_unstable();
v.dedup();
}
// 5. 更新状态
self.color_filter = Ok(ranges);
}
Err(_) => return self,
};
self
}
@@ -211,42 +176,7 @@ impl<'a> OcrBuilder<'a> {
let charset = &self.ocr.model_metadata.charset;
let tokens = &charset.tokens;
// let mut temp_indices = Vec::new();
let mut has_any_match = false;
let estimated_capacity = restrict.estimated_capacity();
// 精准开辟内存,完美避开 8210 个槽位的巨大空置浪费
let mut temp_indices = Vec::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() || idx == 0 {
temp_indices.push(idx);
};
// 组装无拷贝上下文
let ctx = ValidationCtx {
text: token_str,
token_id: idx,
};
if restrict.matches(&ctx) {
temp_indices.push(idx);
has_any_match = true;
}
}
// self.charset_restrict = Some(restrict);
// 终极防御:如果除了 Blank 外什么都没匹配上,退化恢复为 None全量识别
if !has_any_match {
println!("警告:当前限制策略与模型字符集完全没有交集!已自动恢复全量识别。");
self.charset_restrict = None;
} else {
// 这一步非常重要二分查找binary_search强依赖数组【有序】。
// TopN 天然有序,但如果是用户自定义的 CustomList 或者复杂的 Or 组合,
// 遍历出来的索引天然有序,但为了绝对的安全,我们在这里顺手排个序
temp_indices.sort_unstable();
self.charset_restrict = Some(temp_indices);
}
self.charset_restrict = restrict.apply_to_charset(tokens);
self
}
}
@@ -260,7 +190,10 @@ impl<'a> OcrBuilder<'a> {
// =====================================================================
let img_cow = match &self.color_filter {
Err(err_msg) => {
return Err(anyhow::anyhow!("颜色过滤器初始化失败,全链路短路: {}", err_msg));
return Err(anyhow::anyhow!(
"颜色过滤器初始化失败,全链路短路: {}",
err_msg
));
}
Ok(None) => {
// 核心优化点:直接借用原图,不发生任何克隆
@@ -313,10 +246,6 @@ impl<'a> OcrBuilder<'a> {
Ok(tensor)
}
}
impl<'a> OcrBuilder<'a> {
pub fn get_valid_indices(&self) -> HashSet<usize> {

View File

@@ -165,26 +165,17 @@ pub trait ColorFilter {
}
/// 【新扩展的架构方法】将自身安全的合并到已有的普通容器中,并完成去重和排序
/// 完美的责任分离Builder 不再需要关心怎么分配内存、怎么排序去重
fn merge_to_vec(&self, mut existing: Option<Vec<HsvRange>>) -> Result<Option<Vec<HsvRange>>, String> {
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(existing);
return Ok(None);
}
let mut v = match existing {
None => {
// 情况 A第一次配置精准分配
Vec::with_capacity(total_capacity)
}
Some(mut existing_vec) => {
// 情况 B追加配置精准扩容
existing_vec.reserve(total_capacity);
existing_vec
}
};
// 2. 永远一击必中分配精准内存,不需要再考虑追加和扩容!
let mut v = Vec::with_capacity(total_capacity.max(16));
// 2. 倒入数据
self.append_ranges(&mut v);