refactor(load,tract):将 ModelMetadata JSON 加载逻辑解耦至 ddddocr-tract, 优化 Error 枚举结构与错误透传
- 在 load 模块中精简 Error 与 Result 别名定义 - 增加 ParseError 子类型区分路径与字节流加载失败 - 支持通过 #[from] 自动转换 Tract 引擎底层错误 - 移出 core 中的 serde 依赖,保持核心库纯洁 - 在 tract 中实现 TractModelMetadata 扩展 trait 加载解析配置
This commit is contained in:
@@ -5,7 +5,6 @@ edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
image = "0.25.10"
|
||||
base64 = "0.22.1"
|
||||
imageproc = { version = "0.26.2", default-features = true }
|
||||
|
||||
@@ -16,7 +16,7 @@ impl DetBuilder {
|
||||
self.device_id = device_id;
|
||||
self
|
||||
}
|
||||
fn build(self, session: &dyn DetEngine) -> Detector<'_> {
|
||||
fn build<E: DetEngine>(self, session: &E) -> Detector<'_> {
|
||||
Detector {
|
||||
session,
|
||||
use_gpu: self.use_gpu,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::error::{Result, TensorError};
|
||||
use image::{imageops::FilterType, DynamicImage, GenericImageView};
|
||||
use ndarray::{prelude::*, s, Array2, Array3, Array4, Axis};
|
||||
use image::{DynamicImage, GenericImageView, imageops::FilterType};
|
||||
use ndarray::{Array2, Array3, Array4, Axis, prelude::*, s};
|
||||
use std::fmt;
|
||||
// use tract_onnx::prelude::{Tensor};
|
||||
|
||||
|
||||
// use ddddocr_tract::det::session::DetSession;
|
||||
use crate::{DetEngine, DetOutput};
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -28,7 +27,6 @@ impl fmt::Display for DetectionResult {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct Detector<'a> {
|
||||
pub(crate) session: &'a dyn DetEngine,
|
||||
#[allow(dead_code)]
|
||||
@@ -48,7 +46,7 @@ impl<'a> Detector<'a> {
|
||||
|
||||
pub fn predict(&self, image: &DynamicImage) -> Result<Vec<DetectionResult>> {
|
||||
// Rust 中通常在调用层处理文件/PIL转换,这里直接进入核心逻辑
|
||||
self.get_bbox(image)
|
||||
Ok(self.get_bbox(image)?)
|
||||
}
|
||||
/// 2. preproc: 纯 Rust 实现 (替代 OpenCV)
|
||||
fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> (Array4<f32>, f32) {
|
||||
@@ -247,7 +245,10 @@ impl<'a> Detector<'a> {
|
||||
.collect()
|
||||
}
|
||||
/// 6. get_bbox (完全解耦 OpenCV)
|
||||
pub fn get_bbox(&self, dynamic_img: &DynamicImage) -> Result<Vec<DetectionResult>> {
|
||||
pub fn get_bbox(
|
||||
&self,
|
||||
dynamic_img: &DynamicImage,
|
||||
) -> Result<Vec<DetectionResult>, TensorError> {
|
||||
// 使用 utils crate 解码
|
||||
// let dynamic_img = image::load_from_memory(image_bytes).context("Failed to decode utils")?;
|
||||
let (orig_w, orig_h) = dynamic_img.dimensions();
|
||||
@@ -267,14 +268,14 @@ impl<'a> Detector<'a> {
|
||||
let boxes = pred.slice(s![.., 0..4]);
|
||||
let obj_conf = pred.slice(s![.., 4..5]);
|
||||
let cls_conf = pred.slice(s![.., 5..]);
|
||||
let obj_broadcast = obj_conf
|
||||
.broadcast(cls_conf.dim()).ok_or_else(|| {
|
||||
TensorError::DimensionMismatch {
|
||||
expected: format!("可广播至 cls_conf 形状 {:?}", cls_conf.shape()),
|
||||
actual: obj_conf.shape().to_vec(),
|
||||
}
|
||||
})?;
|
||||
// .context("ndarray broadcasting failed for scores calculation")?;
|
||||
let obj_broadcast =
|
||||
obj_conf
|
||||
.broadcast(cls_conf.dim())
|
||||
.ok_or_else(|| TensorError::DimensionMismatch {
|
||||
expected: format!("可广播至 cls_conf 形状 {:?}", cls_conf.shape()),
|
||||
actual: obj_conf.shape().to_vec(),
|
||||
})?;
|
||||
// .context("ndarray broadcasting failed for scores calculation")?;
|
||||
let scores = &obj_broadcast * &cls_conf;
|
||||
// let scores = &pred.slice(s![.., 4..5]) * &pred.slice(s![.., 5..]);
|
||||
|
||||
|
||||
@@ -99,11 +99,10 @@ pub enum DdddError {
|
||||
/// 框架内部不可恢复的逻辑断言错误(例如解析节点 Fact 失败)
|
||||
#[error("内部严重逻辑错误: {0}")]
|
||||
Internal(String),
|
||||
|
||||
/// 【流派核心】接替 anyhow::Error 的用户自定义扩展错误
|
||||
/// 承载任何第三方扩展、解密、特定预处理插件在执行时产生的自定义错误
|
||||
#[error("用户自定义扩展错误: {0}")]
|
||||
Other(#[source]Box<dyn std::error::Error + Send + Sync>),
|
||||
Other(#[source] Box<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
@@ -142,7 +141,7 @@ pub enum ImagePreprocessError {
|
||||
// ================= 新增:针对 HSV 和 Preset 的强类型错误 =================
|
||||
/// HSV 颜色区间非法 (例如 H > 180 或 lower > upper)
|
||||
#[error("HSV 颜色区间参数非法: {0}")]
|
||||
InvalidHsvRange (String ),
|
||||
InvalidHsvRange(String),
|
||||
|
||||
/// 不支持或未知的颜色预设名称
|
||||
#[error("不支持的颜色预设名称: {0}")]
|
||||
@@ -151,6 +150,17 @@ pub enum ImagePreprocessError {
|
||||
/// 颜色过滤器/预处理规则配置非法导致失败
|
||||
#[error("颜色过滤器配置无效或初始化失败: {0}")]
|
||||
FilterConfigInvalid(String),
|
||||
|
||||
#[error("图像维度不匹配!{0}")]
|
||||
MismatchDimensions (String),
|
||||
|
||||
#[error("滑块模板尺寸 [{target_w}x{target_h}] 大于背景图 [{bg_w}x{bg_h}]")]
|
||||
TargetExceedsBackground {
|
||||
target_w: usize,
|
||||
target_h: usize,
|
||||
bg_w: usize,
|
||||
bg_h: usize,
|
||||
},
|
||||
// #[error("Base64 解码失败: {0}")]
|
||||
// Base64(#[from] base64::DecodeError),
|
||||
//
|
||||
|
||||
@@ -4,12 +4,13 @@ pub mod ocr;
|
||||
mod slide;
|
||||
pub mod utils;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::error::{Result, TensorError};
|
||||
|
||||
pub use crate::slide::{SlideResult, Slider};
|
||||
pub use crate::det::{DetBuilder, DetectionResult, Detector};
|
||||
pub use crate::ocr::{Ocr, OcrBuilder, OcrResult};
|
||||
pub use ocr::metadata::ModelMetadata;
|
||||
pub use crate::ocr::{ModelMetadata,Normalization};
|
||||
pub use ocr::Charset;
|
||||
// DetSession
|
||||
|
||||
pub enum OcrOutput {
|
||||
@@ -23,11 +24,10 @@ pub enum DetOutput {
|
||||
|
||||
/// 核心层定义的统一推理引擎接口。
|
||||
/// 未来的 ddddocr-tract 和 ddddocr-ort 都必须实现这个 Trait
|
||||
|
||||
pub trait InferenceEngine {
|
||||
/// 关联类型:具体的 Session 需要声明自己到底产出什么枚举
|
||||
type Output;
|
||||
fn inference(&self, input_array: ndarray::Array4<f32>) -> Result<Self::Output>;
|
||||
fn inference(&self, input_array: ndarray::Array4<f32>) -> Result<Self::Output,TensorError>;
|
||||
}
|
||||
|
||||
pub trait OcrEngine: InferenceEngine<Output = OcrOutput> {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
mod builder;
|
||||
mod charset;
|
||||
mod color_filter;
|
||||
mod executor;
|
||||
pub mod metadata;
|
||||
pub mod color_filter;
|
||||
mod metadata;
|
||||
mod token_filter;
|
||||
|
||||
pub use builder::OcrBuilder;
|
||||
pub use charset::Charset;
|
||||
pub use executor::{Ocr, OcrResult};
|
||||
pub use metadata::{ModelMetadata, Normalization, Resize};
|
||||
pub use token_filter::TokenFilter;
|
||||
// pub use ddddocr_tract::session::OcrSession;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::ocr::executor::Ocr;
|
||||
// use ddddocr_tract::session::OcrSession;
|
||||
use crate::OcrEngine;
|
||||
use crate::ocr::color_filter::ColorFilter;
|
||||
use crate::ocr::token_filter::TokenFilter;
|
||||
use crate::OcrEngine;
|
||||
|
||||
pub struct OcrBuilder {
|
||||
/// 是否修复PNG格式问题
|
||||
@@ -49,7 +49,7 @@ impl OcrBuilder {
|
||||
self.charset_restrict = Some(Box::new(restrict));
|
||||
self
|
||||
}
|
||||
pub fn build(self, session: &dyn OcrEngine) -> Ocr<'_> {
|
||||
pub fn build<E: OcrEngine>(self, session: &E) -> Ocr<'_> {
|
||||
// 1. 原地解析颜色过滤器
|
||||
let final_color_ranges = match &self.color_filter {
|
||||
Some(filter) => filter.collect_to_vec(),
|
||||
|
||||
66
ddddocr-core/src/ocr/charset.rs
Normal file
66
ddddocr-core/src/ocr/charset.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
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,78 +1,11 @@
|
||||
use crate::error::{ 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"
|
||||
use crate::ocr::Charset;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Normalization {
|
||||
/// 映射到 [0.0, 1.0] -> pixel / 255.0
|
||||
ZeroToOne,
|
||||
@@ -102,23 +35,6 @@ pub enum Resize {
|
||||
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 {
|
||||
/// 字符集管理器
|
||||
@@ -134,6 +50,21 @@ pub struct ModelMetadata {
|
||||
}
|
||||
|
||||
impl ModelMetadata {
|
||||
pub fn new(
|
||||
charset: Charset,
|
||||
word: bool,
|
||||
resize: Resize,
|
||||
channel: u8,
|
||||
normalization: Normalization,
|
||||
) -> Self {
|
||||
Self {
|
||||
charset,
|
||||
word,
|
||||
resize,
|
||||
channel,
|
||||
normalization,
|
||||
}
|
||||
}
|
||||
// --- 优雅的工厂模式构造器 ---
|
||||
/// 通用的静态切片转换构造器
|
||||
pub fn from_static_slice(
|
||||
@@ -152,49 +83,4 @@ impl ModelMetadata {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::utils::image_convert::{image_to_ndarray, ColorMode};
|
||||
use crate::error::{ImagePreprocessError, Result};
|
||||
use crate::utils::image_convert::{ColorMode, image_to_ndarray};
|
||||
use crate::utils::image_processor;
|
||||
use crate::utils::image_processor::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray};
|
||||
use anyhow::{Result, anyhow};
|
||||
use image::DynamicImage;
|
||||
use image::Luma;
|
||||
use imageproc::contrast::{ThresholdType, threshold};
|
||||
@@ -32,8 +32,8 @@ impl fmt::Display for SlideResult {
|
||||
pub struct Slider;
|
||||
|
||||
impl Slider {
|
||||
pub fn new() -> Result<Self, anyhow::Error> {
|
||||
Ok(Self)
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
/// 对应 Python: slide_match 滑块匹配接口
|
||||
pub fn slide_match(
|
||||
@@ -42,10 +42,11 @@ impl Slider {
|
||||
background_image: &DynamicImage,
|
||||
simple_target: bool,
|
||||
) -> Result<SlideResult> {
|
||||
let target_array = image_to_ndarray(target_image,ColorMode::RGB)?;
|
||||
let background_array = image_to_ndarray(background_image,ColorMode::RGB)?;
|
||||
let target_array = image_to_ndarray(target_image, ColorMode::RGB)?;
|
||||
let background_array = image_to_ndarray(background_image, ColorMode::RGB)?;
|
||||
|
||||
self.perform_slide_match(target_array.view(), background_array.view(), simple_target)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
/// 对应 Python: slide_comparison 差异比较接口
|
||||
/// 用于比较带坑位的图片与原始背景图,定位差异点
|
||||
@@ -55,36 +56,35 @@ impl Slider {
|
||||
background_image: &DynamicImage,
|
||||
) -> Result<SlideResult> {
|
||||
// 1. 转换为 ndarray (HWC RGB)
|
||||
let target_array = image_to_ndarray(target_image,ColorMode::RGB)?;
|
||||
let background_array = image_to_ndarray(background_image,ColorMode::RGB)?;
|
||||
let target_array = image_to_ndarray(target_image, ColorMode::RGB)?;
|
||||
let background_array = image_to_ndarray(background_image, ColorMode::RGB)?;
|
||||
|
||||
// 2. 执行比较逻辑 (对应 _perform_slide_comparison)
|
||||
self.perform_slide_comparison(target_array.view(), background_array.view())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
/// 对应 Python: _perform_slide_comparison
|
||||
pub fn perform_slide_comparison(
|
||||
&self,
|
||||
target: ArrayView3<u8>,
|
||||
background: ArrayView3<u8>,
|
||||
) -> Result<SlideResult> {
|
||||
) -> Result<SlideResult, ImagePreprocessError> {
|
||||
// 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!(
|
||||
return Err(ImagePreprocessError::MismatchDimensions(format!(
|
||||
"比较模式要求两张图分辨率与通道数完全一致!Target: [{}x{}x{}], Background: [{}x{}x{}]",
|
||||
tw,
|
||||
th,
|
||||
tc,
|
||||
bw,
|
||||
bh,
|
||||
bc
|
||||
));
|
||||
tw, th, tc, bw, bh, bc
|
||||
)));
|
||||
}
|
||||
if th == 0 || tw == 0 {
|
||||
return Err(anyhow!("输入图像尺寸不能为0"));
|
||||
return Err(ImagePreprocessError::InvalidDimensions {
|
||||
expected: "输入图像尺寸不能为0".to_string(),
|
||||
actual: vec![th, tw],
|
||||
});
|
||||
}
|
||||
|
||||
let diff_array = abs_diff(&target, &background);
|
||||
@@ -140,29 +140,31 @@ impl Slider {
|
||||
target: ArrayView3<u8>,
|
||||
background: ArrayView3<u8>,
|
||||
simple_target: bool, // 增加这个参数
|
||||
) -> Result<SlideResult> {
|
||||
) -> Result<SlideResult, ImagePreprocessError> {
|
||||
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"));
|
||||
return Err(ImagePreprocessError::InvalidDimensions {
|
||||
expected: "输入图像的宽度或高度不能为0".to_string(),
|
||||
actual: vec![th, tw, tc],
|
||||
});
|
||||
}
|
||||
if th > bh || tw > bw {
|
||||
return Err(anyhow!(
|
||||
"尺寸不匹配:滑块模板(target)尺寸 [{}x{}] 不能大于背景图(background) [{}x{}]",
|
||||
tw,
|
||||
th,
|
||||
bw,
|
||||
bh
|
||||
));
|
||||
return Err(ImagePreprocessError::TargetExceedsBackground {
|
||||
// "尺寸不匹配:滑块模板(target)尺寸 [{}x{}] 不能大于背景图(background) [{}x{}]",
|
||||
target_w: tw,
|
||||
target_h: th,
|
||||
bg_w: bw,
|
||||
bg_h: bh,
|
||||
});
|
||||
}
|
||||
if tc != bc {
|
||||
return Err(anyhow!(
|
||||
return Err(ImagePreprocessError::MismatchDimensions(format!(
|
||||
"目标图与背景图的通道数不一致 (target: {}, bg: {})",
|
||||
tc,
|
||||
bc
|
||||
));
|
||||
tc, bc
|
||||
)));
|
||||
}
|
||||
|
||||
// 1. 统一灰度化
|
||||
@@ -171,11 +173,11 @@ impl Slider {
|
||||
|
||||
if simple_target {
|
||||
// 2a. 简单模式:直接在灰度图上匹配
|
||||
self.simple_template_match(target_gray.view(), background_gray.view())
|
||||
Ok(self.simple_template_match(target_gray.view(), background_gray.view()))
|
||||
} else {
|
||||
// 2b. 复杂模式:先提取边缘,再匹配
|
||||
|
||||
self.edge_based_match(target_gray.view(), background_gray.view())
|
||||
Ok(self.edge_based_match(target_gray.view(), background_gray.view()))
|
||||
}
|
||||
}
|
||||
/// 对应 Python: _simple_template_match
|
||||
@@ -185,7 +187,7 @@ impl Slider {
|
||||
&self,
|
||||
target: ArrayView2<u8>,
|
||||
background: ArrayView2<u8>,
|
||||
) -> Result<SlideResult> {
|
||||
) -> SlideResult {
|
||||
// 1. 将 ndarray 转换为 imageproc 需要的 ImageBuffer (无拷贝或轻量转换)
|
||||
// 转换逻辑 (假设你已经有方法转回 ImageBuffer)
|
||||
let t_buf = ndarray_to_luma8(target);
|
||||
@@ -211,12 +213,12 @@ impl Slider {
|
||||
// println!("Rust Target Width (tw): {}", tw);
|
||||
// println!("Rust Best Max Loc X: {}", max_loc.0);
|
||||
// println!("Rust Final Center X: {}", center_x);
|
||||
Ok(SlideResult {
|
||||
SlideResult {
|
||||
target: [center_x, center_y],
|
||||
target_x: center_x,
|
||||
target_y: center_y,
|
||||
confidence: max_val as f64,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 对应 Python: _edge_based_match
|
||||
@@ -225,7 +227,7 @@ impl Slider {
|
||||
&self,
|
||||
target: ArrayView2<u8>,
|
||||
background: ArrayView2<u8>,
|
||||
) -> Result<SlideResult> {
|
||||
) -> SlideResult {
|
||||
// 1. 将 ndarray 转换为 ImageBuffer
|
||||
// 注意:Canny 和 match_template 需要 ImageBuffer 格式
|
||||
let t_buf = ndarray_to_luma8(target);
|
||||
@@ -260,11 +262,11 @@ impl Slider {
|
||||
println!("-Rust Target Width (tw): {}", tw);
|
||||
println!("-Rust Best Max Loc X: {}", max_loc.0);
|
||||
println!("-Rust Final Center X: {}", center_x);
|
||||
Ok(SlideResult {
|
||||
SlideResult {
|
||||
target: [center_x, center_y],
|
||||
target_x: center_x,
|
||||
target_y: center_y,
|
||||
confidence: max_val as f64,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::error::{DdddError, ImagePreprocessError, Result};
|
||||
use crate::error::{ImagePreprocessError, Result};
|
||||
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
|
||||
use ndarray::{Array3, ArrayViewD};
|
||||
|
||||
@@ -11,7 +11,7 @@ pub enum ColorMode {
|
||||
|
||||
/// 封装数组转图像的逻辑,
|
||||
// 对应 Python 版 _numpy_to_pil_image
|
||||
pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage> {
|
||||
pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage,ImagePreprocessError> {
|
||||
let shape = array.shape();
|
||||
let dim = shape.len();
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage> {
|
||||
// 对应 Python: array.shape[2] == 4 (RGBA H, W, 4)
|
||||
4 => ColorMode::RGBA,
|
||||
_ => {
|
||||
return Err(ImagePreprocessError::UnsupportedChannels(c))?;
|
||||
return Err(ImagePreprocessError::UnsupportedChannels(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,7 @@ pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage> {
|
||||
return Err(ImagePreprocessError::InvalidDimensions {
|
||||
expected: "2D (H,W) 或 3D (H,W,C)".to_string(),
|
||||
actual: shape.to_vec(),
|
||||
}
|
||||
.into());
|
||||
});
|
||||
}
|
||||
};
|
||||
from_ndarray(array, color_mode)
|
||||
@@ -98,7 +97,7 @@ pub fn png_rgba_white_preprocess(img: &DynamicImage) -> DynamicImage {
|
||||
DynamicImage::ImageRgb8(background)
|
||||
}
|
||||
/// 将 DynamicImage 转换为 array 数组
|
||||
pub fn image_to_ndarray(image: &DynamicImage, mode: ColorMode) -> Result<Array3<u8>> {
|
||||
pub fn image_to_ndarray(image: &DynamicImage, mode: ColorMode) -> Result<Array3<u8>,ImagePreprocessError> {
|
||||
// 1. 模式转换 (对应 utils.convert(target_mode)),此函数在时保留看后续优化是否需要替代image_to_ndarray
|
||||
// Rust utils 库通过 to_rgb8, to_luma8 等方法实现转换
|
||||
let (width, height) = image.dimensions();
|
||||
@@ -114,7 +113,7 @@ pub fn image_to_ndarray(image: &DynamicImage, mode: ColorMode) -> Result<Array3<
|
||||
Ok(array)
|
||||
}
|
||||
/// 将 array 数组转换为 DynamicImage
|
||||
pub fn ndarray_to_image(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage> {
|
||||
pub fn ndarray_to_image(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage,ImagePreprocessError> {
|
||||
let shape = array.shape();
|
||||
|
||||
// 基础边界检查:至少要有 H 和 W 两个维度
|
||||
@@ -127,7 +126,7 @@ pub fn ndarray_to_image(array: ArrayViewD<u8>, mode: ColorMode) -> Result<Dynami
|
||||
from_ndarray(array, mode)
|
||||
}
|
||||
|
||||
fn from_ndarray(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage> {
|
||||
fn from_ndarray(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage,ImagePreprocessError> {
|
||||
let shape = array.shape();
|
||||
|
||||
// 映射:ndarray 的 shape 默认是 [Height, Width, (Channels)]
|
||||
@@ -158,7 +157,6 @@ fn from_ndarray(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage>
|
||||
height,
|
||||
channels,
|
||||
}
|
||||
.into()
|
||||
};
|
||||
|
||||
// 2. 重新解释内存并构建 ImageBuffer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::OcrOutput;
|
||||
use crate::error::{DdddError, Result, TensorError};
|
||||
use crate::error::{Result, TensorError};
|
||||
use ndarray::s;
|
||||
/// 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵
|
||||
pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) -> Result<OcrOutput,TensorError> {
|
||||
|
||||
Reference in New Issue
Block a user