feat: ddddocr-rs 完成 core/ort/tract2 规范整改与发布
准备 - core:内置官方字符集(OLD/BETA)与 ModelMetadata::from_builtin_* 构造器 - ort:导出 Session、实现 Info、修正 cuda feature 接线、共享推理工具 - tract2:包名更名(原 ddddocr-tract 已被占用)并完成规范整改 - 集成测试按领域拆分(ocr / det / slide / common / api_surface) - 发布准备:Cargo.toml 元数据、workspace 版本 0.2.4、LICENSE/ NOTICE、README 模型下载说明
This commit is contained in:
74
ddddocr-tract2/tests/api_surface.rs
Normal file
74
ddddocr-tract2/tests/api_surface.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! 外部视角 API 测试:验证 `ddddocr-tract2` 的公开类型与方法在外部 crate 中可正常使用。
|
||||
|
||||
use ddddocr_core::Resize;
|
||||
use ddddocr_core::traits::{Info, Loader};
|
||||
use ddddocr_core::types::AxisDim;
|
||||
use ddddocr_tract2::loader::ModelLoader;
|
||||
use ddddocr_tract2::{
|
||||
DetRuntime, ModelMetadata, Normalization, OcrBuilder, OcrRuntime, Session, SlideResult, Slider,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn model_path(name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("models")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
/// 验证对外导出的类型(含 `Session` 与 core 便捷重导出)均可直接命名。
|
||||
#[test]
|
||||
fn public_types_are_nameable() {
|
||||
let _: Option<Session> = None;
|
||||
let _: Option<OcrBuilder> = None;
|
||||
let _: Option<SlideResult> = None;
|
||||
let _slider = Slider::new();
|
||||
}
|
||||
|
||||
/// 验证构建器链式 API 可组合使用,且返回的会话类型可显式标注。
|
||||
#[test]
|
||||
fn loader_chain_builds_session() -> anyhow::Result<()> {
|
||||
let path = model_path("common_sml2h3_f32.onnx");
|
||||
assert!(path.exists(), "缺少测试模型: {}", path.display());
|
||||
let _session: Session = ModelLoader::default().num_threads(4).build_for_path(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 验证 `Info` trait 能从真实会话中解析输入/输出信息。
|
||||
#[test]
|
||||
fn info_trait_returns_model_metadata() -> anyhow::Result<()> {
|
||||
let path = model_path("common_sml2h3_f32.onnx");
|
||||
assert!(path.exists(), "缺少测试模型: {}", path.display());
|
||||
let session: Session = ModelLoader::default().build_for_path(path)?;
|
||||
let metadata = ModelMetadata::from_static_slice(
|
||||
&["a", "b"],
|
||||
false,
|
||||
Resize::DynamicWidth(64),
|
||||
1,
|
||||
Normalization::MinusOneToOne,
|
||||
);
|
||||
let ocr = OcrRuntime::new(session, metadata);
|
||||
|
||||
let inputs = ocr.input_info()?;
|
||||
let outputs = ocr.output_info()?;
|
||||
assert!(!inputs.is_empty());
|
||||
assert!(!outputs.is_empty());
|
||||
assert!(matches!(inputs[0].shape[0], AxisDim::Static(1)));
|
||||
assert!(matches!(inputs[0].shape[2], AxisDim::Static(64)));
|
||||
|
||||
let model_info = ocr.model_info()?;
|
||||
assert_eq!(model_info.inputs.len(), inputs.len());
|
||||
assert_eq!(model_info.outputs.len(), outputs.len());
|
||||
assert!(model_info.providers.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 验证检测运行时可以从构建的会话创建。
|
||||
#[test]
|
||||
fn det_runtime_builds_from_session() -> anyhow::Result<()> {
|
||||
let path = model_path("common_det.onnx");
|
||||
assert!(path.exists(), "缺少测试模型: {}", path.display());
|
||||
let session: Session = ModelLoader::default().build_for_path(path)?;
|
||||
let _det = DetRuntime::new(session);
|
||||
Ok(())
|
||||
}
|
||||
25
ddddocr-tract2/tests/common/mod.rs
Normal file
25
ddddocr-tract2/tests/common/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
//! 集成测试共享工具。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 仓库根目录 `models/` 下模型文件的路径。
|
||||
pub fn model_path(name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("models")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
/// 仓库根目录 `samples/` 下样例图片的路径。
|
||||
pub fn sample_path(name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("samples")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
/// 加载图片,失败时附带路径上下文。
|
||||
pub fn load_image<P: AsRef<Path>>(path: P) -> anyhow::Result<image::DynamicImage> {
|
||||
let path_ref = path.as_ref();
|
||||
image::open(path_ref).map_err(|e| anyhow::anyhow!("无法加载图片 {:?}: {}", path_ref, e))
|
||||
}
|
||||
33
ddddocr-tract2/tests/det.rs
Normal file
33
ddddocr-tract2/tests/det.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! 目标检测集成测试。
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{model_path, sample_path};
|
||||
use ddddocr_core::Detector;
|
||||
use ddddocr_core::traits::Loader;
|
||||
use ddddocr_tract2::DetRuntime;
|
||||
use ddddocr_tract2::loader::ModelLoader;
|
||||
use image::GenericImageView;
|
||||
|
||||
/// 检测模型应能从样例图片中找到至少一个目标,且坐标在图片范围内。
|
||||
#[test]
|
||||
fn det_model_detects_targets_in_image() -> anyhow::Result<()> {
|
||||
let session = ModelLoader::default()
|
||||
.build_for_path(model_path("common_det.onnx"))
|
||||
.expect("模型加载失败");
|
||||
let det = DetRuntime::new(session);
|
||||
let img = image::open(sample_path("det1.png")).expect("测试图片不存在");
|
||||
|
||||
let bboxes = Detector::new(&det).predict(&img)?;
|
||||
assert!(!bboxes.is_empty(), "应检测到至少一个目标");
|
||||
|
||||
let (width, height) = img.dimensions();
|
||||
for bbox in &bboxes {
|
||||
assert!(bbox.x1 >= 0 && bbox.y1 >= 0, "检测框左上角不应为负");
|
||||
assert!(
|
||||
bbox.x2 <= width as i32 && bbox.y2 <= height as i32,
|
||||
"检测框右下角不应超出图片范围"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
55
ddddocr-tract2/tests/ocr.rs
Normal file
55
ddddocr-tract2/tests/ocr.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! OCR 识别与模型信息集成测试。
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{model_path, sample_path};
|
||||
use ddddocr_core::traits::{Info, Loader};
|
||||
use ddddocr_core::{ModelMetadata, Normalization, Ocr, Resize};
|
||||
use ddddocr_tract2::OcrRuntime;
|
||||
use ddddocr_tract2::loader::ModelLoader;
|
||||
|
||||
/// 用官方 sml2h3 f32 模型识别验证码图片,结果不应为空。
|
||||
#[test]
|
||||
fn ocr_classification_recognizes_code_image() {
|
||||
let session = ModelLoader::default()
|
||||
.build_for_path(model_path("common_sml2h3_f32.onnx"))
|
||||
.expect("模型加载失败");
|
||||
let metadata = ModelMetadata::from_builtin_beta(
|
||||
false,
|
||||
Resize::DynamicWidth(64),
|
||||
1,
|
||||
Normalization::MinusOneToOne,
|
||||
);
|
||||
let ocr = OcrRuntime::new(session, metadata);
|
||||
|
||||
let img = image::open(sample_path("code2.png")).expect("测试图片不存在");
|
||||
let text = Ocr::builder()
|
||||
.build_with(&ocr)
|
||||
.predict(&img)
|
||||
.expect("识别过程出错")
|
||||
.into_text();
|
||||
|
||||
println!("识别结果: {text}");
|
||||
assert!(!text.is_empty(), "识别结果不应为空");
|
||||
}
|
||||
|
||||
/// 真实模型应能通过 `Info` trait 返回输入/输出张量信息。
|
||||
#[test]
|
||||
fn model_info_lists_inputs_and_outputs() -> anyhow::Result<()> {
|
||||
let session = ModelLoader::default()
|
||||
.build_for_path(model_path("common_huashi666_i64.onnx"))
|
||||
.expect("建立测试模型图失败");
|
||||
let metadata = ModelMetadata::from_builtin_beta(
|
||||
false,
|
||||
Resize::DynamicWidth(64),
|
||||
1,
|
||||
Normalization::MinusOneToOne,
|
||||
);
|
||||
let ocr = OcrRuntime::new(session, metadata);
|
||||
|
||||
let inputs = ocr.input_info()?;
|
||||
let outputs = ocr.output_info()?;
|
||||
assert!(!inputs.is_empty(), "模型应有输入张量信息");
|
||||
assert!(!outputs.is_empty(), "模型应有输出张量信息");
|
||||
Ok(())
|
||||
}
|
||||
52
ddddocr-tract2/tests/slide.rs
Normal file
52
ddddocr-tract2/tests/slide.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! 滑块匹配集成测试。
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{load_image, sample_path};
|
||||
use ddddocr_core::Slider;
|
||||
|
||||
/// 边缘模式匹配应定位到预期坐标。
|
||||
#[test]
|
||||
fn slide_match_locates_target_position() {
|
||||
let engine = Slider::new();
|
||||
let target = load_image(sample_path("target1.png")).expect("请确保 samples/target1.png 存在");
|
||||
let background = load_image(sample_path("background1.png")).expect("请确保 samples/background1.png 存在");
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let result = engine
|
||||
.slide_match(&target, &background, false)
|
||||
.expect("Slide match 执行失败");
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
println!("边缘模式匹配: {result}");
|
||||
println!("耗时: {elapsed:?}");
|
||||
|
||||
assert_eq!(result.target_x, 237);
|
||||
assert_eq!(result.target_y, 77);
|
||||
assert!(result.confidence > 0.0);
|
||||
}
|
||||
|
||||
/// 灰度对比匹配应定位到预期坐标。
|
||||
#[test]
|
||||
fn slide_comparison_locates_target_position() {
|
||||
let engine = Slider::new();
|
||||
let target = load_image(sample_path("target2.jpg")).expect("请确保 samples/target2.jpg 存在");
|
||||
let background =
|
||||
load_image(sample_path("background2.jpg")).expect("请确保 samples/background2.jpg 存在");
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let result = engine
|
||||
.slide_comparison(&target, &background)
|
||||
.expect("Slide comparison 执行失败");
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
println!(
|
||||
"灰度对比匹配: 坐标 [x: {}, y: {}], 置信度 {:.4}",
|
||||
result.target_x, result.target_y, result.confidence
|
||||
);
|
||||
println!("耗时: {elapsed:?}");
|
||||
|
||||
assert_eq!(result.target_x, 171);
|
||||
assert_eq!(result.target_y, 90);
|
||||
assert!(result.confidence > 0.0);
|
||||
}
|
||||
Reference in New Issue
Block a user