Files
ddddocr-rs/ddddocr-core/src/utils/image_convert.rs
CNWei 913ff4d884 refactor(errors): 重构错误处理,支持强类型匹配并剥离 base64 依赖
- 新增 Other变体以及构造函数new
- 剥离图像预处理中的 Base64 相关错误至业务层处理
- 引入强类型 `LogitsDimensionMismatch` 替代不便匹配的字符串错误
- 优化 `normalize_ocr_logits` 的转换流程,兼顾零拷贝性能与精细化报错
- 优化 全库错误处理
2026-07-17 20:08:32 +08:00

181 lines
6.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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),
}
}