refactor: 优化图像输入源设计,重构为零污染的 TryFromImage 特征

- 移除原有的 ImageInput 枚举,避免运行时匹配与所有权限制
- 引入自定义 TryFromImage 特征,专用于将不同来源安全转换为 ImageSource
- 优化错误处理新增 InvalidBase64Header 错误信息
- 迁移 load_image_from_input 到 image_helper.rs 为后续剥离到业务层做准备
This commit is contained in:
2026-07-16 14:13:56 +08:00
parent cd70748393
commit 4f6987f594
8 changed files with 284 additions and 167 deletions

View File

@@ -21,8 +21,12 @@ use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum DdddError { pub enum DdddError {
// 【新增】专门处理文件读取、路径不存在等原生 I/O 错误
#[error("系统网络或文件 I/O 异常: {0}")]
Io(#[from] std::io::Error),
#[error("图像预处理失败: {0}")] #[error("图像预处理失败: {0}")]
PreprocessError(String), PreprocessError(#[from] ImagePreprocessReason),
#[error("模型推理引擎内部发生异常: {0}")] #[error("模型推理引擎内部发生异常: {0}")]
EngineError(#[from] anyhow::Error), EngineError(#[from] anyhow::Error),
@@ -46,5 +50,27 @@ pub enum DdddError {
InternalError(String), InternalError(String),
} }
/// 专门服务于预处理的子错误枚举,保留全部底层上下文
#[derive(Error, Debug)]
pub enum ImagePreprocessReason {
#[error("图片加载或文件 I/O 失败: {0}")]
ImageIo(#[from] image::ImageError),
#[error("图片转矩阵矩阵(ndarray)失败: {0}")]
NdarrayError(#[from] ndarray::ShapeError),
#[error("Base64 解码失败: {0}")]
Base64(#[from] base64::DecodeError),
#[error("Base64 头部格式不正确,缺少 ';base64,' 分隔符")]
InvalidBase64Header,
#[error("不支持的通道数: {0}")]
UnsupportedChannels(usize),
#[error("其他预处理错误: {0}")]
Custom(String),
}
/// 统一用我们自己的 DdddError 包装 Result /// 统一用我们自己的 DdddError 包装 Result
pub type Result<T> = std::result::Result<T, DdddError>; pub type Result<T> = std::result::Result<T, DdddError>;

View File

@@ -1,4 +1,4 @@
use crate::utils::image_io::image_to_ndarray; use crate::utils::image_io::{image_to_ndarray,ColorMode};
use crate::utils::image_processor; use crate::utils::image_processor;
use crate::utils::image_processor::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray}; use crate::utils::image_processor::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
@@ -42,8 +42,8 @@ impl Slider {
background_image: &DynamicImage, background_image: &DynamicImage,
simple_target: bool, simple_target: bool,
) -> Result<SlideResult> { ) -> Result<SlideResult> {
let target_array = image_to_ndarray(target_image); let target_array = image_to_ndarray(target_image,ColorMode::RGB)?;
let background_array = image_to_ndarray(background_image); let background_array = image_to_ndarray(background_image,ColorMode::RGB)?;
self.perform_slide_match(target_array.view(), background_array.view(), simple_target) self.perform_slide_match(target_array.view(), background_array.view(), simple_target)
} }
@@ -55,8 +55,8 @@ impl Slider {
background_image: &DynamicImage, background_image: &DynamicImage,
) -> Result<SlideResult> { ) -> Result<SlideResult> {
// 1. 转换为 ndarray (HWC RGB) // 1. 转换为 ndarray (HWC RGB)
let target_array = image_to_ndarray(target_image); let target_array = image_to_ndarray(target_image,ColorMode::RGB)?;
let background_array = image_to_ndarray(background_image); let background_array = image_to_ndarray(background_image,ColorMode::RGB)?;
// 2. 执行比较逻辑 (对应 _perform_slide_comparison) // 2. 执行比较逻辑 (对应 _perform_slide_comparison)
self.perform_slide_comparison(target_array.view(), background_array.view()) self.perform_slide_comparison(target_array.view(), background_array.view())

View File

@@ -1,5 +1,8 @@
pub mod image_io; pub mod image_io;
pub mod image_processor; pub mod image_processor;
mod tensor_transform; mod tensor_transform;
mod image_helper;
// 对外统一暴露干净的 API 语义层 // 对外统一暴露干净的 API 语义层
pub use tensor_transform::normalize_ocr_logits; pub use tensor_transform::normalize_ocr_logits;
pub use image_io::{ColorMode};

View File

@@ -0,0 +1,102 @@
use crate::error::{DdddError, ImagePreprocessReason, Result};
use crate::utils::image_io::{base64_to_image, ndarray_to_pil_image};
use image::DynamicImage;
use ndarray::ArrayViewD;
use std::path::Path;
use std::path::PathBuf;
pub struct Base64<'a>(pub &'a str);
/// 专属图像输入源转换器
pub struct ImageSource {
inner: DynamicImage,
}
// 1. 将 into_inner 优化为 into_image符合 Rust 官方命名规范
impl ImageSource {
/// 消耗当前包装器,获取最终的 DynamicImage
pub fn into_image(self) -> DynamicImage {
self.inner
}
}
pub trait TryFromImage<T>: Sized {
// 唯一的转换入口,通过目标类型来调用
fn try_from_image(value: T) -> Result<Self>;
}
// 1. 本身是 DynamicImage
impl TryFromImage<DynamicImage> for ImageSource {
fn try_from_image(img: DynamicImage) -> Result<Self> {
Ok(Self { inner: img })
}
}
// 2.2 路径类型 A: &str (最常用)
impl<'a> TryFromImage<&'a str> for ImageSource {
fn try_from_image(path_or_b64: &'a str) -> Result<Self> {
// 1. 嗅探:如果包含 Base64 特征
if path_or_b64.starts_with("data:image/") && path_or_b64.contains(";base64,") {
// 提取出真正的 base64 数据部分
let (_, clean_b64) = path_or_b64.split_once(";base64,").ok_or_else(|| {
// 返回一个明确的、可读性极佳的格式错误
DdddError::PreprocessError(ImagePreprocessReason::InvalidBase64Header)
})?;
// 转换为 Base64 包装器,并复用其 TryFromImage 实现
Self::try_from_image(Base64(clean_b64))
} else {
// 2. 否则,老老实实当作本地路径打开
let img = image::open(path_or_b64).map_err(ImagePreprocessReason::from)?;
Ok(Self { inner: img })
}
}
}
// 2.3 路径类型 B: &Path (标准借用)
impl<'a> TryFromImage<&'a Path> for ImageSource {
fn try_from_image(path: &'a Path) -> Result<Self> {
let img = image::open(path).map_err(ImagePreprocessReason::from)?;
Ok(Self { inner: img })
}
}
// 2.4 路径类型 C: PathBuf / String (拥有所有权,透传给借用)
impl TryFromImage<PathBuf> for ImageSource {
fn try_from_image(path: PathBuf) -> Result<Self> {
Self::try_from_image(path.as_path())
}
}
impl TryFromImage<String> for ImageSource {
fn try_from_image(path_or_b64: String) -> Result<Self> {
Self::try_from_image(path_or_b64.as_str())
}
}
// 2. 支持带有生命周期的借用:直接支持 &[u8](不强绑生命周期到 ImageSource 结构体上!)
impl<'a> TryFromImage<&'a [u8]> for ImageSource {
fn try_from_image(bytes: &'a [u8]) -> Result<Self> {
let img = image::load_from_memory(bytes).map_err(ImagePreprocessReason::from)?;
Ok(Self { inner: img })
}
}
// 4. 完美支持 ndarray 的借用 ArrayViewD
impl<'a> TryFromImage<ArrayViewD<'a, u8>> for ImageSource {
fn try_from_image(array: ArrayViewD<'a, u8>) -> Result<Self> {
let img = ndarray_to_pil_image(array)?;
Ok(Self { inner: img })
}
}
impl<'a> TryFromImage<Base64<'a>> for ImageSource {
fn try_from_image(b64_str: Base64<'a>) -> Result<Self> {
let img = base64_to_image(b64_str.0)?;
Ok(Self { inner: img })
}
}
/// 模拟 Python 的 load_image_from_input
#[allow(dead_code)]
pub fn load_image_from_input<I>(input: I) -> Result<DynamicImage>
where
ImageSource: TryFromImage<I>,
{
let img = ImageSource::try_from_image(input)?.into_image();
Ok(img)
}

View File

@@ -1,43 +1,18 @@
use anyhow::{Context, Result, anyhow, bail}; use crate::error::{DdddError, ImagePreprocessReason, Result};
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, Luma, Rgb, RgbImage, Rgba}; use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb, Rgba};
use ndarray::{Array3, ArrayD, ArrayViewD};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use ndarray::{Array3, ArrayD, ArrayViewD};
#[derive(Debug)] #[derive(Debug)]
pub enum ColorMode { pub enum ColorMode {
RGB, RGB,
RGBA, RGBA,
L, L,
} }
/// 定义支持的输入类型枚举 /// 将base64编码的图片转换为 DynamicImage
pub enum ImageInput { pub fn base64_to_image(b64_str: &str) -> Result<DynamicImage> {
Bytes(Vec<u8>),
Array(ArrayD<u8>), // 对应 numpy 数组
Path(PathBuf),
Base64(String),
DynamicImage(DynamicImage),
}
/// 模拟 Python 的 load_image_from_input
#[allow(dead_code)]
pub fn load_image_from_input(img_input: ImageInput) -> Result<DynamicImage> {
match img_input {
// 2. 处理字节流 (Bytes)
ImageInput::Bytes(bytes) => {
image::load_from_memory(&bytes).context("Failed to load utils from bytes")
}
// 1. 已经是 DynamicImage
ImageInput::DynamicImage(i) => Ok(i),
// 5. 处理 ndarray (Numpy-like)
// 假设输入是 HWC 格式的 Array3<u8>
ImageInput::Array(a) => numpy_to_pil_image(a.view()),
// 4. 处理 Base64 字符串
ImageInput::Base64(b) => base64_to_image(&b),
// 3. 处理文件路径 (Path)
ImageInput::Path(p) => image::open(p).context("Failed to open utils from path"),
}
}
fn base64_to_image(b64_str: &str) -> Result<DynamicImage> {
// 过滤掉可能存在的 base64 前缀,例如 "data:utils/png;base64," // 过滤掉可能存在的 base64 前缀,例如 "data:utils/png;base64,"
let clean_b64 = if let Some(pos) = b64_str.find(",") { let clean_b64 = if let Some(pos) = b64_str.find(",") {
&b64_str[pos + 1..] &b64_str[pos + 1..]
@@ -47,18 +22,18 @@ fn base64_to_image(b64_str: &str) -> Result<DynamicImage> {
let bytes = general_purpose::STANDARD let bytes = general_purpose::STANDARD
.decode(clean_b64.trim()) .decode(clean_b64.trim())
.map_err(|e| anyhow!("Base64 decode error: {}", e))?; .map_err(ImagePreprocessReason::from)?;
image::load_from_memory(&bytes).context("Failed to load utils from decoded base64") let img = image::load_from_memory(&bytes).map_err(ImagePreprocessReason::from)?;
Ok(img)
} }
/// 读取图片文件并转换为 base64 编码字符串 /// 读取图片文件并转换为 base64 编码字符串
/// 对应 Python 版 get_img_base64 // 对应 Python 版 get_img_base64
pub fn get_img_base64<P: AsRef<Path>>(image_path: P) -> Result<String> { pub fn img_base64<P: AsRef<Path>>(image_path: P) -> Result<String> {
// 1. 读取文件原始字节流 // 1. 读取文件原始字节流
// 使用 AsRef<Path> 泛型可以让函数同时支持 String, &str, PathBuf 等类型 // 使用 AsRef<Path> 泛型可以让函数同时支持 String, &str, PathBuf 等类型
let image_data = fs::read(&image_path) let image_data = fs::read(&image_path)?;
.with_context(|| format!("Failed to read utils file: {:?}", image_path.as_ref()))?;
// 2. 进行 Base64 编码 // 2. 进行 Base64 编码
// 使用 STANDARD 引擎对齐 Python 的 base64.b64encode // 使用 STANDARD 引擎对齐 Python 的 base64.b64encode
@@ -67,55 +42,50 @@ pub fn get_img_base64<P: AsRef<Path>>(image_path: P) -> Result<String> {
Ok(b64_string) Ok(b64_string)
} }
/// 封装数组转图像的逻辑,对齐 Python 版 _numpy_to_pil_image /// 封装数组转图像的逻辑,
fn numpy_to_pil_image(array: ArrayViewD<u8>) -> Result<DynamicImage> { // 对应 Python 版 _numpy_to_pil_image
pub(crate) fn ndarray_to_pil_image(array: ArrayViewD<u8>) -> Result<DynamicImage> {
let shape = array.shape(); let shape = array.shape();
let dim = shape.len(); let dim = shape.len();
// 1. 确保数据在内存中是连续的 (C order / Standard Layout) // 1. 确保数据在内存中是连续的 (C order / Standard Layout)
// 如果 arr 是经过切片或转置的,这一步会进行必要的内存拷贝 // 如果 arr 是经过切片或转置的,这一步会进行必要的内存拷贝
let standard = array.as_standard_layout(); // let standard = array.as_standard_layout();
let (raw_data, _offset) = standard.to_owned().into_raw_vec_and_offset(); // let (raw_data, _offset) = standard.to_owned().into_raw_vec_and_offset();
match dim { let color_mode = match dim {
// 对应 Python: len(array.shape) == 2 (灰度图 H, W) // 对应 Python: len(array.shape) == 2 (灰度图 H, W)
2 => { 2 => ColorMode::L,
let (h, w) = (shape[0], shape[1]);
ImageBuffer::<Luma<u8>, _>::from_raw(w as u32, h as u32, raw_data)
.map(DynamicImage::ImageLuma8)
.ok_or_else(|| anyhow!("Failed to create Luma utils from 2D array"))
}
// 对应 Python: len(array.shape) == 3 (H, W, C) // 对应 Python: len(array.shape) == 3 (H, W, C)
3 => { 3 => {
let (h, w, c) = (shape[0], shape[1], shape[2]); let (_h, _w, c) = (shape[0], shape[1], shape[2]);
match c { match c {
// 对应 Python: array.shape[2] == 1 (单通道 H, W, 1) // 对应 Python: array.shape[2] == 1 (单通道 H, W, 1)
1 => ImageBuffer::<Luma<u8>, _>::from_raw(w as u32, h as u32, raw_data) 1 => ColorMode::L,
.map(DynamicImage::ImageLuma8),
// 对应 Python: array.shape[2] == 3 (RGB H, W, 3) // 对应 Python: array.shape[2] == 3 (RGB H, W, 3)
3 => ImageBuffer::<Rgb<u8>, _>::from_raw(w as u32, h as u32, raw_data) 3 => ColorMode::RGB,
.map(DynamicImage::ImageRgb8),
// 对应 Python: array.shape[2] == 4 (RGBA H, W, 4) // 对应 Python: array.shape[2] == 4 (RGBA H, W, 4)
4 => ImageBuffer::<Rgba<u8>, _>::from_raw(w as u32, h as u32, raw_data) 4 => ColorMode::RGBA,
.map(DynamicImage::ImageRgba8),
_ => { _ => {
return Err(anyhow!("不支持的通道数: {}", c)); return Err(DdddError::PreprocessError(
ImagePreprocessReason::UnsupportedChannels(c),
));
} }
} }
.ok_or_else(|| anyhow!("转换彩色图失败"))
} }
_ => {
_ => Err(anyhow!("不支持的数组维度: {},仅支持 2D 或 3D", dim)), return Err(DdddError::DimensionMismatch {
} expected: "2D (H,W) 或 3D (H,W,C)".to_string(),
actual: shape.to_vec(),
});
}
};
from_ndarray(array, color_mode)
} }
/// 对应 Python 的 png_rgba_black_preprocess /// 处理PNG图片的RGBA透明背景将透明部分设置为白色背景
/// 将带有透明通道的图片转换为白色背景的 RGB 图片 // 对应 Python 的 png_rgba_black_preprocess
pub fn png_rgba_white_preprocess(img: &DynamicImage) -> DynamicImage { pub fn png_rgba_white_preprocess(img: &DynamicImage) -> DynamicImage {
// 1. 检查是否包含透明通道,如果没有,直接克隆并返回 // 1. 检查是否包含透明通道,如果没有,直接克隆并返回
if !img.color().has_alpha() { if !img.color().has_alpha() {
@@ -161,104 +131,71 @@ pub fn png_rgba_white_preprocess(img: &DynamicImage) -> DynamicImage {
DynamicImage::ImageRgb8(background) DynamicImage::ImageRgb8(background)
} }
pub fn image_to_numpy(image: &DynamicImage, mode: ColorMode) -> Result<Array3<u8>> { /// 将 DynamicImage 转换为 array 数组
pub fn image_to_ndarray(image: &DynamicImage, mode: ColorMode) -> Result<Array3<u8>> {
// 1. 模式转换 (对应 utils.convert(target_mode)),此函数在时保留看后续优化是否需要替代image_to_ndarray // 1. 模式转换 (对应 utils.convert(target_mode)),此函数在时保留看后续优化是否需要替代image_to_ndarray
// Rust utils 库通过 to_rgb8, to_luma8 等方法实现转换 // Rust utils 库通过 to_rgb8, to_luma8 等方法实现转换
let (width, height) = image.dimensions(); let (width, height) = image.dimensions();
let (channels, raw) = match mode { let (channels, raw) = match mode {
ColorMode::RGB => (3, image.to_rgb8().into_raw()),
ColorMode::L => (1, image.to_luma8().into_raw()), ColorMode::L => (1, image.to_luma8().into_raw()),
ColorMode::RGB => (3, image.to_rgb8().into_raw()),
ColorMode::RGBA => (4, image.to_rgba8().into_raw()), ColorMode::RGBA => (4, image.to_rgba8().into_raw()),
}; };
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(|e| anyhow!("Failed to build ndarray: {}", e)) .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::DimensionMismatch {
expected: "At least 2D array [H, W]".to_string(),
actual: shape.to_vec(),
});
}
from_ndarray(array, mode)
} }
pub fn numpy_to_image(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage> { fn from_ndarray(array: ArrayViewD<u8>, mode: ColorMode) -> Result<DynamicImage> {
let shape = array.shape(); let shape = array.shape();
// 1. 基础维度检查 (必须是 H, W, C 三维数组)
if shape.len() != 3 {
bail!("Expected a 3D array (H, W, C), but got {}D", shape.len());
}
// 映射ndarray 的 shape 默认是 [Height, Width, (Channels)]
// image 库的 from_raw 接收 (width, height)
let height = shape[0] as u32; let height = shape[0] as u32;
let width = shape[1] as u32; let width = shape[1] as u32;
let channels = shape[2];
// 2. 检查通道数是否与模式匹配 // 1. 确保数据在内存中是连续的 (C order)
let expected_channels = match mode {
ColorMode::L => 1,
ColorMode::RGB => 3,
ColorMode::RGBA => 4,
};
if channels != expected_channels {
bail!(
"Mode {:?} expects {} channels, but array has {}",
mode,
expected_channels,
channels
);
}
// 确保数据连续性 (C-order)
let standard = array.as_standard_layout(); let standard = array.as_standard_layout();
let (raw_data, _) = standard.to_owned().into_raw_vec_and_offset(); let (raw_data, _) = standard.to_owned().into_raw_vec_and_offset();
let raw_len = raw_data.len();
// 2. 重新解释内存并构建 ImageBuffer
match mode { match mode {
ColorMode::L => ImageBuffer::<Luma<u8>, _>::from_raw(width, height, raw_data) ColorMode::L => ImageBuffer::<Luma<u8>, _>::from_raw(width, height, raw_data)
.map(DynamicImage::ImageLuma8), .map(DynamicImage::ImageLuma8).ok_or_else(|| {
DdddError::PreprocessError(ImagePreprocessReason::Custom(format!(
"Failed to construct Luma image: buffer size {} does not match expected {} ({}x{}x1)",
raw_len, width * height * 1, width, height
)))
}),
ColorMode::RGB => ImageBuffer::<Rgb<u8>, _>::from_raw(width, height, raw_data) ColorMode::RGB => ImageBuffer::<Rgb<u8>, _>::from_raw(width, height, raw_data)
.map(DynamicImage::ImageRgb8), .map(DynamicImage::ImageRgb8).ok_or_else(|| {
DdddError::PreprocessError(ImagePreprocessReason::Custom(format!(
"Failed to construct RGB image: buffer size {} does not match expected {} ({}x{}x3)",
raw_len, width * height * 3, width, height
)))
}),
ColorMode::RGBA => ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, raw_data) ColorMode::RGBA => ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, raw_data)
.map(DynamicImage::ImageRgba8), .map(DynamicImage::ImageRgba8).ok_or_else(|| {
DdddError::PreprocessError(ImagePreprocessReason::Custom(format!(
"Failed to construct RGBA image: buffer size {} does not match expected {} ({}x{}x4)",
raw_len, width * height * 4, width, height
)))
}),
} }
.ok_or_else(|| anyhow!("Failed to construct ImageBuffer. Buffer size might be incorrect."))
}
pub fn image_to_ndarray(img: &DynamicImage) -> Array3<u8> {
let (width, height) = img.dimensions();
// 1. 强制转为 RGB8 (丢弃 Alpha 通道,与 Python 的 target_mode='RGB' 对齐)
let rgb_img = img.to_rgb8();
// 2. 获取原始像素数据
let raw_data = rgb_img.into_raw();
// 3. 构造数组 (通道数改为 3)
Array3::from_shape_vec((height as usize, width as usize, 3), raw_data)
.expect("Failed to construct ndarray from utils") // 建议显式报错,而不是返回全黑图
} }
#[allow(dead_code)]
fn save_rust_result(result: &ImageBuffer<Luma<f32>, Vec<f32>>, filename: &str) {
let (width, height) = result.dimensions();
// 1. 寻找最值进行归一化
let mut max_val = f32::MIN;
let mut min_val = f32::MAX;
for p in result.pixels() {
if p.0[0] > max_val {
max_val = p.0[0];
}
if p.0[0] < min_val {
min_val = p.0[0];
}
}
// 2. 创建 8 位灰度图
let mut out_buf = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let val = result.get_pixel(x, y).0[0];
let normalized = if max_val > min_val {
((val - min_val) / (max_val - min_val) * 255.0) as u8
} else {
0u8
};
out_buf.put_pixel(x, y, Luma([normalized]));
}
}
// 3. 保存
DynamicImage::ImageLuma8(out_buf).save(filename).unwrap();
println!("Rust 结果热力图已保存至: {}", filename);
}

View File

@@ -1,6 +1,6 @@
use image::{DynamicImage, GrayImage, ImageBuffer, Luma, imageops::FilterType}; use image::{imageops::FilterType, DynamicImage, GrayImage, ImageBuffer, Luma};
use ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip}; use ndarray::{azip, Array2, Array3, ArrayView2, ArrayView3};
use std::cmp::{max, min}; use std::cmp::{max, min};
// 模拟openCV // 模拟openCV
@@ -174,8 +174,6 @@ pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
(h_opencv, s_opencv, v_opencv) (h_opencv, s_opencv, v_opencv)
} }
/// 对应 Python 的 convert_to_grayscale /// 对应 Python 的 convert_to_grayscale
/// 将图像转换为灰度图 (L模式) /// 将图像转换为灰度图 (L模式)
pub fn convert_to_grayscale(image: &DynamicImage) -> GrayImage { pub fn convert_to_grayscale(image: &DynamicImage) -> GrayImage {

View File

@@ -6,16 +6,22 @@ pub fn normalize_ocr_logits(array: ndarray::ArrayD<f32>, shape: &[usize]) -> Res
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 {
// 形状: [Steps, 1, Classes]
(shape[0], shape[2], array) (shape[0], shape[2], array)
} else if shape[0] == 1 { } else if shape[0] == 1 {
// 形状: [1, Steps, Classes]
(shape[1], shape[2], array) (shape[1], shape[2], array)
} else { } else {
// 默认取第一个 batch: [Batch, Steps, Classes]
// 使用 ndarray 的 s! 宏,对应 Python 的 output[0, :, :] // 使用 ndarray 的 s! 宏,对应 Python 的 output[0, :, :]
let sliced = array.slice_move(s![0, .., ..]); let sliced = array.slice_move(s![0, .., ..]);
(shape[1], shape[2], sliced.into_dyn()) (shape[1], shape[2], sliced.into_dyn())
} }
} }
// 形状: [Steps, Classes]
2 => (shape[0], shape[1], array), 2 => (shape[0], shape[1], array),
// 形状: [Classes] -> 单字符输出(对应 Python 的 ndim == 0 保护逻辑)
// 我们把它虚构成一个 [1, Classes] 的 2D 矩阵来复用后面的 argmax 逻辑
1 => (1, shape[0], array), 1 => (1, shape[0], array),
_ => { _ => {
return Err(DdddError::DimensionMismatch { return Err(DdddError::DimensionMismatch {

View File

@@ -1,10 +1,10 @@
use anyhow::Context;
use ddddocr_core::det::DetectionResult; use ddddocr_core::det::DetectionResult;
use ddddocr_core::{DetBuilder, Detector, ModelMetadata, Ocr, Slider}; // 假设你的包名是这个 use ddddocr_core::{DetBuilder, Detector, ModelMetadata, Ocr, Slider}; // 假设你的包名是这个
use ddddocr_tract::{DetSession,OcrSession}; use ddddocr_tract::{DetSession, OcrSession};
use image::{DynamicImage, Rgb}; use image::{DynamicImage, ImageBuffer, Luma, Rgb};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use anyhow::Context;
use tract_onnx::prelude::{ShapeFact, TDim}; use tract_onnx::prelude::{ShapeFact, TDim};
mod char_slice; mod char_slice;
@@ -67,7 +67,40 @@ fn save_debug_image(
img.save(output_path)?; img.save(output_path)?;
Ok(()) Ok(())
} }
#[allow(dead_code)]
fn save_rust_result(result: &ImageBuffer<Luma<f32>, Vec<f32>>, filename: &str) {
let (width, height) = result.dimensions();
// 1. 寻找最值进行归一化
let mut max_val = f32::MIN;
let mut min_val = f32::MAX;
for p in result.pixels() {
if p.0[0] > max_val {
max_val = p.0[0];
}
if p.0[0] < min_val {
min_val = p.0[0];
}
}
// 2. 创建 8 位灰度图
let mut out_buf = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let val = result.get_pixel(x, y).0[0];
let normalized = if max_val > min_val {
((val - min_val) / (max_val - min_val) * 255.0) as u8
} else {
0u8
};
out_buf.put_pixel(x, y, Luma([normalized]));
}
}
// 3. 保存
DynamicImage::ImageLuma8(out_buf).save(filename).unwrap();
println!("Rust 结果热力图已保存至: {}", filename);
}
#[test] #[test]
fn test_full_classification() { fn test_full_classification() {
// 1. 初始化模型 // 1. 初始化模型
@@ -84,7 +117,8 @@ fn test_full_classification() {
.expect("模型加载失败"); .expect("模型加载失败");
// 2. 加载测试图片 // 2. 加载测试图片
let img = image::open("D:/CNWei/CNW/Rust/ddddocr-rs/samples/code2.png").expect("测试图片不存在"); let img =
image::open("D:/CNWei/CNW/Rust/ddddocr-rs/samples/code2.png").expect("测试图片不存在");
// 3. 执行识别 // 3. 执行识别
let result = Ocr::new(&ocr) let result = Ocr::new(&ocr)
@@ -117,7 +151,11 @@ fn test_det_load() -> anyhow::Result<()> {
println!("未检测到任何目标。"); println!("未检测到任何目标。");
} else { } else {
// 如果 save_debug_image 报错,记得去把它的入参类型和内部访问也改为 DetectionResult // 如果 save_debug_image 报错,记得去把它的入参类型和内部访问也改为 DetectionResult
save_debug_image(&img, &bboxes, "D:/CNWei/CNW/Rust/ddddocr-rs/samples/result.jpg")?; save_debug_image(
&img,
&bboxes,
"D:/CNWei/CNW/Rust/ddddocr-rs/samples/result.jpg",
)?;
for (i, bbox) in bboxes.iter().enumerate() { for (i, bbox) in bboxes.iter().enumerate() {
// 【修改点 3】将原来的 bbox[0].. 索引访问改为结构体字段访问 // 【修改点 3】将原来的 bbox[0].. 索引访问改为结构体字段访问
@@ -133,8 +171,10 @@ fn test_real_slide_match() {
// 1. 加载你准备好的测试图 // 1. 加载你准备好的测试图
// 假设图片放在项目根目录下的 assets 文件夹 // 假设图片放在项目根目录下的 assets 文件夹
let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/hua.png").expect("请确保 samples/hua.png 存在"); let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/hua.png")
let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/huatu.png").expect("请确保 samples/huatu.png 存在"); .expect("请确保 samples/hua.png 存在");
let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/huatu.png")
.expect("请确保 samples/huatu.png 存在");
// 2. 执行匹配 // 2. 执行匹配
// 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false // 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false
@@ -162,8 +202,10 @@ fn test_real_slide_comparison() {
// 1. 加载你准备好的测试图 // 1. 加载你准备好的测试图
// 假设图片放在项目根目录下的 assets 文件夹 // 假设图片放在项目根目录下的 assets 文件夹
let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/ken.jpg").expect("请确保 samples/ken.jpg 存在"); let target_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/ken.jpg")
let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/kenyuan.jpg").expect("请确保 samples/kenyuan.jpg 存在"); .expect("请确保 samples/ken.jpg 存在");
let bg_img = load_image("D:/CNWei/CNW/Rust/ddddocr-rs/samples/kenyuan.jpg")
.expect("请确保 samples/kenyuan.jpg 存在");
// 2. 执行匹配 // 2. 执行匹配
// 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false // 如果是那种带有明显阴影边缘的复杂滑块,建议 simple_target 传 false
@@ -189,7 +231,10 @@ fn test_real_slide_comparison() {
#[test] #[test]
fn test_resolve_shape_logic_direct() { fn test_resolve_shape_logic_direct() {
// 创建一个哑 ModelLoader 实例session 用不上,因为我们直接测私有方法) // 创建一个哑 ModelLoader 实例session 用不上,因为我们直接测私有方法)
let loader = ModelLoader::model_for_path("D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",).expect("建立测试模型图失败"); let loader = ModelLoader::model_for_path(
let md_info=&loader.model_info().context("信息"); "D:\\CNWei\\CNW\\Rust\\ddddocr-rs\\models\\common_sml2h3_f32.onnx",
println!("md_info: {:?}",md_info); )
} .expect("建立测试模型图失败");
let md_info = &loader.model_info().context("信息");
println!("md_info: {:?}", md_info);
}