use super::{PreprocessError, Result};
use image::{GrayImage, Luma};
use imageproc::geometric_transformations::{rotate_about_center, Interpolation};
use std::f32;
pub fn detect_rotation(image: &GrayImage) -> Result<f32> {
let (width, height) = image.dimensions();
if width < 10 || height < 10 {
return Err(PreprocessError::InvalidParameters(
"Image too small for rotation detection".to_string(),
));
}
let angles = [-45.0, -30.0, -15.0, 0.0, 15.0, 30.0, 45.0];
let mut max_score = 0.0;
let mut best_angle = 0.0;
for &angle in &angles {
let score = calculate_projection_score(image, angle);
if score > max_score {
max_score = score;
best_angle = angle;
}
}
let fine_angles: Vec<f32> = (-5..=5).map(|i| best_angle + (i as f32) * 2.0).collect();
max_score = 0.0;
for angle in fine_angles {
let score = calculate_projection_score(image, angle);
if score > max_score {
max_score = score;
best_angle = angle;
}
}
Ok(best_angle)
}
fn calculate_projection_score(image: &GrayImage, angle: f32) -> f32 {
let (width, height) = image.dimensions();
if angle.abs() < 0.1 {
return calculate_horizontal_projection_variance(image);
}
let rad = angle.to_radians();
let cos_a = rad.cos();
let sin_a = rad.sin();
let mut projection = vec![0u32; height as usize];
for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y)[0];
if pixel < 128 {
let proj_y = ((y as f32) * cos_a - (x as f32) * sin_a) as i32;
if proj_y >= 0 && proj_y < height as i32 {
projection[proj_y as usize] += 1;
}
}
}
}
calculate_variance(&projection)
}
fn calculate_horizontal_projection_variance(image: &GrayImage) -> f32 {
let (width, height) = image.dimensions();
let mut projection = vec![0u32; height as usize];
for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y)[0];
if pixel < 128 {
projection[y as usize] += 1;
}
}
}
calculate_variance(&projection)
}
fn calculate_variance(projection: &[u32]) -> f32 {
if projection.is_empty() {
return 0.0;
}
let mean = projection.iter().sum::<u32>() as f32 / projection.len() as f32;
let variance = projection
.iter()
.map(|&x| {
let diff = x as f32 - mean;
diff * diff
})
.sum::<f32>()
/ projection.len() as f32;
variance
}
pub fn rotate_image(image: &GrayImage, angle: f32) -> Result<GrayImage> {
if angle.abs() < 0.01 {
return Ok(image.clone());
}
let radians = -angle.to_radians(); let rotated = rotate_about_center(
image,
radians,
Interpolation::Bilinear,
Luma([255]), );
Ok(rotated)
}
pub fn detect_rotation_with_confidence(image: &GrayImage) -> Result<(f32, f32)> {
let angle = detect_rotation(image)?;
let current_score = calculate_projection_score(image, angle);
let baseline_score = calculate_projection_score(image, 0.0);
let confidence = if baseline_score > 0.0 {
(current_score / baseline_score).min(1.0)
} else {
0.5 };
Ok((angle, confidence))
}
pub fn auto_rotate(image: &GrayImage, confidence_threshold: f32) -> Result<(GrayImage, f32, f32)> {
let (angle, confidence) = detect_rotation_with_confidence(image)?;
if confidence >= confidence_threshold && angle.abs() > 0.5 {
let rotated = rotate_image(image, -angle)?;
Ok((rotated, angle, confidence))
} else {
Ok((image.clone(), 0.0, confidence))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_text_image() -> GrayImage {
let mut img = GrayImage::new(200, 100);
for pixel in img.pixels_mut() {
*pixel = Luma([255]);
}
for y in [20, 25, 50, 55] {
for x in 10..190 {
img.put_pixel(x, y, Luma([0]));
}
}
img
}
#[test]
fn test_detect_rotation_straight() {
let img = create_text_image();
let angle = detect_rotation(&img);
assert!(angle.is_ok());
let a = angle.unwrap();
assert!(a.abs() < 10.0);
}
#[test]
fn test_rotate_image() {
let img = create_text_image();
let rotated = rotate_image(&img, 15.0);
assert!(rotated.is_ok());
let result = rotated.unwrap();
assert_eq!(result.dimensions(), img.dimensions());
}
#[test]
fn test_rotate_no_change() {
let img = create_text_image();
let rotated = rotate_image(&img, 0.001);
assert!(rotated.is_ok());
let result = rotated.unwrap();
assert_eq!(result.dimensions(), img.dimensions());
}
#[test]
fn test_rotation_confidence() {
let img = create_text_image();
let result = detect_rotation_with_confidence(&img);
assert!(result.is_ok());
let (angle, confidence) = result.unwrap();
assert!(confidence >= 0.0 && confidence <= 1.0);
println!(
"Detected angle: {:.2}°, confidence: {:.2}",
angle, confidence
);
}
#[test]
fn test_auto_rotate_with_threshold() {
let img = create_text_image();
let result = auto_rotate(&img, 0.95);
assert!(result.is_ok());
let (rotated, angle, confidence) = result.unwrap();
assert_eq!(rotated.dimensions(), img.dimensions());
println!(
"Auto-rotate: angle={:.2}°, confidence={:.2}",
angle, confidence
);
}
#[test]
fn test_projection_variance() {
let projection = vec![10, 50, 100, 50, 10];
let variance = calculate_variance(&projection);
assert!(variance > 0.0);
}
#[test]
fn test_rotation_small_image_error() {
let small_img = GrayImage::new(5, 5);
let result = detect_rotation(&small_img);
assert!(result.is_err());
}
#[test]
fn test_rotation_roundtrip() {
let img = create_text_image();
let rotated = rotate_image(&img, 30.0).unwrap();
let unrotated = rotate_image(&rotated, -30.0).unwrap();
assert_eq!(unrotated.dimensions(), img.dimensions());
}
}