refactor(errors): 重构错误处理,支持强类型匹配并剥离 base64 依赖

- 新增 Other变体以及构造函数new
- 剥离图像预处理中的 Base64 相关错误至业务层处理
- 引入强类型 `LogitsDimensionMismatch` 替代不便匹配的字符串错误
- 优化 `normalize_ocr_logits` 的转换流程,兼顾零拷贝性能与精细化报错
- 优化 全库错误处理
This commit is contained in:
2026-07-17 20:08:32 +08:00
parent 4f6987f594
commit 913ff4d884
12 changed files with 397 additions and 200 deletions

View File

@@ -1,6 +1,6 @@
use crate::loader::ModelLoader;
use anyhow::Context;
use ddddocr_core::error::{DdddError, Result};
use ddddocr_core::error::{DdddError, Result, TensorErrorReason};
use ddddocr_core::{DetEngine, DetOutput, InferenceEngine};
use ndarray::Ix3;
use std::path::Path;
@@ -42,26 +42,28 @@ impl InferenceEngine for DetSession {
// let result = self.ocr.run(tvec!(tensor.into()))?;
let tensor = Tensor::from(input_array);
let mut result = self
.session
.run(tvec!(tensor.into()))
.context("执行模型推理失败")?;
let mut result = self.session.run(tvec!(tensor.into())).map_err(|_| {
DdddError::Inference(TensorErrorReason::EngineError(
"执行模型推理失败".to_string(),
))
})?;
println!("模型输出原始数据: {:?}", result);
// Ok(result.swap_remove(0).into_tensor())
let raw_tensor = result.swap_remove(0).into_tensor();
let array_d = raw_tensor
.into_array::<f32>()
.context("Tract 实体张量无法转换为 ndarray::ArrayD")?;
let array_d = raw_tensor.into_array::<f32>().map_err(|_| {
DdddError::Inference(TensorErrorReason::EngineError(
"Tract 实体张量无法转换为 ndarray::ArrayD".to_string(),
))
})?;
// 提前利用克隆(Clone)备份好当前未转维度前的真实 shape (Vec<usize>)
let actual_shape = array_d.shape().to_vec();
let array3 =
array_d
.into_dimensionality::<Ix3>()
.map_err(|_| DdddError::DimensionMismatch {
expected: "3D 检测矩阵 [Batch, Box_Count, Box_Attributes]".to_string(),
actual: actual_shape, // 优雅降维失败时动态捕获
})?;
let array3 = array_d.into_dimensionality::<Ix3>().map_err(|_| {
DdddError::Inference(TensorErrorReason::TensorDimensionMismatch {
expected: "3D 检测矩阵 [Batch, Box_Count, Box_Attributes]".to_string(),
actual: actual_shape, // 优雅降维失败时动态捕获
})
})?;
Ok(DetOutput::Detection(array3))
// 在引擎内部消化掉 DatumType 强耦合

View File

@@ -56,12 +56,12 @@ impl ModelLoader {
P: AsRef<std::path::Path>,
{
let session = onnx()
.model_for_path(model_path)
.with_context(|| "加载 ONNX 模型失败,请检查路径是否正确")?
.into_optimized()
.with_context(|| "优化 Tract 模型图失败")?
.into_runnable()
.with_context(|| "构建可运行 Tract 实例失败")?;
.model_for_path(model_path).map_err(DdddError::new)?
// .with_context(|| "加载 ONNX 模型失败,请检查路径是否正确")?
.into_optimized().map_err(DdddError::new)?
// .with_context(|| "优化 Tract 模型图失败")?
.into_runnable().map_err(DdddError::new)?;
// .with_context(|| "构建可运行 Tract 实例失败")?;
Ok(Self { session })
}
/// 策略 B从内存字节流加载模型配合 include_bytes! 使用)
@@ -71,11 +71,13 @@ impl ModelLoader {
let session = onnx()
.model_for_read(&mut cursor)
.with_context(|| "从内存字节流解析 ONNX 模型失败")?
.map_err(DdddError::new)?
// .with_context(|| "从内存字节流解析 ONNX 模型失败")?
.into_optimized()
.with_context(|| "优化 Tract 模型图失败")?
.into_runnable()
.with_context(|| "构建可运行 Tract 实例失败")?;
.map_err(DdddError::new)?
// .with_context(|| "优化 Tract 模型图失败")?
.into_runnable().map_err(DdddError::new)?;
// .with_context(|| "构建可运行 Tract 实例失败")?;
Ok(Self { session })
}
@@ -90,13 +92,13 @@ impl ModelLoader {
// 使用私有辅助函数统一处理,消除重复代码
let inputs = self.resolve_tensors(
model
.input_outlets()
.map_err(|e| DdddError::InternalError(format!("获取输入节点失败: {:?}", e)))?,
.input_outlets().map_err(DdddError::new)?
// .map_err(|e| DdddError::InternalError(format!("获取输入节点失败: {:?}", e)))?,
)?;
let outputs = self.resolve_tensors(
model
.output_outlets()
.map_err(|e| DdddError::InternalError(format!("获取输出节点失败: {:?}", e)))?,
.output_outlets().map_err(DdddError::new)?
// .map_err(|e| DdddError::InternalError(format!("获取输出节点失败: {:?}", e)))?,
)?;
Ok(ModelInfo {
@@ -113,9 +115,10 @@ impl ModelLoader {
outlets
.iter()
.map(|&outlet_id| {
let fact = model.outlet_fact(outlet_id).map_err(|e| {
DdddError::InternalError(format!("解析节点 Fact 失败: {:?}", e))
})?;
let fact = model.outlet_fact(outlet_id).map_err(DdddError::new)?;
// .map_err(|e| {
// DdddError::InternalError(format!("解析节点 Fact 失败: {:?}", e))
// })?;
let shape = self.resolve_shape(&fact.shape)?;
let node_name = model.node(outlet_id.node).name.clone();

View File

@@ -1,6 +1,7 @@
use crate::loader::ModelLoader;
use anyhow::Context;
use ddddocr_core::error::{DdddError, Result};
use ddddocr_core::error::{DdddError, Result, TensorErrorReason};
use ddddocr_core::utils::normalize_ocr_logits;
use ddddocr_core::{InferenceEngine, ModelMetadata, OcrEngine, OcrOutput};
use ndarray::s;
use std::path::Path;
@@ -47,7 +48,12 @@ impl InferenceEngine for OcrSession {
let mut result = self
.session
.run(tvec!(tensor.into()))
.context("执行模型推理失败")?;
.map_err(|_| {
DdddError::Inference(TensorErrorReason::EngineError(
"执行模型推理失败".to_string(),
))
})?;
// .context("执行模型推理失败")?;
println!("模型输出原始数据: {:?}", result);
// Ok(result.swap_remove(0).into_tensor())
let raw_tensor = result.swap_remove(0).into_tensor();
@@ -56,16 +62,23 @@ impl InferenceEngine for OcrSession {
DatumType::I64 => {
let array_d = raw_tensor
.into_array::<i64>()
.context("Tract 无法获取 i64 内存视图")?;
.map_err(|_| {
DdddError::Inference(TensorErrorReason::EngineError(
"Tract 无法获取 i64 内存视图".to_string(),
))
})?;
// .context("Tract 无法获取 i64 内存视图")?;
// 🌟 提前提取真实维度
let actual_shape = array_d.shape().to_vec();
// 转成标准的 Array1 传给 core
let array1 = array_d
.to_owned()
.into_dimensionality::<ndarray::Ix1>()
.map_err(|_| DdddError::DimensionMismatch {
expected: "1D 字符索引静态矩阵".to_string(),
actual: actual_shape,
.map_err(|_| {
DdddError::Inference(TensorErrorReason::TensorDimensionMismatch {
expected: "1D 字符索引静态矩阵".to_string(),
actual: actual_shape,
})
})?;
Ok(OcrOutput::Indices(array1))
}
@@ -74,51 +87,17 @@ impl InferenceEngine for OcrSession {
println!("模型输出shape数据: {:?}", shape);
let view = raw_tensor
.to_array_view::<f32>()
.context("Tract 无法获取 f32 内存视图")?;
.map_err(|_| {
DdddError::Inference(TensorErrorReason::EngineError(
"Tract 无法获取 f32 内存视图".to_string(),
))
})?;
// 1. 极其纯粹的、无拷贝的多维 Shape 压扁清洗
let (steps, classes, data_dyn_view) = match shape.len() {
3 => {
if shape[1] == 1 {
// 形状: [Steps, 1, Classes] -> 你的原有逻辑
(shape[0], shape[2], view.into_dyn())
} else if shape[0] == 1 {
// 形状: [1, Steps, Classes] -> 另一种常见导出格式
(shape[1], shape[2], view.into_dyn())
} else {
// 默认取第一个 batch: [Batch, Steps, Classes]
// 使用 slice 对应 Python 的 output[0, :, :]
let sliced = view.slice(s![0, .., ..]);
(shape[1], shape[2], sliced.into_dyn())
}
}
// 形状: [Steps, Classes] -> 已经剥离了 Batch 维度
2 => (shape[0], shape[1], view.into_dyn()),
// 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑)
// 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
1 => (1, shape[0], view.into_dyn()),
_ => {
return Err(DdddError::DimensionMismatch {
expected: "1D, 2D, or 3D OCR Logits".to_string(),
actual: shape.to_vec(),
});
}
};
// 转换为标准的 2D 静态矩阵 [Steps, Classes]
let matrix_cow = data_dyn_view
.to_shape(ndarray::Ix2(steps, classes))
.map_err(|_| DdddError::DimensionMismatch {
expected: format!("无法将形状调整为 [{}, {}]", steps, classes),
actual: shape.to_vec(),
})?
.to_owned(); // 转换为 Owned断开与 tract 内存生命周期的绑定,方便传递给 core
Ok(OcrOutput::Logits(matrix_cow))
normalize_ocr_logits(view, shape)
}
_ => Err(
// anyhow::anyhow!("不支持的模型输出数据类型: {:?}",raw_tensor.datum_type())
DdddError::UnknownOutputFormat,
DdddError::Inference(TensorErrorReason::UnknownOutputFormat)
),
}
}

View File

@@ -232,7 +232,8 @@ fn test_real_slide_comparison() {
fn test_resolve_shape_logic_direct() {
// 创建一个哑 ModelLoader 实例session 用不上,因为我们直接测私有方法)
let loader = ModelLoader::model_for_path(
"D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",
// "D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",
"D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_huashi666_i64.onnx",
)
.expect("建立测试模型图失败");
let md_info = &loader.model_info().context("信息");