//! 图像处理算法: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, b: &ArrayView3) -> Array3 { // 利用 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) -> Array2 { 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, Vec>) -> (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, Vec>) -> Option { // 统计每个标签出现的频率(即面积) 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, Vec>, 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) -> ImageBuffer, Vec> { 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 参数我们直接使用 FilterType,Lanczos3 是最接近 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::, Vec>::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)); } }