Files
ddddocr-rs/ddddocr-core/src/utils/image_processor.rs
CNWei fe61895926 feat(core): 扩展 API、完善日志与代码文档规范
- 公开颜色过滤与字符集限制扩展 API,修复宏路径
- 库内打印替换为 tracing 日志,清理遗留废弃代码
- 补充核心逻辑单元测试与 crate 元数据
- 开启 missing_docs 并统一 rustfmt/clippy 格式
2026-08-06 19:58:54 +08:00

233 lines
7.5 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.

//! 图像处理算法OpenCV 风格的常用函数封装。
use image::{DynamicImage, GrayImage, ImageBuffer, Luma, imageops::FilterType};
use ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip};
use std::cmp::{max, min};
// 模拟openCV
/// 计算两个 HWC 数组的绝对差值(对应 cv2.absdiff
pub fn abs_diff(a: &ArrayView3<u8>, b: &ArrayView3<u8>) -> Array3<u8> {
// 利用 ndarray 的 map_collect生成差值的绝对值数组
// 或者直接使用 zip_mut_with 处理以减少内存分配
let mut diff = Array3::zeros(a.dim());
azip!((res in &mut diff, &va in a, &vb in b) {
*res = va.abs_diff(vb);
});
diff
}
/// RGB 到灰度转换
pub fn rgb_to_gray(rgb: ArrayView3<u8>) -> Array2<u8> {
let (h, w, _) = rgb.dim();
Array2::from_shape_fn((h, w), |(y, x)| {
let r = rgb[[y, x, 0]] as f32;
let g = rgb[[y, x, 1]] as f32;
let b = rgb[[y, x, 2]] as f32;
// 完全忽略 a只按权重计算
(0.299 * r + 0.587 * g + 0.114 * b) as u8
})
}
/// 查找匹配结果图中的最大值及其坐标(对应 cv2.minMaxLoc
pub fn min_max_loc(result_map: &ImageBuffer<Luma<f32>, Vec<f32>>) -> (f32, (u32, u32)) {
// 4. 找到最佳匹配位置 (对齐 cv2.minMaxLoc)
let mut max_val: f32 = -1.0;
let mut max_loc = (0, 0);
// 遍历匹配得分图
for (x, y, score) in result_map.enumerate_pixels() {
let s = score.0[0];
// 可以在此处加入你之前验证过的起始位过滤
// if x < 15 { continue; }
if s > max_val {
max_val = s;
max_loc = (x, y);
}
}
(max_val, max_loc)
}
/// 模拟 findContours返回面积最大的连通域标签找不到时返回 `None`。
pub fn find_contours_and_max(labelled: &ImageBuffer<Luma<u32>, Vec<u32>>) -> Option<u32> {
// 统计每个标签出现的频率(即面积)
let mut max_label = 0;
let mut max_area = 0;
let mut areas = std::collections::HashMap::new();
for pixel in labelled.pixels() {
let label = pixel.0[0];
if label == 0 {
continue;
} // 跳过背景
let count = areas.entry(label).or_insert(0);
*count += 1;
if *count > max_area {
max_area = *count;
max_label = label;
}
}
if max_label == 0 {
None
} else {
Some(max_label)
}
}
/// 计算指定连通域标签的外接矩形(对应 cv2.boundingRect返回 `(min_x, min_y, width, height)`。
pub fn bounding_rect(
labelled: &ImageBuffer<Luma<u32>, Vec<u32>>,
max_label: u32,
) -> (u32, u32, u32, u32) {
// 5. 计算最大区域的边界框 (对应 cv2.boundingRect)
let mut min_x = labelled.width();
let mut max_x = 0;
let mut min_y = labelled.height();
let mut max_y = 0;
for (x, y, pixel) in labelled.enumerate_pixels() {
if pixel.0[0] == max_label {
min_x = min(min_x, x);
max_x = max(max_x, x);
min_y = min(min_y, y);
max_y = max(max_y, y);
}
}
let w = max_x - min_x;
let h = max_y - min_y;
(min_x, min_y, w, h)
}
/// 根据左上角坐标与矩形长宽,计算其中央核心点坐标
#[inline]
pub fn calculate_center(top_left: (u32, u32), width: usize, height: usize) -> (i32, i32) {
let center_x = top_left.0 as i32 + (width as i32 / 2);
let center_y = top_left.1 as i32 + (height as i32 / 2);
(center_x, center_y)
}
/// 将 2D 灰度 ndarray 视图转换为灰度 ImageBuffer。
pub fn ndarray_to_luma8(array: ArrayView2<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>> {
let (height, width) = array.dim();
// 技巧:直接将已有的规整连续内存打平转换,或用 from_raw 包装
// 此处保留安全的一步转换,但用更内聚的迭代器或切片拷贝进行速度优化
let mut buffer = ImageBuffer::new(width as u32, height as u32);
for y in 0..height {
for x in 0..width {
buffer.put_pixel(x as u32, y as u32, Luma([array[[y, x]]]));
}
}
buffer
}
// =====================================================================
// 5. 核心高性能图像转换算法 (纯 Rust 编写)
// =====================================================================
/// RGB 像素转换为 OpenCV 风格的 HSV 值。
#[inline(always)]
pub fn rgb_to_opencv_hsv(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
// 1. 规避高昂的除法,直接转为 f32 进行比对
let r_f = r as f32;
let g_f = g as f32;
let b_f = b as f32;
let max = r_f.max(g_f).max(b_f);
let min = r_f.min(g_f).min(b_f);
let delta = max - min;
// 2. 计算 H (色调) - 移除负数取余陷阱,改用平铺分支
let h = if delta == 0.0 {
0.0
} else if max == r_f {
let mut diff = (g_f - b_f) / delta;
if diff < 0.0 {
diff += 6.0; // 规避 Rust f32 % 负数的行为
}
60.0 * diff
} else if max == g_f {
60.0 * (((b_f - r_f) / delta) + 2.0)
} else {
60.0 * (((r_f - g_f) / delta) + 4.0)
};
// OpenCV 的 H 量化H / 2
// 注意OpenCV 底层使用截断还是四舍五入与特定版本有关,
// 标准的 cvtColor 内部实现通常是: h * (180.0 / 360.0) -> h * 0.5
// 这里使用强转(截断)若单测对齐发现差1可改为 (h * 0.5 + 0.5) 或 round()
let h_opencv = (h * 0.5) as u8;
// 3. 计算 S (饱和度)
// OpenCV 公式: S = max == 0 ? 0 : 255 * delta / max
let s_opencv = if max == 0.0 {
0
} else {
((255.0 * delta) / max) as u8
};
// 4. 计算 V (明度)
let v_opencv = max as u8;
(h_opencv, s_opencv, v_opencv)
}
/// 将图像转换为灰度图L 模式)。
pub fn convert_to_grayscale(image: &DynamicImage) -> GrayImage {
// Rust utils 库的 to_luma8 会根据标准的亮度公式进行转换
image.to_luma8()
}
/// 按指定宽高调整图像尺寸。
pub fn resize_image(
image: &DynamicImage,
target_width: u32,
target_height: u32,
// resample 参数我们直接使用 FilterTypeLanczos3 是最接近 Python LANCZOS 的
) -> DynamicImage {
// image::imageops::resize 的最高层封装
// FilterType::Lanczos3 与 Python Pillow 的 Image.LANCZOS 算法完全对齐,缩放质量最高
image.resize_exact(target_width, target_height, FilterType::Lanczos3)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn abs_diff_absolutes() {
let a = Array3::from_shape_vec((1, 1, 3), vec![10, 200, 5]).unwrap();
let b = Array3::from_shape_vec((1, 1, 3), vec![200, 100, 5]).unwrap();
let d = abs_diff(&a.view(), &b.view());
assert_eq!(d[[0, 0, 0]], 190);
assert_eq!(d[[0, 0, 1]], 100);
assert_eq!(d[[0, 0, 2]], 0);
}
#[test]
fn rgb_to_gray_white_is_255() {
let rgb = Array3::from_shape_vec((1, 1, 3), vec![255, 255, 255]).unwrap();
assert_eq!(rgb_to_gray(rgb.view())[[0, 0]], 255);
}
#[test]
fn min_max_loc_finds_max() {
let buf = ImageBuffer::<Luma<f32>, Vec<f32>>::from_fn(3, 2, |x, y| {
Luma([if x == 2 && y == 1 { 0.9 } else { 0.1 }])
});
let (val, loc) = min_max_loc(&buf);
assert_eq!(val, 0.9);
assert_eq!(loc, (2, 1));
}
#[test]
fn calculate_center_midpoint() {
assert_eq!(calculate_center((10, 20), 6, 4), (13, 22));
}
#[test]
fn rgb_to_opencv_hsv_red() {
assert_eq!(rgb_to_opencv_hsv(255, 0, 0), (0, 255, 255));
}
}