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

@@ -0,0 +1,180 @@
use crate::error::{DdddError, ImagePreprocessReason, Result};
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
use ndarray::{Array3, ArrayViewD};
#[derive(Debug)]
pub enum ColorMode {
RGB,
RGBA,
L,
}
/// 封装数组转图像的逻辑,
// 对应 Python 版 _numpy_to_pil_image
pub fn ndarray_to_hwc_image(array: ArrayViewD<u8>) -> Result<DynamicImage> {
let shape = array.shape();
let dim = shape.len();
// 1. 确保数据在内存中是连续的 (C order / Standard Layout)
// 如果 arr 是经过切片或转置的,这一步会进行必要的内存拷贝
// let standard = array.as_standard_layout();
// let (raw_data, _offset) = standard.to_owned().into_raw_vec_and_offset();
let color_mode = match dim {
// 对应 Python: len(array.shape) == 2 (灰度图 H, W)
2 => ColorMode::L,
// 对应 Python: len(array.shape) == 3 (H, W, C)
3 => {
let (_h, _w, c) = (shape[0], shape[1], shape[2]);
match c {
// 对应 Python: array.shape[2] == 1 (单通道 H, W, 1)
1 => ColorMode::L,
// 对应 Python: array.shape[2] == 3 (RGB H, W, 3)
3 => ColorMode::RGB,
// 对应 Python: array.shape[2] == 4 (RGBA H, W, 4)
4 => ColorMode::RGBA,
_ => {
return Err(DdddError::Preprocess(
ImagePreprocessReason::UnsupportedChannels(c),
));
}
}
}
_ => {
return Err(DdddError::Preprocess(
ImagePreprocessReason::InvalidImageDimensions {
expected: "2D (H,W) 或 3D (H,W,C)".to_string(),
actual: shape.to_vec(),
},
));
}
};
from_ndarray(array, color_mode)
}
/// 处理PNG图片的RGBA透明背景将透明部分设置为白色背景
// 对应 Python 的 png_rgba_black_preprocess
pub fn png_rgba_white_preprocess(img: &DynamicImage) -> DynamicImage {
// 1. 检查是否包含透明通道,如果没有,直接克隆并返回
if !img.color().has_alpha() {
return DynamicImage::ImageRgb8(img.to_rgb8());
}
let (width, height) = img.dimensions();
// 2. 创建一个新的 RGB 图像缓冲,默认填充为白色 (255, 255, 255)
let mut background = ImageBuffer::from_pixel(width, height, Rgb([255u8, 255u8, 255u8]));
// 3. 获取原图的 RGBA 视图
let rgba_img = img.to_rgba8();
// 4. 遍历像素并手动进行 Alpha 混合
// 对应 Python 的 utils.paste(img, ..., mask=img)
// 使用 enumerate_pixels_mut 同时获取坐标和背景像素的可变引用,减少查找开销
for (x, y, bg_pixel) in background.enumerate_pixels_mut() {
// 安全性说明x, y 源自 background 尺寸,与 rgba_img 一致get_pixel 是安全的
let src_pixel = rgba_img.get_pixel(x, y);
let alpha_u8 = src_pixel[3];
match alpha_u8 {
// 情况 A完全不透明直接覆盖背景色
255 => {
bg_pixel.0 = [src_pixel[0], src_pixel[1], src_pixel[2]];
}
// 情况 B完全透明保持背景色白色无需操作
0 => {
continue;
}
// 情况 C半透明进行 Alpha 混合计算
_ => {
let alpha = alpha_u8 as f32 / 255.0;
let inv_alpha = 1.0 - alpha;
bg_pixel[0] = (src_pixel[0] as f32 * alpha + 255.0 * inv_alpha).round() as u8;
bg_pixel[1] = (src_pixel[1] as f32 * alpha + 255.0 * inv_alpha).round() as u8;
bg_pixel[2] = (src_pixel[2] as f32 * alpha + 255.0 * inv_alpha).round() as u8;
}
}
}
DynamicImage::ImageRgb8(background)
}
/// 将 DynamicImage 转换为 array 数组
pub fn image_to_ndarray(image: &DynamicImage, mode: ColorMode) -> Result<Array3<u8>> {
// 1. 模式转换 (对应 utils.convert(target_mode)),此函数在时保留看后续优化是否需要替代image_to_ndarray
// Rust utils 库通过 to_rgb8, to_luma8 等方法实现转换
let (width, height) = image.dimensions();
let (channels, raw) = match mode {
ColorMode::L => (1, image.to_luma8().into_raw()),
ColorMode::RGB => (3, image.to_rgb8().into_raw()),
ColorMode::RGBA => (4, image.to_rgba8().into_raw()),
};
let array = Array3::from_shape_vec((height as usize, width as usize, channels), raw)
.map_err(ImagePreprocessReason::from)?;
Ok(array)
}
/// 将 array 数组转换为 DynamicImage
pub fn ndarray_to_image(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage> {
let shape = array.shape();
// 基础边界检查:至少要有 H 和 W 两个维度
if shape.len() < 2 {
return Err(DdddError::Preprocess(
ImagePreprocessReason::InvalidImageDimensions {
expected: "至少为 2D array [H, W]".to_string(),
actual: shape.to_vec(),
},
));
}
from_ndarray(array, mode)
}
fn from_ndarray(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage> {
let shape = array.shape();
// 映射ndarray 的 shape 默认是 [Height, Width, (Channels)]
// image 库的 from_raw 接收 (width, height)
let height = shape[0] as u32;
let width = shape[1] as u32;
// 1. 确保数据在内存中是连续的 (C order)
let standard = array.as_standard_layout();
let (raw_data, _) = standard.to_owned().into_raw_vec_and_offset();
let raw_len = raw_data.len();
// 获取当前模式对应的通道数
let channels = match mode {
ColorMode::L => 1,
ColorMode::RGB => 3,
ColorMode::RGBA => 4,
};
let expected_len = (width * height) as usize * channels;
// 构造通用错误闭包,避免 match 分支中重复编写冗长的错误对象
let make_err = || {
DdddError::Preprocess(ImagePreprocessReason::BufferLengthMismatch {
expected: expected_len,
actual: raw_len,
width,
height,
channels,
})
};
// 2. 重新解释内存并构建 ImageBuffer
match mode {
ColorMode::L => ImageBuffer::<Luma<u8>, _>::from_raw(width, height, raw_data)
.map(DynamicImage::ImageLuma8)
.ok_or_else(make_err),
ColorMode::RGB => ImageBuffer::<Rgb<u8>, _>::from_raw(width, height, raw_data)
.map(DynamicImage::ImageRgb8)
.ok_or_else(make_err),
ColorMode::RGBA => ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, raw_data)
.map(DynamicImage::ImageRgba8)
.ok_or_else(make_err),
}
}