use super::text_detection::Detection;
use super::validation::ensure_non_empty_images;
use crate::ConfigValidator;
use crate::core::OCRError;
use crate::core::traits::TaskDefinition;
use crate::core::traits::task::{ImageTaskInput, Task, TaskSchema, TaskType};
use crate::utils::ScoreValidator;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, ConfigValidator)]
pub struct SealTextDetectionConfig {
#[validate(range(min = 0.0, max = 1.0))]
pub score_threshold: f32,
#[validate(range(min = 0.0, max = 1.0))]
pub box_threshold: f32,
#[validate(min = 0.0)]
pub unclip_ratio: f32,
#[validate(min = 1)]
pub max_candidates: usize,
}
impl Default for SealTextDetectionConfig {
fn default() -> Self {
Self {
score_threshold: 0.2, box_threshold: 0.6, unclip_ratio: 0.5, max_candidates: 1000,
}
}
}
#[derive(Debug, Clone)]
pub struct SealTextDetectionOutput {
pub detections: Vec<Vec<Detection>>,
}
impl SealTextDetectionOutput {
pub fn empty() -> Self {
Self {
detections: Vec::new(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
detections: Vec::with_capacity(capacity),
}
}
}
impl TaskDefinition for SealTextDetectionOutput {
const TASK_NAME: &'static str = "seal_text_detection";
const TASK_DOC: &'static str =
"Seal text detection - locating text regions in seal/stamp images";
fn empty() -> Self {
SealTextDetectionOutput::empty()
}
}
#[derive(Debug, Default)]
pub struct SealTextDetectionTask {
pub config: SealTextDetectionConfig,
}
impl SealTextDetectionTask {
pub fn new() -> Self {
Self::default()
}
pub fn with_config(config: SealTextDetectionConfig) -> Self {
Self { config }
}
}
impl Task for SealTextDetectionTask {
type Config = SealTextDetectionConfig;
type Input = ImageTaskInput;
type Output = SealTextDetectionOutput;
fn task_type(&self) -> TaskType {
TaskType::SealTextDetection
}
fn schema(&self) -> TaskSchema {
TaskSchema::new(
self.task_type(),
vec!["image".to_string()],
vec!["seal_text_boxes".to_string(), "scores".to_string()],
)
}
fn validate_input(&self, input: &Self::Input) -> Result<(), OCRError> {
ensure_non_empty_images(
&input.images,
"Input images cannot be empty for seal text detection",
)?;
Ok(())
}
fn validate_output(&self, output: &Self::Output) -> Result<(), OCRError> {
let validator = ScoreValidator::new_unit_range("score");
for (batch_idx, detections) in output.detections.iter().enumerate() {
let scores: Vec<f32> = detections.iter().map(|d| d.score).collect();
validator.validate_scores_with(&scores, |det_idx| {
format!("Batch {}, detection {}", batch_idx, det_idx)
})?;
for (det_idx, detection) in detections.iter().enumerate() {
if detection.bbox.points.is_empty() {
return Err(OCRError::InvalidInput {
message: format!(
"Batch {}, detection {}: empty bounding box points",
batch_idx, det_idx
),
});
}
}
}
Ok(())
}
fn empty_output(&self) -> Self::Output {
SealTextDetectionOutput::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::processors::{BoundingBox, Point};
use image::RgbImage;
#[test]
fn test_seal_text_detection_task_creation() {
let task = SealTextDetectionTask::new();
assert_eq!(task.task_type(), TaskType::SealTextDetection);
}
#[test]
fn test_input_validation() {
let task = SealTextDetectionTask::new();
let empty_input = ImageTaskInput::new(vec![]);
assert!(task.validate_input(&empty_input).is_err());
let valid_input = ImageTaskInput::new(vec![RgbImage::new(100, 100)]);
assert!(task.validate_input(&valid_input).is_ok());
let invalid_input = ImageTaskInput::new(vec![RgbImage::new(0, 100)]);
assert!(task.validate_input(&invalid_input).is_err());
}
#[test]
fn test_output_validation() {
let task = SealTextDetectionTask::new();
let valid_bbox = BoundingBox {
points: vec![
Point { x: 10.0, y: 10.0 },
Point { x: 50.0, y: 10.0 },
Point { x: 50.0, y: 30.0 },
Point { x: 10.0, y: 30.0 },
],
};
let valid_detection = Detection::new(valid_bbox, 0.95);
let valid_output = SealTextDetectionOutput {
detections: vec![vec![valid_detection]],
};
assert!(task.validate_output(&valid_output).is_ok());
let invalid_bbox = BoundingBox {
points: vec![Point { x: 0.0, y: 0.0 }],
};
let invalid_detection = Detection::new(invalid_bbox, 1.5); let invalid_score_output = SealTextDetectionOutput {
detections: vec![vec![invalid_detection]],
};
assert!(task.validate_output(&invalid_score_output).is_err());
let empty_bbox = BoundingBox { points: vec![] };
let empty_bbox_detection = Detection::new(empty_bbox, 0.95);
let empty_bbox_output = SealTextDetectionOutput {
detections: vec![vec![empty_bbox_detection]],
};
assert!(task.validate_output(&empty_bbox_output).is_err());
}
#[test]
fn test_schema() {
let task = SealTextDetectionTask::new();
let schema = task.schema();
assert_eq!(schema.task_type, TaskType::SealTextDetection);
assert_eq!(schema.input_types, vec!["image"]);
assert_eq!(schema.output_types, vec!["seal_text_boxes", "scores"]);
}
}