refactor: 重构 core 包目录结构并消除旧版 mod.rs
- 优化 剥离 models,algo 层并平铺业务模块 - 重构 统一使用现代 filename.rs + 文件夹结构替代旧版 mod.rs
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
mod slide;
|
|
||||||
|
|
||||||
pub use slide::{SlideResult, Slider};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::models::det::executor::Detector;
|
use crate::det::executor::Detector;
|
||||||
// use ddddocr_tract::det::session::DetSession;
|
// use ddddocr_tract::det::session::DetSession;
|
||||||
use crate::DetEngine;
|
use crate::DetEngine;
|
||||||
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
mod algo;
|
pub mod det;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod models;
|
pub mod ocr;
|
||||||
|
mod slide;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub use crate::algo::{SlideResult, Slider};
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
pub use crate::models::det::{DetBuilder, DetectionResult, Detector};
|
|
||||||
pub use crate::models::ocr::{Ocr, OcrBuilder, OcrResult};
|
pub use crate::slide::{SlideResult, Slider};
|
||||||
pub use models::ocr::metadata::ModelMetadata;
|
pub use crate::det::{DetBuilder, DetectionResult, Detector};
|
||||||
|
pub use crate::ocr::{Ocr, OcrBuilder, OcrResult};
|
||||||
|
pub use ocr::metadata::ModelMetadata;
|
||||||
// DetSession
|
// DetSession
|
||||||
|
|
||||||
pub enum OcrOutput {
|
pub enum OcrOutput {
|
||||||
@@ -33,5 +35,3 @@ pub trait OcrEngine: InferenceEngine<Output = OcrOutput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait DetEngine: InferenceEngine<Output = DetOutput> {}
|
pub trait DetEngine: InferenceEngine<Output = DetOutput> {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod ocr;
|
|
||||||
pub mod det;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::models::ocr::executor::Ocr;
|
use crate::ocr::executor::Ocr;
|
||||||
// use ddddocr_tract::session::OcrSession;
|
// use ddddocr_tract::session::OcrSession;
|
||||||
use crate::models::ocr::color_filter::ColorFilter;
|
use crate::ocr::color_filter::ColorFilter;
|
||||||
use crate::models::ocr::token_filter::TokenFilter;
|
use crate::ocr::token_filter::TokenFilter;
|
||||||
use crate::OcrEngine;
|
use crate::OcrEngine;
|
||||||
|
|
||||||
pub struct OcrBuilder {
|
pub struct OcrBuilder {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::utils::image_proc::rgb_to_opencv_hsv;
|
use crate::utils::image_processor::rgb_to_opencv_hsv;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use image::{DynamicImage, ImageBuffer, Rgb};
|
use image::{DynamicImage, ImageBuffer, Rgb};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::models::ocr::metadata::Resize;
|
use crate::ocr::metadata::Resize;
|
||||||
|
|
||||||
use crate::models::ocr::color_filter::{HsvRange, apply_to_image};
|
use crate::ocr::color_filter::{HsvRange, apply_to_image};
|
||||||
// use ddddocr_tract::session::{ModelOutput, OcrSession};
|
// use ddddocr_tract::session::{ModelOutput, OcrSession};
|
||||||
use crate::utils::image_io::png_rgba_white_preprocess;
|
use crate::utils::image_io::png_rgba_white_preprocess;
|
||||||
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
use crate::utils::image_processor::{convert_to_grayscale, resize_image};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::utils::image_proc;
|
|
||||||
use crate::utils::image_proc::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray};
|
|
||||||
use crate::utils::image_io::image_to_ndarray;
|
use crate::utils::image_io::image_to_ndarray;
|
||||||
|
use crate::utils::image_processor;
|
||||||
|
use crate::utils::image_processor::{abs_diff, min_max_loc, ndarray_to_luma8, rgb_to_gray};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use image::Luma;
|
use image::Luma;
|
||||||
@@ -10,8 +10,8 @@ use imageproc::edges::canny;
|
|||||||
use imageproc::morphology::{close, open};
|
use imageproc::morphology::{close, open};
|
||||||
use imageproc::region_labelling::{Connectivity, connected_components};
|
use imageproc::region_labelling::{Connectivity, connected_components};
|
||||||
use imageproc::template_matching::{MatchTemplateMethod, match_template};
|
use imageproc::template_matching::{MatchTemplateMethod, match_template};
|
||||||
use std::fmt;
|
|
||||||
use ndarray::{ArrayView2, ArrayView3};
|
use ndarray::{ArrayView2, ArrayView3};
|
||||||
|
use std::fmt;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SlideResult {
|
pub struct SlideResult {
|
||||||
pub target: [i32; 2],
|
pub target: [i32; 2],
|
||||||
@@ -67,7 +67,6 @@ impl Slider {
|
|||||||
target: ArrayView3<u8>,
|
target: ArrayView3<u8>,
|
||||||
background: ArrayView3<u8>,
|
background: ArrayView3<u8>,
|
||||||
) -> Result<SlideResult> {
|
) -> Result<SlideResult> {
|
||||||
|
|
||||||
// 1. 计算差异数组 (复用 cv2::absdiff)
|
// 1. 计算差异数组 (复用 cv2::absdiff)
|
||||||
let (th, tw, tc) = target.dim();
|
let (th, tw, tc) = target.dim();
|
||||||
let (bh, bw, bc) = background.dim();
|
let (bh, bw, bc) = background.dim();
|
||||||
@@ -111,11 +110,12 @@ impl Slider {
|
|||||||
|
|
||||||
// // 统计每个标签出现的频率(即面积)
|
// // 统计每个标签出现的频率(即面积)
|
||||||
// 4. 寻找最大连通区域 (对应 findContours + max area)
|
// 4. 寻找最大连通区域 (对应 findContours + max area)
|
||||||
if let Some(max_label) = image_proc::find_contours_and_max(&labelled) {
|
if let Some(max_label) = image_processor::find_contours_and_max(&labelled) {
|
||||||
// 5. 计算最大区域的边界框 (对应 cv2.boundingRect)
|
// 5. 计算最大区域的边界框 (对应 cv2.boundingRect)
|
||||||
let (x, y, w, h) = image_proc::bounding_rect(&labelled, max_label);
|
let (x, y, w, h) = image_processor::bounding_rect(&labelled, max_label);
|
||||||
// 6. 计算中心点 (调用之前封装的 calculate_center)
|
// 6. 计算中心点 (调用之前封装的 calculate_center)
|
||||||
let (center_x, center_y) = image_proc::calculate_center((x, y), w as usize, h as usize);
|
let (center_x, center_y) =
|
||||||
|
image_processor::calculate_center((x, y), w as usize, h as usize);
|
||||||
|
|
||||||
Ok(SlideResult {
|
Ok(SlideResult {
|
||||||
target: [center_x, center_y],
|
target: [center_x, center_y],
|
||||||
@@ -206,7 +206,8 @@ impl Slider {
|
|||||||
// 4. 计算中心点 (与 Python 逻辑完全一致)
|
// 4. 计算中心点 (与 Python 逻辑完全一致)
|
||||||
let (th, tw) = target.dim();
|
let (th, tw) = target.dim();
|
||||||
|
|
||||||
let (center_x, center_y) = image_proc::calculate_center(max_loc, tw as usize, th as usize);
|
let (center_x, center_y) =
|
||||||
|
image_processor::calculate_center(max_loc, tw as usize, th as usize);
|
||||||
// println!("Rust Target Width (tw): {}", tw);
|
// println!("Rust Target Width (tw): {}", tw);
|
||||||
// println!("Rust Best Max Loc X: {}", max_loc.0);
|
// println!("Rust Best Max Loc X: {}", max_loc.0);
|
||||||
// println!("Rust Final Center X: {}", center_x);
|
// println!("Rust Final Center X: {}", center_x);
|
||||||
@@ -251,7 +252,8 @@ impl Slider {
|
|||||||
// 5. 计算中心位置 (对齐 Python 逻辑)
|
// 5. 计算中心位置 (对齐 Python 逻辑)
|
||||||
// target_w, target_h 来自输入数组的维度
|
// target_w, target_h 来自输入数组的维度
|
||||||
let (th, tw) = target.dim();
|
let (th, tw) = target.dim();
|
||||||
let (center_x, center_y) = image_proc::calculate_center(max_loc, tw as usize, th as usize);
|
let (center_x, center_y) =
|
||||||
|
image_processor::calculate_center(max_loc, tw as usize, th as usize);
|
||||||
|
|
||||||
// 打印调试信息,方便与 Python 对比
|
// 打印调试信息,方便与 Python 对比
|
||||||
// println!("Edge Match: max_val: {}, max_loc: {:?}", max_val, max_loc);
|
// println!("Edge Match: max_val: {}, max_loc: {:?}", max_val, max_loc);
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
pub mod image_io;
|
pub mod image_io;
|
||||||
pub mod image_processor;
|
pub mod image_processor;
|
||||||
pub mod image_proc;
|
|
||||||
mod tensor_transform;
|
mod tensor_transform;
|
||||||
// 对外统一暴露干净的 API 语义层
|
// 对外统一暴露干净的 API 语义层
|
||||||
pub use image_proc::*;
|
pub use tensor_transform::normalize_ocr_logits;
|
||||||
pub use tensor_transform::normalize_ocr_logits;
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
use image::{ImageBuffer, Luma};
|
|
||||||
use ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip};
|
|
||||||
use std::cmp::{max, min};
|
|
||||||
|
|
||||||
// 模拟openCV
|
|
||||||
/// 1. 计算两个数组的绝对差值 (对应 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 as i16 - vb as i16).abs() as u8;
|
|
||||||
});
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 1. 模拟 findContours 并获取最大面积区域的 Label
|
|
||||||
/// 返回 Option<u32>,如果找不到任何区域则返回 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 高性能转换:将 `ndarray` 2D 灰度视图规整为 `image::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 编写)
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
#[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)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,180 @@
|
|||||||
use image::{DynamicImage, GrayImage, imageops::FilterType, Rgb, ImageBuffer};
|
use image::{DynamicImage, GrayImage, ImageBuffer, Luma, imageops::FilterType};
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use crate::models::ocr::color_filter::HsvRange;
|
use ndarray::{Array2, Array3, ArrayView2, ArrayView3, azip};
|
||||||
use crate::utils::image_proc::rgb_to_opencv_hsv;
|
use std::cmp::{max, min};
|
||||||
|
|
||||||
|
// 模拟openCV
|
||||||
|
/// 1. 计算两个数组的绝对差值 (对应 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 as i16 - vb as i16).abs() as u8;
|
||||||
|
});
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 1. 模拟 findContours 并获取最大面积区域的 Label
|
||||||
|
/// 返回 Option<u32>,如果找不到任何区域则返回 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 高性能转换:将 `ndarray` 2D 灰度视图规整为 `image::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 编写)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// 对应 Python 的 convert_to_grayscale
|
/// 对应 Python 的 convert_to_grayscale
|
||||||
/// 将图像转换为灰度图 (L模式)
|
/// 将图像转换为灰度图 (L模式)
|
||||||
@@ -18,23 +191,8 @@ pub fn resize_image(
|
|||||||
target_height: u32,
|
target_height: u32,
|
||||||
// resample 参数我们直接使用 FilterType,Lanczos3 是最接近 Python LANCZOS 的
|
// resample 参数我们直接使用 FilterType,Lanczos3 是最接近 Python LANCZOS 的
|
||||||
) -> DynamicImage {
|
) -> DynamicImage {
|
||||||
|
|
||||||
// image::imageops::resize 的最高层封装
|
// image::imageops::resize 的最高层封装
|
||||||
// FilterType::Lanczos3 与 Python Pillow 的 Image.LANCZOS 算法完全对齐,缩放质量最高
|
// FilterType::Lanczos3 与 Python Pillow 的 Image.LANCZOS 算法完全对齐,缩放质量最高
|
||||||
image.resize_exact(target_width, target_height, FilterType::Lanczos3)
|
image.resize_exact(target_width, target_height, FilterType::Lanczos3)
|
||||||
}
|
}
|
||||||
// pub fn resize_image(
|
|
||||||
// image: &GrayImage,
|
|
||||||
// target_width: u32,
|
|
||||||
// target_height: u32,
|
|
||||||
// // resample 参数我们直接使用 FilterType,Lanczos3 是最接近 Python LANCZOS 的
|
|
||||||
// ) -> GrayImage {
|
|
||||||
// // 使用 resize 算法进行精确缩放
|
|
||||||
// image::imageops::resize(
|
|
||||||
// image,
|
|
||||||
// target_width,
|
|
||||||
// target_height,
|
|
||||||
// FilterType::Lanczos3
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::loader::{ModelLoader, ModelSession, ModelType};
|
use crate::loader::ModelLoader;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use ddddocr_core::error::{DdddError, Result};
|
use ddddocr_core::error::{DdddError, Result};
|
||||||
use ddddocr_core::{DetEngine, DetOutput, InferenceEngine};
|
use ddddocr_core::{DetEngine, DetOutput, InferenceEngine};
|
||||||
@@ -10,15 +10,6 @@ pub struct DetSession {
|
|||||||
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModelSession for DetSession {
|
|
||||||
fn get_model_type(&self) -> ModelType {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn desc(&self) -> String {
|
|
||||||
"Detection Model 加载成功".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DetSession {
|
impl DetSession {
|
||||||
pub fn new<P>(model_path: P) -> Result<Self>
|
pub fn new<P>(model_path: P) -> Result<Self>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -3,18 +3,6 @@ use ddddocr_core::error::Result;
|
|||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use tract_onnx::onnx;
|
use tract_onnx::onnx;
|
||||||
use tract_onnx::prelude::*; // 引入核心层的统一错误类型
|
use tract_onnx::prelude::*; // 引入核心层的统一错误类型
|
||||||
/// OCR 模型:包含路径和字符集
|
|
||||||
|
|
||||||
pub enum ModelType {
|
|
||||||
Ocr,
|
|
||||||
Det,
|
|
||||||
Custom,
|
|
||||||
}
|
|
||||||
// 定义统一的 trait
|
|
||||||
pub trait ModelSession {
|
|
||||||
fn get_model_type(&self) -> ModelType;
|
|
||||||
fn desc(&self) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ModelLoader {
|
pub struct ModelLoader {
|
||||||
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
pub session: RunnableModel<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::borrow::Cow;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use ddddocr_core::models::ocr::metadata::Charset;
|
use ddddocr_core::ocr::metadata::Charset;
|
||||||
use ddddocr_core::models::ocr::metadata::{Normalization, Resize};
|
use ddddocr_core::ocr::metadata::{Normalization, Resize};
|
||||||
|
|
||||||
pub const CHARSET_BETA: &[&str] = &[
|
pub const CHARSET_BETA: &[&str] = &[
|
||||||
"", "笤", "谴", "膀", "荔", "佰", "电", "臁", "矍", "同", "奇", "芄", "吠", "6", "曛", "荇",
|
"", "笤", "谴", "膀", "荔", "佰", "电", "臁", "矍", "同", "奇", "芄", "吠", "6", "曛", "荇",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use ddddocr_core::models::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, Rgb};
|
||||||
@@ -6,7 +6,7 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
mod char_slice;
|
mod char_slice;
|
||||||
use char_slice::CHARSET_BETA;
|
use char_slice::CHARSET_BETA;
|
||||||
use ddddocr_core::models::ocr::metadata::{Normalization, Resize};
|
use ddddocr_core::ocr::metadata::{Normalization, Resize};
|
||||||
|
|
||||||
fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
|
fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
|
||||||
// 1. 先将泛型转为具体的 &Path 引用
|
// 1. 先将泛型转为具体的 &Path 引用
|
||||||
|
|||||||
Reference in New Issue
Block a user