refactor: 优化 slide_model.rs
- 新增 cv2.rs 模拟 opencv
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
use crate::cv2::{min_max_loc, rgb_to_gray, ndarray_to_luma8, abs_diff};
|
||||
use crate::image_io::image_to_ndarray;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use image::{DynamicImage, GenericImageView};
|
||||
use tract_onnx::prelude::tract_ndarray::{Array2, Array3, ArrayView2, ArrayView3, Axis, s};
|
||||
use imageproc::template_matching::{match_template, MatchTemplateMethod};
|
||||
use image::{ImageBuffer, Luma};
|
||||
use crate::image_io::image_to_ndarray;
|
||||
use crate::cv2::rgb_to_gray;
|
||||
use imageproc::edges::canny;
|
||||
use imageproc::distance_transform::Norm;
|
||||
use imageproc::edges::canny;
|
||||
use imageproc::morphology::{close, open};
|
||||
use imageproc::region_labelling::{connected_components, Connectivity};
|
||||
use imageproc::region_labelling::{Connectivity, connected_components};
|
||||
use imageproc::template_matching::{MatchTemplateMethod, match_template};
|
||||
use std::cmp::{max, min};
|
||||
use imageproc::contrast::{threshold, ThresholdType};
|
||||
use tract_onnx::prelude::tract_ndarray::{Array2, Array3, ArrayView2, ArrayView3, Axis, s};
|
||||
|
||||
pub struct SlideResult {
|
||||
pub target: [i32; 2],
|
||||
@@ -28,26 +29,26 @@ impl Slide {
|
||||
/// 对应 Python: slide_match
|
||||
pub fn slide_match(
|
||||
&self,
|
||||
target_pil: &DynamicImage,
|
||||
background_pil: &DynamicImage,
|
||||
target_image: &DynamicImage,
|
||||
background_image: &DynamicImage,
|
||||
simple_target: bool,
|
||||
) -> Result<SlideResult> {
|
||||
let target_array = image_to_ndarray(target_pil);
|
||||
let background_array = image_to_ndarray(background_pil);
|
||||
let target_array = image_to_ndarray(target_image);
|
||||
let background_array = image_to_ndarray(background_image);
|
||||
|
||||
self.perform_slide_match(target_array.view(), background_array.view(),simple_target)
|
||||
self.perform_slide_match(target_array.view(), background_array.view(), simple_target)
|
||||
.map_err(|e| anyhow!("滑块匹配失败: {}", e))
|
||||
}
|
||||
/// 对应 Python: slide_comparison
|
||||
/// 用于比较带坑位的图片与原始背景图,定位差异点
|
||||
pub fn slide_comparison(
|
||||
&self,
|
||||
target_pil: &DynamicImage,
|
||||
background_pil: &DynamicImage,
|
||||
target_image: &DynamicImage,
|
||||
background_image: &DynamicImage,
|
||||
) -> Result<SlideResult> {
|
||||
// 1. 转换为 ndarray (HWC RGB)
|
||||
let target_array = image_to_ndarray(target_pil);
|
||||
let background_array = image_to_ndarray(background_pil);
|
||||
let target_array = image_to_ndarray(target_image);
|
||||
let background_array = image_to_ndarray(background_image);
|
||||
|
||||
// 2. 执行比较逻辑 (对应 _perform_slide_comparison)
|
||||
self.perform_slide_comparison(target_array.view(), background_array.view())
|
||||
@@ -63,25 +64,32 @@ impl Slide {
|
||||
|
||||
// 1. 计算图像差异并灰度化 (对应 cv2.absdiff + cv2.cvtColor)
|
||||
// 使用 OpenCV 标准权重公式:0.299R + 0.587G + 0.114B
|
||||
let mut diff_buffer = ImageBuffer::new(w as u32, h as u32);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let r_diff = (target[[y, x, 0]] as i16 - background[[y, x, 0]] as i16).abs() as f32;
|
||||
let g_diff = (target[[y, x, 1]] as i16 - background[[y, x, 1]] as i16).abs() as f32;
|
||||
let b_diff = (target[[y, x, 2]] as i16 - background[[y, x, 2]] as i16).abs() as f32;
|
||||
// let mut diff_buffer = ImageBuffer::new(w as u32, h as u32);
|
||||
// for y in 0..h {
|
||||
// for x in 0..w {
|
||||
// let r_diff = (target[[y, x, 0]] as i16 - background[[y, x, 0]] as i16).abs() as f32;
|
||||
// let g_diff = (target[[y, x, 1]] as i16 - background[[y, x, 1]] as i16).abs() as f32;
|
||||
// let b_diff = (target[[y, x, 2]] as i16 - background[[y, x, 2]] as i16).abs() as f32;
|
||||
//
|
||||
// let gray_diff = (0.299 * r_diff + 0.587 * g_diff + 0.114 * b_diff) as u8;
|
||||
// diff_buffer.put_pixel(x as u32, y as u32, Luma([gray_diff]));
|
||||
// }
|
||||
// }
|
||||
// 1. 计算差异数组 (复用 cv2::absdiff)
|
||||
let diff_array = abs_diff(&target, &background);
|
||||
|
||||
let gray_diff = (0.299 * r_diff + 0.587 * g_diff + 0.114 * b_diff) as u8;
|
||||
diff_buffer.put_pixel(x as u32, y as u32, Luma([gray_diff]));
|
||||
}
|
||||
}
|
||||
// 2. 转换为灰度数组 (复用你的 cv2::rgb_to_gray)
|
||||
let gray_array = rgb_to_gray(diff_array.view());
|
||||
// 3. 转为 ImageBuffer 以使用 imageproc 的高级功能
|
||||
let gray_buffer = ndarray_to_luma8(gray_array.view());
|
||||
|
||||
// 2. 二值化 (对应 cv2.threshold(..., 30, 255, cv2.THRESH_BINARY))
|
||||
let mut binary = ImageBuffer::new(w as u32, h as u32);
|
||||
for (x, y, pixel) in diff_buffer.enumerate_pixels() {
|
||||
let val = if pixel.0[0] > 30 { 255u8 } else { 0u8 };
|
||||
binary.put_pixel(x, y, Luma([val]));
|
||||
}
|
||||
|
||||
// let mut binary = ImageBuffer::new(w as u32, h as u32);
|
||||
// for (x, y, pixel) in diff_buffer.enumerate_pixels() {
|
||||
// let val = if pixel.0[0] > 30 { 255u8 } else { 0u8 };
|
||||
// binary.put_pixel(x, y, Luma([val]));
|
||||
// }
|
||||
let binary = threshold(&gray_buffer, 30, ThresholdType::Binary);
|
||||
// 3. 形态学操作去噪 (对应 cv2.morphologyEx)
|
||||
// 闭运算 (Close): 先膨胀后腐蚀,用于填补缺口内的细小黑色空洞
|
||||
// 开运算 (Open): 先腐蚀后膨胀,用于消除背景中的白色噪点点
|
||||
@@ -102,7 +110,9 @@ impl Slide {
|
||||
|
||||
for pixel in labelled.pixels() {
|
||||
let label = pixel.0[0];
|
||||
if label == 0 { continue; } // 跳过背景
|
||||
if label == 0 {
|
||||
continue;
|
||||
} // 跳过背景
|
||||
let count = areas.entry(label).or_insert(0);
|
||||
*count += 1;
|
||||
if *count > max_area {
|
||||
@@ -112,7 +122,12 @@ impl Slide {
|
||||
}
|
||||
|
||||
if max_label == 0 {
|
||||
return Ok(SlideResult { target: [0, 0], target_x: 0, target_y: 0, confidence: 0.0 });
|
||||
return Ok(SlideResult {
|
||||
target: [0, 0],
|
||||
target_x: 0,
|
||||
target_y: 0,
|
||||
confidence: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 计算最大区域的边界框 (对应 cv2.boundingRect)
|
||||
@@ -174,32 +189,27 @@ impl Slide {
|
||||
background: ArrayView2<u8>,
|
||||
) -> Result<SlideResult> {
|
||||
// 1. 将 ndarray 转换为 imageproc 需要的 ImageBuffer (无拷贝或轻量转换)
|
||||
let (th, tw) = target.dim();
|
||||
let (bh, bw) = background.dim();
|
||||
|
||||
// let (bh, bw) = background.dim();
|
||||
|
||||
// 转换逻辑 (假设你已经有方法转回 ImageBuffer)
|
||||
let t_buf = self.ndarray_to_luma8(target);
|
||||
let b_buf = self.ndarray_to_luma8(background);
|
||||
t_buf.save("debug_rust_target.png").unwrap();
|
||||
|
||||
let t_buf = ndarray_to_luma8(target);
|
||||
let b_buf = ndarray_to_luma8(background);
|
||||
// t_buf.save("debug_rust_target.png").unwrap();
|
||||
|
||||
// 2. 调用 imageproc 的 NCC 算法 (等价于 cv2.TM_CCOEFF_NORMED)
|
||||
let result = match_template(&b_buf, &t_buf, MatchTemplateMethod::CrossCorrelationNormalized);
|
||||
// 模板匹配 (完全对齐 cv2.matchTemplate(..., cv2.TM_CCOEFF_NORMED))
|
||||
let result = match_template(
|
||||
&b_buf,
|
||||
&t_buf,
|
||||
MatchTemplateMethod::CrossCorrelationNormalized,
|
||||
);
|
||||
// save_rust_result(&result, "debug_rust_target2.png");
|
||||
// 3. 寻找最大值 (等价于 cv2.minMaxLoc)
|
||||
let mut max_val: f32 = -1.0;
|
||||
let mut max_loc = (0, 0);
|
||||
|
||||
for (x, y, score) in result.enumerate_pixels() {
|
||||
let s = score.0[0];
|
||||
// 这里的 x, y 是左上角坐标
|
||||
if s > max_val {
|
||||
max_val = s;
|
||||
max_loc = (x, y);
|
||||
}
|
||||
}
|
||||
let (max_val, max_loc) = min_max_loc(&result);
|
||||
|
||||
// 4. 计算中心点 (与 Python 逻辑完全一致)
|
||||
let (th, tw) = target.dim();
|
||||
let center_x = max_loc.0 as i32 + (tw as i32 / 2);
|
||||
let center_y = max_loc.1 as i32 + (th as i32 / 2);
|
||||
// println!("Rust Target Width (tw): {}", tw);
|
||||
@@ -212,17 +222,7 @@ impl Slide {
|
||||
confidence: max_val as f64,
|
||||
})
|
||||
}
|
||||
|
||||
fn ndarray_to_luma8(&self, array: ArrayView2<u8>) -> ImageBuffer<Luma<u8>, Vec<u8>> {
|
||||
let (height, width) = array.dim();
|
||||
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
|
||||
}
|
||||
|
||||
/// 对应 Python: _edge_based_match
|
||||
/// 基于边缘检测的滑块匹配 (对齐 Python _edge_based_match)
|
||||
pub fn edge_based_match(
|
||||
@@ -232,8 +232,8 @@ impl Slide {
|
||||
) -> Result<SlideResult> {
|
||||
// 1. 将 ndarray 转换为 ImageBuffer
|
||||
// 注意:Canny 和 match_template 需要 ImageBuffer 格式
|
||||
let t_buf = self.ndarray_to_luma8(target);
|
||||
let b_buf = self.ndarray_to_luma8(background);
|
||||
let t_buf = ndarray_to_luma8(target);
|
||||
let b_buf = ndarray_to_luma8(background);
|
||||
|
||||
// 2. 边缘检测 (完全对齐 cv2.Canny(50, 150))
|
||||
// 这步会生成黑底白线的二值化边缘图
|
||||
@@ -245,29 +245,14 @@ impl Slide {
|
||||
|
||||
// 3. 模板匹配 (完全对齐 cv2.matchTemplate(..., cv2.TM_CCOEFF_NORMED))
|
||||
// 在边缘图上计算归一化互相关系数
|
||||
let result_map = match_template(
|
||||
let result = match_template(
|
||||
&background_edges,
|
||||
&target_edges,
|
||||
MatchTemplateMethod::CrossCorrelationNormalized
|
||||
MatchTemplateMethod::CrossCorrelationNormalized,
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
let (max_val, max_loc) = min_max_loc(&result);
|
||||
// 5. 计算中心位置 (对齐 Python 逻辑)
|
||||
// target_w, target_h 来自输入数组的维度
|
||||
let (th, tw) = target.dim();
|
||||
@@ -287,4 +272,5 @@ impl Slide {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user