refactor(error): 规范化分层错误类型并优化异常捕捉
- 新增 tracing 记录异常,移除不必要的 Result - 重构 错误处理架构
This commit is contained in:
@@ -21,3 +21,4 @@ serde = { version = "1.0.228", features = ["derive"] }
|
|||||||
serde_json = "1.0.150"
|
serde_json = "1.0.150"
|
||||||
ndarray = "0.16.1"
|
ndarray = "0.16.1"
|
||||||
thiserror = "1.0" # 刚好可以开始接入你需要的标准库错误处理
|
thiserror = "1.0" # 刚好可以开始接入你需要的标准库错误处理
|
||||||
|
tracing = "0.1.44" # 埋入日志打点(后续需要继续优化,现在只是简单尝试)
|
||||||
@@ -13,5 +13,5 @@ serde = { workspace = true }
|
|||||||
serde_json = "1.0.150"
|
serde_json = "1.0.150"
|
||||||
ndarray = { workspace = true } # 继承自工作空间
|
ndarray = { workspace = true } # 继承自工作空间
|
||||||
thiserror = { workspace = true } # 刚好可以开始接入你需要的标准库错误处理
|
thiserror = { workspace = true } # 刚好可以开始接入你需要的标准库错误处理
|
||||||
|
tracing={workspace = true}
|
||||||
#serde = { workspace = true, features = ["derive"] }
|
#serde = { workspace = true, features = ["derive"] }
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::{Context, Result};
|
use crate::error::{Result, TensorError};
|
||||||
use image::{imageops::FilterType, DynamicImage, GenericImageView};
|
use image::{imageops::FilterType, DynamicImage, GenericImageView};
|
||||||
use std::fmt;
|
|
||||||
use ndarray::{prelude::*, s, Array2, Array3, Array4, Axis};
|
use ndarray::{prelude::*, s, Array2, Array3, Array4, Axis};
|
||||||
|
use std::fmt;
|
||||||
// use tract_onnx::prelude::{Tensor};
|
// use tract_onnx::prelude::{Tensor};
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ impl<'a> Detector<'a> {
|
|||||||
self.get_bbox(image)
|
self.get_bbox(image)
|
||||||
}
|
}
|
||||||
/// 2. preproc: 纯 Rust 实现 (替代 OpenCV)
|
/// 2. preproc: 纯 Rust 实现 (替代 OpenCV)
|
||||||
fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> Result<(Array4<f32>, f32)> {
|
fn preproc(&self, image: &DynamicImage, input_size: (u32, u32)) -> (Array4<f32>, f32) {
|
||||||
let (target_h, target_w) = input_size;
|
let (target_h, target_w) = input_size;
|
||||||
let (img_w, img_h) = image.dimensions();
|
let (img_w, img_h) = image.dimensions();
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ impl<'a> Detector<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((array, r))
|
(array, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 3. demo_postprocess (逻辑与 Python 一致)
|
/// 3. demo_postprocess (逻辑与 Python 一致)
|
||||||
@@ -252,7 +252,7 @@ impl<'a> Detector<'a> {
|
|||||||
// 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.session.run(tvec!(input_tensor.into()))?;
|
// let outputs = self.session.session.run(tvec!(input_tensor.into()))?;
|
||||||
@@ -268,8 +268,13 @@ impl<'a> Detector<'a> {
|
|||||||
let obj_conf = pred.slice(s![.., 4..5]);
|
let obj_conf = pred.slice(s![.., 4..5]);
|
||||||
let cls_conf = pred.slice(s![.., 5..]);
|
let cls_conf = pred.slice(s![.., 5..]);
|
||||||
let obj_broadcast = obj_conf
|
let obj_broadcast = obj_conf
|
||||||
.broadcast(cls_conf.dim())
|
.broadcast(cls_conf.dim()).ok_or_else(|| {
|
||||||
.context("ndarray broadcasting failed for scores calculation")?;
|
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 = &obj_broadcast * &cls_conf;
|
||||||
// let scores = &pred.slice(s![.., 4..5]) * &pred.slice(s![.., 5..]);
|
// let scores = &pred.slice(s![.., 4..5]) * &pred.slice(s![.., 5..]);
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ use thiserror::Error;
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
/// 统一用我们自己的 DdddError 包装 Result
|
/// 统一用我们自己的 DdddError 包装 Result
|
||||||
pub type Result<T> = std::result::Result<T, DdddError>;
|
// pub type Result<T> = std::result::Result<T, DdddError>;
|
||||||
|
pub type Result<T, E = DdddError> = std::result::Result<T, E>;
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 1. 顶层全局 Error 分流器 (去 anyhow 化,完全基于标准库/自定义类型)
|
// 1. 顶层全局 Error 分流器 (去 anyhow 化,完全基于标准库/自定义类型)
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -83,27 +84,26 @@ pub enum DdddError {
|
|||||||
// /// 系统文件、网络等原生 I/O 异常 (高优先级自动转换)
|
// /// 系统文件、网络等原生 I/O 异常 (高优先级自动转换)
|
||||||
// #[error("系统网络或文件 I/O 异常: {0}")]
|
// #[error("系统网络或文件 I/O 异常: {0}")]
|
||||||
// Io(#[from] std::io::Error),
|
// Io(#[from] std::io::Error),
|
||||||
|
|
||||||
/// 图像预处理阶段发生异常
|
/// 图像预处理阶段发生异常
|
||||||
#[error("图像预处理失败: {0}")]
|
#[error("图像预处理失败: {0}")]
|
||||||
Preprocess(#[from] ImagePreprocessReason),
|
Preprocess(#[from] ImagePreprocessError),
|
||||||
|
|
||||||
/// 推理引擎与张量操作阶段发生异常
|
/// 推理引擎与张量操作阶段发生异常
|
||||||
#[error("推理与模型输入/输出张量异常: {0}")]
|
#[error("推理与模型输入/输出张量异常: {0}")]
|
||||||
Inference(#[from] TensorErrorReason),
|
Inference(#[from] TensorError),
|
||||||
|
|
||||||
/// 算法后处理解码阶段发生异常
|
/// 算法后处理解码阶段发生异常
|
||||||
#[error("后处理解码错误: {0}")]
|
#[error("后处理解码错误: {0}")]
|
||||||
Decode(#[from] DecodeReason),
|
Decode(#[from] DecodeError),
|
||||||
|
|
||||||
/// 框架内部不可恢复的逻辑断言错误(例如解析节点 Fact 失败)
|
/// 框架内部不可恢复的逻辑断言错误(例如解析节点 Fact 失败)
|
||||||
#[error("内部严重逻辑错误: {0}")]
|
#[error("内部严重逻辑错误: {0}")]
|
||||||
Internal(String),
|
Internal(String),
|
||||||
|
|
||||||
/// 【流派一核心】接替 anyhow::Error 的用户自定义扩展错误
|
/// 【流派核心】接替 anyhow::Error 的用户自定义扩展错误
|
||||||
/// 承载任何第三方扩展、解密、特定预处理插件在执行时产生的自定义错误
|
/// 承载任何第三方扩展、解密、特定预处理插件在执行时产生的自定义错误
|
||||||
#[error("用户自定义扩展错误: {0}")]
|
#[error("用户自定义扩展错误: {0}")]
|
||||||
Other(Box<dyn std::error::Error + Send + Sync>),
|
Other(#[source]Box<dyn std::error::Error + Send + Sync>),
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -111,7 +111,7 @@ pub enum DdddError {
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ImagePreprocessReason {
|
pub enum ImagePreprocessError {
|
||||||
// #[error("图片加载或解码失败: {0}")]
|
// #[error("图片加载或解码失败: {0}")]
|
||||||
// ImageIo(#[from] image::ImageError),
|
// ImageIo(#[from] image::ImageError),
|
||||||
// image_io
|
// image_io
|
||||||
@@ -119,14 +119,16 @@ pub enum ImagePreprocessReason {
|
|||||||
Ndarray(#[from] ndarray::ShapeError),
|
Ndarray(#[from] ndarray::ShapeError),
|
||||||
// image_io
|
// image_io
|
||||||
#[error("图像矩阵维度不合规!预期: {expected},实际图像形状: {actual:?}")]
|
#[error("图像矩阵维度不合规!预期: {expected},实际图像形状: {actual:?}")]
|
||||||
InvalidImageDimensions {
|
InvalidDimensions {
|
||||||
expected: String,
|
expected: String,
|
||||||
actual: Vec<usize>,
|
actual: Vec<usize>,
|
||||||
},
|
},
|
||||||
|
|
||||||
// image_io
|
// image_io
|
||||||
/// 从 ndarray 原始数据构建图像缓冲区时,缓冲区长度与分辨率/通道数不匹配
|
/// 从 ndarray 原始数据构建图像缓冲区时,缓冲区长度与分辨率/通道数不匹配
|
||||||
#[error("图像缓冲区长度不匹配!预期大小: {expected},实际大小: {actual} (分辨率: {width}x{height}, 通道数: {channels})")]
|
#[error(
|
||||||
|
"图像缓冲区长度不匹配!预期大小: {expected},实际大小: {actual} (分辨率: {width}x{height}, 通道数: {channels})"
|
||||||
|
)]
|
||||||
BufferLengthMismatch {
|
BufferLengthMismatch {
|
||||||
expected: usize,
|
expected: usize,
|
||||||
actual: usize,
|
actual: usize,
|
||||||
@@ -137,7 +139,18 @@ pub enum ImagePreprocessReason {
|
|||||||
// image_io
|
// image_io
|
||||||
#[error("不支持的图像通道数: {0} (仅支持单通道灰度L、3通道RGB、4通道RGBA)")]
|
#[error("不支持的图像通道数: {0} (仅支持单通道灰度L、3通道RGB、4通道RGBA)")]
|
||||||
UnsupportedChannels(usize),
|
UnsupportedChannels(usize),
|
||||||
|
// ================= 新增:针对 HSV 和 Preset 的强类型错误 =================
|
||||||
|
/// HSV 颜色区间非法 (例如 H > 180 或 lower > upper)
|
||||||
|
#[error("HSV 颜色区间参数非法: {0}")]
|
||||||
|
InvalidHsvRange (String ),
|
||||||
|
|
||||||
|
/// 不支持或未知的颜色预设名称
|
||||||
|
#[error("不支持的颜色预设名称: {0}")]
|
||||||
|
UnknownColorPreset(String),
|
||||||
|
|
||||||
|
/// 颜色过滤器/预处理规则配置非法导致失败
|
||||||
|
#[error("颜色过滤器配置无效或初始化失败: {0}")]
|
||||||
|
FilterConfigInvalid(String),
|
||||||
// #[error("Base64 解码失败: {0}")]
|
// #[error("Base64 解码失败: {0}")]
|
||||||
// Base64(#[from] base64::DecodeError),
|
// Base64(#[from] base64::DecodeError),
|
||||||
//
|
//
|
||||||
@@ -153,14 +166,14 @@ pub enum ImagePreprocessReason {
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum TensorErrorReason {
|
pub enum TensorError {
|
||||||
/// 替换原有的 anyhow::Error,明确将 Tract/ONNX 引擎底层报错序列化为干净的 String
|
/// 替换原有的 anyhow::Error,明确将 Tract/ONNX 引擎底层报错序列化为干净的 String
|
||||||
#[error("推理引擎内部发生异常: {0}")]
|
#[error("推理引擎内部发生异常: {0}")]
|
||||||
EngineError(String),
|
Engine(String),
|
||||||
|
|
||||||
/// 模型张量维度不匹配 (原有的顶层 DimensionMismatch 被优雅地归入本模块)
|
/// 模型张量维度不匹配 (原有的顶层 DimensionMismatch 被优雅地归入本模块)
|
||||||
#[error("模型张量维度不匹配!预期: {expected},实际 Tensor 形状: {actual:?}")]
|
#[error("模型张量维度不匹配!预期: {expected},实际 Tensor 形状: {actual:?}")]
|
||||||
TensorDimensionMismatch {
|
DimensionMismatch {
|
||||||
expected: String,
|
expected: String,
|
||||||
actual: Vec<usize>,
|
actual: Vec<usize>,
|
||||||
},
|
},
|
||||||
@@ -184,10 +197,10 @@ pub enum TensorErrorReason {
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DecodeReason {
|
pub enum DecodeError {
|
||||||
/// CTC 解码器解码过程中的逻辑报错
|
/// CTC 解码器解码过程中的逻辑报错
|
||||||
#[error("CTC 解码异常: {0}")]
|
#[error("CTC 解码异常: {0}")]
|
||||||
CtcDecodeError(String),
|
Ctc(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -223,7 +236,7 @@ impl DdddError {
|
|||||||
pub fn is_invalid_dimensions(&self) -> bool {
|
pub fn is_invalid_dimensions(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
DdddError::Preprocess(ImagePreprocessReason::InvalidImageDimensions { .. })
|
DdddError::Preprocess(ImagePreprocessError::InvalidDimensions { .. })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,22 +244,21 @@ impl DdddError {
|
|||||||
pub fn is_unsupported_channels(&self) -> bool {
|
pub fn is_unsupported_channels(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
DdddError::Preprocess(ImagePreprocessReason::UnsupportedChannels(_))
|
DdddError::Preprocess(ImagePreprocessError::UnsupportedChannels(_))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 提取出底层最原始的那个错误(无论是 IO、预处理、推理、还是第三方扩展错误)
|
// 提取出底层最原始的那个错误(无论是 IO、预处理、推理、还是第三方扩展错误)
|
||||||
/// 方便外层统一打印更深层的 `source` 链条
|
// 方便外层统一打印更深层的 `source` 链条
|
||||||
pub fn source_error(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
// pub fn source_error(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
use std::error::Error;
|
// use std::error::Error;
|
||||||
match self {
|
// match self {
|
||||||
// DdddError::Io(err) => Some(err),
|
// // DdddError::Io(err) => Some(err),
|
||||||
DdddError::Preprocess(err) => Some(err),
|
// DdddError::Preprocess(err) => Some(err),
|
||||||
DdddError::Inference(err) => Some(err),
|
// DdddError::Inference(err) => Some(err),
|
||||||
DdddError::Decode(err) => Some(err),
|
// DdddError::Decode(err) => Some(err),
|
||||||
DdddError::Other(err) => Some(err.as_ref()),
|
// DdddError::Other(err) => Some(err.as_ref()),
|
||||||
DdddError::Internal(_) => None, // Internal 内部目前只有 String,没有底层的 Error source
|
// DdddError::Internal(_) => None, // Internal 内部目前只有 String,没有底层的 Error source
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::error::{ImagePreprocessError, Result};
|
||||||
use crate::utils::image_processor::rgb_to_opencv_hsv;
|
use crate::utils::image_processor::rgb_to_opencv_hsv;
|
||||||
use anyhow::anyhow;
|
|
||||||
use image::{DynamicImage, ImageBuffer, Rgb};
|
use image::{DynamicImage, ImageBuffer, Rgb};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
@@ -18,12 +18,13 @@ fn is_pixel_matched(ranges: &[HsvRange], h: u8, s: u8, v: u8) -> bool {
|
|||||||
pub fn apply_to_image(
|
pub fn apply_to_image(
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
hsv_ranges: &[HsvRange],
|
hsv_ranges: &[HsvRange],
|
||||||
) -> anyhow::Result<DynamicImage> {
|
) -> Result<DynamicImage, ImagePreprocessError> {
|
||||||
// 1. 统一转换为连续内存的 RGB8 缓冲区 (对应 Python 的 Image 到 RGB/BGR 数组转换)
|
// 1. 统一转换为连续内存的 RGB8 缓冲区 (对应 Python 的 Image 到 RGB/BGR 数组转换)
|
||||||
let rgb_img = image.to_rgb8();
|
let rgb_img = image.to_rgb8();
|
||||||
let (width, height) = rgb_img.dimensions();
|
let (width, height) = rgb_img.dimensions();
|
||||||
let mut raw_pixels = rgb_img.into_raw();
|
let mut raw_pixels = rgb_img.into_raw();
|
||||||
|
let actual_len = raw_pixels.len();
|
||||||
|
let expected_len = (width as usize) * (height as usize) * 3;
|
||||||
// 2. 密集计算核心:原地流式迭代修改
|
// 2. 密集计算核心:原地流式迭代修改
|
||||||
// 每次取出 3 个 u8 字节,分别代表 [R, G, B],无多余掩膜矩阵内存分配
|
// 每次取出 3 个 u8 字节,分别代表 [R, G, B],无多余掩膜矩阵内存分配
|
||||||
for chunk in raw_pixels.chunks_exact_mut(3) {
|
for chunk in raw_pixels.chunks_exact_mut(3) {
|
||||||
@@ -45,8 +46,14 @@ pub fn apply_to_image(
|
|||||||
|
|
||||||
// 3. 将扁平字节数组重新打包回 DynamicImage 容器
|
// 3. 将扁平字节数组重新打包回 DynamicImage 容器
|
||||||
let filtered_buffer = ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(width, height, raw_pixels)
|
let filtered_buffer = ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(width, height, raw_pixels)
|
||||||
.ok_or_else(|| anyhow!("图像缓冲重新组装失败,维度与数据大小不匹配"))?;
|
// .ok_or_else(|| anyhow!("图像缓冲重新组装失败,维度与数据大小不匹配"))?;
|
||||||
|
.ok_or_else(|| ImagePreprocessError::BufferLengthMismatch {
|
||||||
|
expected: expected_len,
|
||||||
|
actual: actual_len,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
channels: 3,
|
||||||
|
})?;
|
||||||
Ok(DynamicImage::ImageRgb8(filtered_buffer))
|
Ok(DynamicImage::ImageRgb8(filtered_buffer))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,16 +71,22 @@ impl HsvRange {
|
|||||||
impl HsvRange {
|
impl HsvRange {
|
||||||
/// 验证当前 HSV 范围是否合法
|
/// 验证当前 HSV 范围是否合法
|
||||||
/// 对应 Python 逻辑:H 在 0-180,S/V 在 0-255,且下界 <= 上界
|
/// 对应 Python 逻辑:H 在 0-180,S/V 在 0-255,且下界 <= 上界
|
||||||
pub fn validate(&self) -> Result<(), String> {
|
pub fn validate(&self) -> Result<(), ImagePreprocessError> {
|
||||||
// 1. 校验 H 通道边界 (OpenCV 中 H 范围是 0-180)
|
// 1. 校验 H 通道边界 (OpenCV 中 H 范围是 0-180)
|
||||||
if self.lower.0 > 180 || self.upper.0 > 180 {
|
if self.lower.0 > 180 || self.upper.0 > 180 {
|
||||||
return Err("H通道值必须在 0-180 范围内".to_string());
|
// return Err("H通道值必须在 0-180 范围内".to_string());
|
||||||
|
return Err(ImagePreprocessError::InvalidHsvRange(
|
||||||
|
"H通道值必须在 0-180 范围内".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 校验下界不能大于上界
|
// 2. 校验下界不能大于上界
|
||||||
if self.lower.0 > self.upper.0 || self.lower.1 > self.upper.1 || self.lower.2 > self.upper.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());
|
// return Err("HSV范围下界不能大于上界".to_string());
|
||||||
|
return Err(ImagePreprocessError::InvalidHsvRange(
|
||||||
|
"HSV范围下界不能大于上界".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -150,7 +163,7 @@ impl ColorPreset {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 校验逻辑:在这里实现完美的“责任分离”
|
/// 校验逻辑:在这里实现完美的“责任分离”
|
||||||
pub fn validate(&self) -> Result<(), String> {
|
pub fn validate(&self) -> Result<(), ImagePreprocessError> {
|
||||||
match self {
|
match self {
|
||||||
// 1. 快捷变体:完全绕过,根本不校验,0 运行时开销放行!
|
// 1. 快捷变体:完全绕过,根本不校验,0 运行时开销放行!
|
||||||
ColorPreset::Custom(ranges) => {
|
ColorPreset::Custom(ranges) => {
|
||||||
@@ -166,7 +179,7 @@ impl ColorPreset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ColorPreset {
|
impl FromStr for ColorPreset {
|
||||||
type Err = String;
|
type Err = ImagePreprocessError;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
"red" => Ok(ColorPreset::Red),
|
"red" => Ok(ColorPreset::Red),
|
||||||
@@ -179,7 +192,8 @@ impl FromStr for ColorPreset {
|
|||||||
"black" => Ok(ColorPreset::Black),
|
"black" => Ok(ColorPreset::Black),
|
||||||
"white" => Ok(ColorPreset::White),
|
"white" => Ok(ColorPreset::White),
|
||||||
"gray" => Ok(ColorPreset::Gray),
|
"gray" => Ok(ColorPreset::Gray),
|
||||||
_ => Err(format!("不支持的颜色预设: {}", s)),
|
// _ => Err(format!("不支持的颜色预设: {}", s)),
|
||||||
|
_ => Err(ImagePreprocessError::UnknownColorPreset(s.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,12 +214,12 @@ pub trait ColorFilter {
|
|||||||
fn estimated_count(&self) -> usize;
|
fn estimated_count(&self) -> usize;
|
||||||
/// 将自身的有效约束平铺追加到统一目标容器中
|
/// 将自身的有效约束平铺追加到统一目标容器中
|
||||||
/// 验证当前过滤器是否合法,默认直接放行(Ok(()))
|
/// 验证当前过滤器是否合法,默认直接放行(Ok(()))
|
||||||
fn validate_self(&self) -> Result<(), String> {
|
fn validate_self(&self) -> Result<(), ImagePreprocessError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
/// 【新扩展的架构方法】将自身安全的合并到已有的普通容器中,并完成去重和排序
|
/// 【新扩展的架构方法】将自身安全的合并到已有的普通容器中,并完成去重和排序
|
||||||
/// 完美的责任分离:Builder 不再需要关心怎么分配内存、怎么排序去重
|
/// 完美的责任分离:Builder 不再需要关心怎么分配内存、怎么排序去重
|
||||||
fn collect_to_vec(&self) -> Result<Option<Vec<HsvRange>>, String> {
|
fn collect_to_vec(&self) -> Result<Option<Vec<HsvRange>>, ImagePreprocessError> {
|
||||||
// 1. 触发自检
|
// 1. 触发自检
|
||||||
self.validate_self()?;
|
self.validate_self()?;
|
||||||
|
|
||||||
@@ -237,7 +251,7 @@ impl ColorFilter for ColorPreset {
|
|||||||
// 直接获取切片长度
|
// 直接获取切片长度
|
||||||
self.matches().len()
|
self.matches().len()
|
||||||
}
|
}
|
||||||
fn validate_self(&self) -> Result<(), String> {
|
fn validate_self(&self) -> Result<(), ImagePreprocessError> {
|
||||||
// 直接调用我们在第一步中为 ColorPreset 实现的精细化分流校验
|
// 直接调用我们在第一步中为 ColorPreset 实现的精细化分流校验
|
||||||
// 快捷变体在这里会直接返回 Ok(()), 只有 Custom 才会去真正校验
|
// 快捷变体在这里会直接返回 Ok(()), 只有 Custom 才会去真正校验
|
||||||
self.validate()
|
self.validate()
|
||||||
@@ -261,7 +275,7 @@ impl<'a> ColorFilter for MultiOrColorRestrict<'a> {
|
|||||||
self.filters.iter().map(|f| f.estimated_count()).sum()
|
self.filters.iter().map(|f| f.estimated_count()).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_self(&self) -> Result<(), String> {
|
fn validate_self(&self) -> Result<(), ImagePreprocessError> {
|
||||||
// 递归政审:只要其中一个子过滤器校验失败(比如某个 Custom 变体非法),立刻熔断
|
// 递归政审:只要其中一个子过滤器校验失败(比如某个 Custom 变体非法),立刻熔断
|
||||||
for f in &self.filters {
|
for f in &self.filters {
|
||||||
f.validate_self()?;
|
f.validate_self()?;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use crate::ocr::color_filter::{HsvRange, apply_to_image};
|
|||||||
// use ddddocr_tract::session::{ModelOutput, OcrSession};
|
// use ddddocr_tract::session::{ModelOutput, OcrSession};
|
||||||
use crate::utils::image_convert::png_rgba_white_preprocess;
|
use crate::utils::image_convert::png_rgba_white_preprocess;
|
||||||
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
||||||
use anyhow::Result;
|
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
@@ -17,7 +16,9 @@ use ndarray::ArrayView2;
|
|||||||
// Indices(ndarray::Array1<i64>), // 拥有完整所有权的 1维数组,可任意传递和返回
|
// Indices(ndarray::Array1<i64>), // 拥有完整所有权的 1维数组,可任意传递和返回
|
||||||
// Logits(ndarray::Array2<f32>), // 拥有完整所有权的 2维矩阵,可任意传递和返回
|
// Logits(ndarray::Array2<f32>), // 拥有完整所有权的 2维矩阵,可任意传递和返回
|
||||||
// }
|
// }
|
||||||
|
use crate::error::{ImagePreprocessError, Result, TensorError};
|
||||||
use crate::{OcrEngine, OcrOutput};
|
use crate::{OcrEngine, OcrOutput};
|
||||||
|
use tracing::{ warn};
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub enum OcrResult {
|
pub enum OcrResult {
|
||||||
/// 纯文本分支(对应 probability = false)
|
/// 纯文本分支(对应 probability = false)
|
||||||
@@ -107,7 +108,7 @@ pub struct Ocr<'a> {
|
|||||||
pub(crate) png_fix: bool,
|
pub(crate) png_fix: bool,
|
||||||
pub(crate) probability: bool,
|
pub(crate) probability: bool,
|
||||||
/// 颜色过滤:保留的颜色列表
|
/// 颜色过滤:保留的颜色列表
|
||||||
pub(crate) final_color_ranges: Result<Option<Vec<HsvRange>>, String>,
|
pub(crate) final_color_ranges: Result<Option<Vec<HsvRange>>, ImagePreprocessError>,
|
||||||
|
|
||||||
/// 字符集范围
|
/// 字符集范围
|
||||||
pub(crate) final_charset_indices: Option<Vec<usize>>,
|
pub(crate) final_charset_indices: Option<Vec<usize>>,
|
||||||
@@ -127,7 +128,7 @@ impl<'a> Ocr<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a> Ocr<'a> {
|
impl<'a> Ocr<'a> {
|
||||||
pub fn predict(&self, image: &DynamicImage) -> anyhow::Result<OcrResult> {
|
pub fn predict(&self, image: &DynamicImage) -> Result<OcrResult> {
|
||||||
println!("当前颜色过滤器状态: {:?}", self.final_color_ranges);
|
println!("当前颜色过滤器状态: {:?}", self.final_color_ranges);
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -137,10 +138,13 @@ impl<'a> Ocr<'a> {
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
let img_cow = match &self.final_color_ranges {
|
let img_cow = match &self.final_color_ranges {
|
||||||
Err(err_msg) => {
|
Err(err_msg) => {
|
||||||
return Err(anyhow::anyhow!(
|
// return Err(anyhow::anyhow!(
|
||||||
"颜色过滤器初始化失败,全链路短路: {}",
|
// "颜色过滤器初始化失败,全链路短路: {}",
|
||||||
err_msg
|
// err_msg
|
||||||
));
|
// ));
|
||||||
|
return Err(ImagePreprocessError::FilterConfigInvalid(
|
||||||
|
err_msg.to_string(),
|
||||||
|
))?;
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// 核心优化点:直接借用原图,不发生任何克隆
|
// 核心优化点:直接借用原图,不发生任何克隆
|
||||||
@@ -168,12 +172,12 @@ impl<'a> Ocr<'a> {
|
|||||||
// let raw_indices = self.ocr.extract_indices_from_tensor(&raw_tensor)?;
|
// let raw_indices = self.ocr.extract_indices_from_tensor(&raw_tensor)?;
|
||||||
// // 步骤 2: 将索引切片 `&[i64]` 传给解码器进行 CTC 去重和字符映射
|
// // 步骤 2: 将索引切片 `&[i64]` 传给解码器进行 CTC 去重和字符映射
|
||||||
// let final_text = self.ctc_decode_to_string(&raw_indices);
|
// let final_text = self.ctc_decode_to_string(&raw_indices);
|
||||||
let ocr_output = self.process_model_output(raw_tensor);
|
let ocr_output = self.process_model_output(raw_tensor)?;
|
||||||
ocr_output
|
Ok(ocr_output)
|
||||||
}
|
}
|
||||||
/// 对应 Python 的 _preprocess_image
|
/// 对应 Python 的 _preprocess_image
|
||||||
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
|
/// 负责:透明背景修复 -> 灰度化 -> 按比例 Resize -> 归一化 -> 4维张量转换
|
||||||
fn preprocess_image(&self, img: &DynamicImage) -> anyhow::Result<ndarray::Array4<f32>> {
|
fn preprocess_image(&self, img: &DynamicImage) -> Result<ndarray::Array4<f32>,ImagePreprocessError> {
|
||||||
// 1. 获取模型元数据配置
|
// 1. 获取模型元数据配置
|
||||||
let meta = self.session.metadata();
|
let meta = self.session.metadata();
|
||||||
let norm = &meta.normalization; // 获取归一化器
|
let norm = &meta.normalization; // 获取归一化器
|
||||||
@@ -239,7 +243,12 @@ impl<'a> Ocr<'a> {
|
|||||||
array
|
array
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => return Err(anyhow::anyhow!("不支持的通道数配置: {}", meta.channel)),
|
// _ => return Err(anyhow::anyhow!("不支持的通道数配置: {}", meta.channel)),
|
||||||
|
_ => {
|
||||||
|
return Err(ImagePreprocessError::UnsupportedChannels(
|
||||||
|
meta.channel as usize,
|
||||||
|
));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(array4)
|
Ok(array4)
|
||||||
// Ok(tensor)
|
// Ok(tensor)
|
||||||
@@ -265,13 +274,14 @@ impl<'a> Ocr<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 这段代码未来直接放入 ddddocr-core
|
// 这段代码未来直接放入 ddddocr-core
|
||||||
fn process_model_output(&self, output: OcrOutput) -> anyhow::Result<OcrResult> {
|
fn process_model_output(&self, output: OcrOutput) -> Result<OcrResult,TensorError> {
|
||||||
match output {
|
match output {
|
||||||
OcrOutput::Indices(array1) => {
|
OcrOutput::Indices(array1) => {
|
||||||
// 对应你原来的 process_i64_tensor
|
// 对应你原来的 process_i64_tensor
|
||||||
let slice = array1
|
let slice = array1
|
||||||
.as_slice()
|
.as_slice()
|
||||||
.ok_or_else(|| anyhow::anyhow!("内存不连续,无法执行零拷贝解码"))?;
|
// .ok_or_else(|| anyhow::anyhow!("内存不连续,无法执行零拷贝解码"))?;
|
||||||
|
.ok_or_else(|| TensorError::NonContiguousMemory)?;
|
||||||
let final_text = self.ctc_decode_to_string(slice);
|
let final_text = self.ctc_decode_to_string(slice);
|
||||||
|
|
||||||
if self.probability {
|
if self.probability {
|
||||||
@@ -528,8 +538,9 @@ impl<'a> Ocr<'a> {
|
|||||||
// 5. 字符映射
|
// 5. 字符映射
|
||||||
if let Some(char_str) = tokens.get(u_idx) {
|
if let Some(char_str) = tokens.get(u_idx) {
|
||||||
res.push_str(char_str);
|
res.push_str(char_str);
|
||||||
} else {
|
}
|
||||||
eprintln!("警告: 预测索引 {} 超出字符集范围", u_idx);
|
else {
|
||||||
|
warn!("警告: 预测索引 {} 超出字符集范围", u_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use crate::error::{ Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::error::{DdddError, ImagePreprocessReason, Result};
|
use crate::error::{DdddError, ImagePreprocessError, Result};
|
||||||
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
|
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
|
||||||
use ndarray::{Array3, ArrayViewD};
|
use ndarray::{Array3, ArrayViewD};
|
||||||
|
|
||||||
@@ -35,19 +35,16 @@ pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage> {
|
|||||||
// 对应 Python: array.shape[2] == 4 (RGBA H, W, 4)
|
// 对应 Python: array.shape[2] == 4 (RGBA H, W, 4)
|
||||||
4 => ColorMode::RGBA,
|
4 => ColorMode::RGBA,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DdddError::Preprocess(
|
return Err(ImagePreprocessError::UnsupportedChannels(c))?;
|
||||||
ImagePreprocessReason::UnsupportedChannels(c),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DdddError::Preprocess(
|
return Err(ImagePreprocessError::InvalidDimensions {
|
||||||
ImagePreprocessReason::InvalidImageDimensions {
|
|
||||||
expected: "2D (H,W) 或 3D (H,W,C)".to_string(),
|
expected: "2D (H,W) 或 3D (H,W,C)".to_string(),
|
||||||
actual: shape.to_vec(),
|
actual: shape.to_vec(),
|
||||||
},
|
}
|
||||||
));
|
.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
from_ndarray(array, color_mode)
|
from_ndarray(array, color_mode)
|
||||||
@@ -113,7 +110,7 @@ pub fn image_to_ndarray(image: &DynamicImage, mode: ColorMode) -> Result<Array3<
|
|||||||
};
|
};
|
||||||
|
|
||||||
let array = Array3::from_shape_vec((height as usize, width as usize, channels), raw)
|
let array = Array3::from_shape_vec((height as usize, width as usize, channels), raw)
|
||||||
.map_err(ImagePreprocessReason::from)?;
|
.map_err(ImagePreprocessError::from)?;
|
||||||
Ok(array)
|
Ok(array)
|
||||||
}
|
}
|
||||||
/// 将 array 数组转换为 DynamicImage
|
/// 将 array 数组转换为 DynamicImage
|
||||||
@@ -122,12 +119,10 @@ pub fn ndarray_to_image(array: ArrayViewD<u8>, mode: ColorMode) -> Result<Dynami
|
|||||||
|
|
||||||
// 基础边界检查:至少要有 H 和 W 两个维度
|
// 基础边界检查:至少要有 H 和 W 两个维度
|
||||||
if shape.len() < 2 {
|
if shape.len() < 2 {
|
||||||
return Err(DdddError::Preprocess(
|
return Err(ImagePreprocessError::InvalidDimensions {
|
||||||
ImagePreprocessReason::InvalidImageDimensions {
|
|
||||||
expected: "至少为 2D array [H, W]".to_string(),
|
expected: "至少为 2D array [H, W]".to_string(),
|
||||||
actual: shape.to_vec(),
|
actual: shape.to_vec(),
|
||||||
},
|
})?;
|
||||||
));
|
|
||||||
}
|
}
|
||||||
from_ndarray(array, mode)
|
from_ndarray(array, mode)
|
||||||
}
|
}
|
||||||
@@ -156,13 +151,14 @@ fn from_ndarray(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage>
|
|||||||
|
|
||||||
// 构造通用错误闭包,避免 match 分支中重复编写冗长的错误对象
|
// 构造通用错误闭包,避免 match 分支中重复编写冗长的错误对象
|
||||||
let make_err = || {
|
let make_err = || {
|
||||||
DdddError::Preprocess(ImagePreprocessReason::BufferLengthMismatch {
|
ImagePreprocessError::BufferLengthMismatch {
|
||||||
expected: expected_len,
|
expected: expected_len,
|
||||||
actual: raw_len,
|
actual: raw_len,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
channels,
|
channels,
|
||||||
})
|
}
|
||||||
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. 重新解释内存并构建 ImageBuffer
|
// 2. 重新解释内存并构建 ImageBuffer
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::error::{DdddError, ImagePreprocessReason, Result};
|
use crate::error::{DdddError, Result};
|
||||||
use crate::utils::image_convert::ndarray_to_hwc_image;
|
use crate::utils::image_convert::ndarray_to_hwc_image;
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use ndarray::ArrayViewD;
|
use ndarray::ArrayViewD;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::OcrOutput;
|
use crate::OcrOutput;
|
||||||
use crate::error::{DdddError, Result, TensorErrorReason};
|
use crate::error::{DdddError, Result, TensorError};
|
||||||
use ndarray::s;
|
use ndarray::s;
|
||||||
/// 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵
|
/// 核心层复用资产:将异构的动态维度矩阵转化为标准 OCR 2D Logits 矩阵
|
||||||
pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) -> Result<OcrOutput> {
|
pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) -> Result<OcrOutput,TensorError> {
|
||||||
let (steps, classes, data_dyn_view) = match shape.len() {
|
let (steps, classes, data_dyn_view) = match shape.len() {
|
||||||
3 => {
|
3 => {
|
||||||
if shape[1] == 1 {
|
if shape[1] == 1 {
|
||||||
@@ -24,12 +24,12 @@ pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) ->
|
|||||||
// 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
|
// 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
|
||||||
1 => (1, shape[0], array),
|
1 => (1, shape[0], array),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DdddError::Inference(
|
return Err(
|
||||||
TensorErrorReason::TensorDimensionMismatch {
|
TensorError::DimensionMismatch {
|
||||||
expected: "1D, 2D, or 3D OCR Logits".to_string(),
|
expected: "1D, 2D, or 3D OCR Logits".to_string(),
|
||||||
actual: shape.to_vec(),
|
actual: shape.to_vec(),
|
||||||
},
|
},
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,10 +39,10 @@ pub fn normalize_ocr_logits(array: ndarray::ArrayViewD<f32>, shape: &[usize]) ->
|
|||||||
.map_err(|shape_err| {
|
.map_err(|shape_err| {
|
||||||
// 如果是因为切片导致不连续且无法进行零拷贝变换,抛出 NonContiguousMemory
|
// 如果是因为切片导致不连续且无法进行零拷贝变换,抛出 NonContiguousMemory
|
||||||
if !data_dyn_view.is_standard_layout() {
|
if !data_dyn_view.is_standard_layout() {
|
||||||
DdddError::Inference(TensorErrorReason::NonContiguousMemory)
|
TensorError::NonContiguousMemory
|
||||||
} else {
|
} else {
|
||||||
// 否则,说明是纯粹的数据元素数量不对(Shape 不匹配),抛出专属的强类型错误
|
// 否则,说明是纯粹的数据元素数量不对(Shape 不匹配),抛出专属的强类型错误
|
||||||
DdddError::Inference(TensorErrorReason::LogitsDimensionMismatch(shape_err))
|
TensorError::LogitsDimensionMismatch(shape_err)
|
||||||
}
|
}
|
||||||
})?
|
})?
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::loader::ModelLoader;
|
use crate::loader::ModelLoader;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use ddddocr_core::error::{DdddError, Result, TensorErrorReason};
|
use ddddocr_core::error::{DdddError, Result, TensorError};
|
||||||
use ddddocr_core::utils::normalize_ocr_logits;
|
use ddddocr_core::utils::normalize_ocr_logits;
|
||||||
use ddddocr_core::{InferenceEngine, ModelMetadata, OcrEngine, OcrOutput};
|
use ddddocr_core::{InferenceEngine, ModelMetadata, OcrEngine, OcrOutput};
|
||||||
use ndarray::s;
|
use ndarray::s;
|
||||||
@@ -49,7 +49,7 @@ impl InferenceEngine for OcrSession {
|
|||||||
.session
|
.session
|
||||||
.run(tvec!(tensor.into()))
|
.run(tvec!(tensor.into()))
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
DdddError::Inference(TensorErrorReason::EngineError(
|
DdddError::Inference(TensorError::EngineError(
|
||||||
"执行模型推理失败".to_string(),
|
"执行模型推理失败".to_string(),
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
Reference in New Issue
Block a user