pub mod deskew;
pub mod enhancement;
pub mod pipeline;
pub mod rotation;
pub mod segmentation;
pub mod transforms;
use image::{DynamicImage, GrayImage};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PreprocessError {
#[error("Image loading error: {0}")]
ImageLoad(String),
#[error("Invalid parameters: {0}")]
InvalidParameters(String),
#[error("Processing error: {0}")]
Processing(String),
#[error("Segmentation error: {0}")]
Segmentation(String),
}
pub type Result<T> = std::result::Result<T, PreprocessError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreprocessOptions {
pub auto_rotate: bool,
pub auto_deskew: bool,
pub enhance_contrast: bool,
pub denoise: bool,
pub threshold: Option<u8>,
pub adaptive_threshold: bool,
pub adaptive_window_size: u32,
pub target_width: Option<u32>,
pub target_height: Option<u32>,
pub detect_regions: bool,
pub blur_sigma: f32,
pub clahe_clip_limit: f32,
pub clahe_tile_size: u32,
}
impl Default for PreprocessOptions {
fn default() -> Self {
Self {
auto_rotate: true,
auto_deskew: true,
enhance_contrast: true,
denoise: true,
threshold: None,
adaptive_threshold: true,
adaptive_window_size: 15,
target_width: None,
target_height: None,
detect_regions: true,
blur_sigma: 1.0,
clahe_clip_limit: 2.0,
clahe_tile_size: 8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegionType {
Text,
Math,
Table,
Figure,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextRegion {
pub region_type: RegionType,
pub bbox: (u32, u32, u32, u32),
pub confidence: f32,
pub text_height: f32,
pub baseline_angle: f32,
}
pub fn preprocess(image: &DynamicImage, options: &PreprocessOptions) -> Result<GrayImage> {
pipeline::PreprocessPipeline::builder()
.auto_rotate(options.auto_rotate)
.auto_deskew(options.auto_deskew)
.enhance_contrast(options.enhance_contrast)
.denoise(options.denoise)
.blur_sigma(options.blur_sigma)
.clahe_clip_limit(options.clahe_clip_limit)
.clahe_tile_size(options.clahe_tile_size)
.threshold(options.threshold)
.adaptive_threshold(options.adaptive_threshold)
.adaptive_window_size(options.adaptive_window_size)
.target_size(options.target_width, options.target_height)
.build()
.process(image)
}
pub fn detect_text_regions(image: &GrayImage, min_region_size: u32) -> Result<Vec<TextRegion>> {
segmentation::find_text_regions(image, min_region_size)
}
#[cfg(test)]
mod tests {
use super::*;
use image::{Rgb, RgbImage};
fn create_test_image(width: u32, height: u32) -> DynamicImage {
let mut img = RgbImage::new(width, height);
for y in 0..height {
for x in 0..width {
let val = ((x + y) % 256) as u8;
img.put_pixel(x, y, Rgb([val, val, val]));
}
}
DynamicImage::ImageRgb8(img)
}
#[test]
fn test_preprocess_default_options() {
let img = create_test_image(100, 100);
let options = PreprocessOptions::default();
let result = preprocess(&img, &options);
assert!(result.is_ok());
let processed = result.unwrap();
assert_eq!(processed.width(), 100);
assert_eq!(processed.height(), 100);
}
#[test]
fn test_preprocess_with_resize() {
let img = create_test_image(200, 200);
let mut options = PreprocessOptions::default();
options.target_width = Some(100);
options.target_height = Some(100);
let result = preprocess(&img, &options);
assert!(result.is_ok());
let processed = result.unwrap();
assert_eq!(processed.width(), 100);
assert_eq!(processed.height(), 100);
}
#[test]
fn test_preprocess_options_builder() {
let options = PreprocessOptions {
auto_rotate: false,
auto_deskew: false,
enhance_contrast: true,
denoise: true,
threshold: Some(128),
adaptive_threshold: false,
..Default::default()
};
assert!(!options.auto_rotate);
assert!(!options.auto_deskew);
assert!(options.enhance_contrast);
assert_eq!(options.threshold, Some(128));
}
#[test]
fn test_region_type_serialization() {
let region = TextRegion {
region_type: RegionType::Math,
bbox: (10, 20, 100, 50),
confidence: 0.95,
text_height: 12.0,
baseline_angle: 0.5,
};
let json = serde_json::to_string(®ion).unwrap();
let deserialized: TextRegion = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.region_type, RegionType::Math);
assert_eq!(deserialized.bbox, (10, 20, 100, 50));
assert!((deserialized.confidence - 0.95).abs() < 0.001);
}
}