git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
378 lines
11 KiB
Rust
378 lines
11 KiB
Rust
// Preprocessing tests for ruvector-scipix
|
|
//
|
|
// Tests image preprocessing functions including grayscale conversion,
|
|
// Gaussian blur, Otsu thresholding, rotation detection, deskewing,
|
|
// CLAHE enhancement, and pipeline chaining.
|
|
// Target: 90%+ coverage of preprocessing module
|
|
|
|
#[cfg(test)]
|
|
mod preprocess_tests {
|
|
use std::f32::consts::PI;
|
|
|
|
// Mock image structures for testing
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
struct GrayImage {
|
|
width: u32,
|
|
height: u32,
|
|
data: Vec<u8>,
|
|
}
|
|
|
|
impl GrayImage {
|
|
fn new(width: u32, height: u32) -> Self {
|
|
Self {
|
|
width,
|
|
height,
|
|
data: vec![0; (width * height) as usize],
|
|
}
|
|
}
|
|
|
|
fn from_fn<F>(width: u32, height: u32, f: F) -> Self
|
|
where
|
|
F: Fn(u32, u32) -> u8,
|
|
{
|
|
let mut data = Vec::with_capacity((width * height) as usize);
|
|
for y in 0..height {
|
|
for x in 0..width {
|
|
data.push(f(x, y));
|
|
}
|
|
}
|
|
Self {
|
|
width,
|
|
height,
|
|
data,
|
|
}
|
|
}
|
|
|
|
fn get_pixel(&self, x: u32, y: u32) -> u8 {
|
|
self.data[(y * self.width + x) as usize]
|
|
}
|
|
}
|
|
|
|
// Mock preprocessing functions
|
|
fn to_grayscale(rgb: &[u8; 3]) -> u8 {
|
|
(0.299 * rgb[0] as f32 + 0.587 * rgb[1] as f32 + 0.114 * rgb[2] as f32) as u8
|
|
}
|
|
|
|
fn gaussian_blur(image: &GrayImage, sigma: f32) -> GrayImage {
|
|
// Simple mock - just return a copy
|
|
image.clone()
|
|
}
|
|
|
|
fn otsu_threshold(image: &GrayImage) -> u8 {
|
|
// Simple mock implementation
|
|
let sum: u32 = image.data.iter().map(|&x| x as u32).sum();
|
|
let avg = sum / image.data.len() as u32;
|
|
avg as u8
|
|
}
|
|
|
|
fn apply_threshold(image: &GrayImage, threshold: u8) -> GrayImage {
|
|
GrayImage::from_fn(image.width, image.height, |x, y| {
|
|
if image.get_pixel(x, y) > threshold {
|
|
255
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
}
|
|
|
|
fn detect_rotation_angle(image: &GrayImage) -> f32 {
|
|
// Mock: return 0 for simplicity
|
|
0.0
|
|
}
|
|
|
|
fn deskew_angle(image: &GrayImage) -> f32 {
|
|
// Mock: return small random angle
|
|
2.5
|
|
}
|
|
|
|
fn apply_clahe(image: &GrayImage, clip_limit: f32) -> GrayImage {
|
|
// Mock: increase contrast slightly
|
|
GrayImage::from_fn(image.width, image.height, |x, y| {
|
|
let pixel = image.get_pixel(x, y);
|
|
((pixel as f32 * 1.2).min(255.0)) as u8
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_grayscale_conversion_white() {
|
|
let white = [255u8, 255, 255];
|
|
let gray = to_grayscale(&white);
|
|
assert_eq!(gray, 255);
|
|
}
|
|
|
|
#[test]
|
|
fn test_grayscale_conversion_black() {
|
|
let black = [0u8, 0, 0];
|
|
let gray = to_grayscale(&black);
|
|
assert_eq!(gray, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_grayscale_conversion_red() {
|
|
let red = [255u8, 0, 0];
|
|
let gray = to_grayscale(&red);
|
|
// 0.299 * 255 ≈ 76
|
|
assert!(gray >= 70 && gray <= 80);
|
|
}
|
|
|
|
#[test]
|
|
fn test_grayscale_conversion_green() {
|
|
let green = [0u8, 255, 0];
|
|
let gray = to_grayscale(&green);
|
|
// 0.587 * 255 ≈ 150
|
|
assert!(gray >= 145 && gray <= 155);
|
|
}
|
|
|
|
#[test]
|
|
fn test_grayscale_conversion_blue() {
|
|
let blue = [0u8, 0, 255];
|
|
let gray = to_grayscale(&blue);
|
|
// 0.114 * 255 ≈ 29
|
|
assert!(gray >= 25 && gray <= 35);
|
|
}
|
|
|
|
#[test]
|
|
fn test_gaussian_blur_preserves_dimensions() {
|
|
let image = GrayImage::new(100, 100);
|
|
let blurred = gaussian_blur(&image, 1.0);
|
|
|
|
assert_eq!(blurred.width, 100);
|
|
assert_eq!(blurred.height, 100);
|
|
}
|
|
|
|
#[test]
|
|
fn test_gaussian_blur_multiple_sigmas() {
|
|
let image = GrayImage::new(50, 50);
|
|
|
|
let sigmas = vec![0.5, 1.0, 1.5, 2.0, 3.0];
|
|
for sigma in sigmas {
|
|
let blurred = gaussian_blur(&image, sigma);
|
|
assert_eq!(blurred.width, image.width);
|
|
assert_eq!(blurred.height, image.height);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_otsu_thresholding_uniform_image() {
|
|
let image = GrayImage::from_fn(50, 50, |_, _| 128);
|
|
let threshold = otsu_threshold(&image);
|
|
assert_eq!(threshold, 128);
|
|
}
|
|
|
|
#[test]
|
|
fn test_otsu_thresholding_bimodal_image() {
|
|
// Create image with two distinct levels
|
|
let image = GrayImage::from_fn(100, 100, |x, y| {
|
|
if (x + y) % 2 == 0 {
|
|
50
|
|
} else {
|
|
200
|
|
}
|
|
});
|
|
|
|
let threshold = otsu_threshold(&image);
|
|
// Threshold should be between the two peaks
|
|
assert!(threshold > 50 && threshold < 200);
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_threshold_creates_binary_image() {
|
|
let image = GrayImage::from_fn(50, 50, |x, y| ((x + y) % 256) as u8);
|
|
let binary = apply_threshold(&image, 128);
|
|
|
|
// Check all pixels are either 0 or 255
|
|
for pixel in binary.data.iter() {
|
|
assert!(*pixel == 0 || *pixel == 255);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_threshold_low_threshold() {
|
|
let image = GrayImage::from_fn(50, 50, |_, _| 100);
|
|
let binary = apply_threshold(&image, 50);
|
|
|
|
// All pixels should be 255 (above threshold)
|
|
assert!(binary.data.iter().all(|&x| x == 255));
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_threshold_high_threshold() {
|
|
let image = GrayImage::from_fn(50, 50, |_, _| 100);
|
|
let binary = apply_threshold(&image, 150);
|
|
|
|
// All pixels should be 0 (below threshold)
|
|
assert!(binary.data.iter().all(|&x| x == 0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_rotation_detection_zero() {
|
|
let image = GrayImage::new(100, 100);
|
|
let angle = detect_rotation_angle(&image);
|
|
assert!((angle - 0.0).abs() < 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rotation_detection_90_degrees() {
|
|
let image = GrayImage::from_fn(100, 100, |x, _| x as u8);
|
|
let angle = detect_rotation_angle(&image);
|
|
// In real implementation, should detect 0, 90, 180, or 270
|
|
assert!(angle >= -180.0 && angle <= 180.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rotation_detection_180_degrees() {
|
|
let image = GrayImage::from_fn(100, 100, |x, y| ((x + y) % 256) as u8);
|
|
let angle = detect_rotation_angle(&image);
|
|
assert!(angle >= -180.0 && angle <= 180.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rotation_detection_270_degrees() {
|
|
let image = GrayImage::new(100, 100);
|
|
let angle = detect_rotation_angle(&image);
|
|
assert!(angle >= -180.0 && angle <= 180.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deskew_angle_detection() {
|
|
let image = GrayImage::new(100, 100);
|
|
let angle = deskew_angle(&image);
|
|
|
|
// Skew angle should typically be small (< 45 degrees)
|
|
assert!(angle.abs() < 45.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deskew_angle_horizontal_lines() {
|
|
let image = GrayImage::from_fn(100, 100, |_, y| {
|
|
if y % 10 == 0 {
|
|
255
|
|
} else {
|
|
0
|
|
}
|
|
});
|
|
|
|
let angle = deskew_angle(&image);
|
|
// Should detect minimal skew for horizontal lines
|
|
assert!(angle.abs() < 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clahe_enhancement() {
|
|
let image = GrayImage::from_fn(100, 100, |x, y| ((x + y) % 128) as u8);
|
|
let enhanced = apply_clahe(&image, 2.0);
|
|
|
|
assert_eq!(enhanced.width, image.width);
|
|
assert_eq!(enhanced.height, image.height);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clahe_increases_contrast() {
|
|
let low_contrast = GrayImage::from_fn(50, 50, |x, _| (100 + x % 20) as u8);
|
|
let enhanced = apply_clahe(&low_contrast, 2.0);
|
|
|
|
// Calculate simple contrast measure
|
|
let original_range = calculate_range(&low_contrast);
|
|
let enhanced_range = calculate_range(&enhanced);
|
|
|
|
// Enhanced image should have equal or greater range
|
|
assert!(enhanced_range >= original_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clahe_preserves_dimensions() {
|
|
let image = GrayImage::new(256, 256);
|
|
let enhanced = apply_clahe(&image, 2.0);
|
|
|
|
assert_eq!(enhanced.width, 256);
|
|
assert_eq!(enhanced.height, 256);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clahe_different_clip_limits() {
|
|
let image = GrayImage::from_fn(50, 50, |x, y| ((x + y) % 256) as u8);
|
|
|
|
let clip_limits = vec![1.0, 2.0, 3.0, 4.0];
|
|
for limit in clip_limits {
|
|
let enhanced = apply_clahe(&image, limit);
|
|
assert_eq!(enhanced.width, image.width);
|
|
assert_eq!(enhanced.height, image.height);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipeline_chaining_blur_then_threshold() {
|
|
let image = GrayImage::from_fn(100, 100, |x, y| ((x + y) % 256) as u8);
|
|
|
|
// Chain operations
|
|
let blurred = gaussian_blur(&image, 1.0);
|
|
let threshold = otsu_threshold(&blurred);
|
|
let binary = apply_threshold(&blurred, threshold);
|
|
|
|
// Verify final result is binary
|
|
assert!(binary.data.iter().all(|&x| x == 0 || x == 255));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipeline_chaining_enhance_then_threshold() {
|
|
let image = GrayImage::from_fn(100, 100, |x, y| ((x + y) % 128) as u8);
|
|
|
|
// Chain CLAHE then threshold
|
|
let enhanced = apply_clahe(&image, 2.0);
|
|
let threshold = otsu_threshold(&enhanced);
|
|
let binary = apply_threshold(&enhanced, threshold);
|
|
|
|
assert!(binary.data.iter().all(|&x| x == 0 || x == 255));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipeline_full_preprocessing() {
|
|
let image = GrayImage::from_fn(100, 100, |x, y| ((x + y) % 256) as u8);
|
|
|
|
// Full pipeline: blur -> enhance -> threshold
|
|
let blurred = gaussian_blur(&image, 1.0);
|
|
let enhanced = apply_clahe(&blurred, 2.0);
|
|
let threshold = otsu_threshold(&enhanced);
|
|
let binary = apply_threshold(&enhanced, threshold);
|
|
|
|
assert_eq!(binary.width, image.width);
|
|
assert_eq!(binary.height, image.height);
|
|
assert!(binary.data.iter().all(|&x| x == 0 || x == 255));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipeline_preserves_dimensions_throughout() {
|
|
let image = GrayImage::new(200, 150);
|
|
|
|
let blurred = gaussian_blur(&image, 1.5);
|
|
assert_eq!((blurred.width, blurred.height), (200, 150));
|
|
|
|
let enhanced = apply_clahe(&blurred, 2.0);
|
|
assert_eq!((enhanced.width, enhanced.height), (200, 150));
|
|
|
|
let binary = apply_threshold(&enhanced, 128);
|
|
assert_eq!((binary.width, binary.height), (200, 150));
|
|
}
|
|
|
|
// Helper functions
|
|
fn calculate_range(image: &GrayImage) -> u8 {
|
|
let min = *image.data.iter().min().unwrap_or(&0);
|
|
let max = *image.data.iter().max().unwrap_or(&255);
|
|
max - min
|
|
}
|
|
|
|
#[test]
|
|
fn test_edge_case_empty_like_image() {
|
|
let tiny = GrayImage::new(1, 1);
|
|
assert_eq!(tiny.width, 1);
|
|
assert_eq!(tiny.height, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edge_case_large_image_dimensions() {
|
|
let large = GrayImage::new(4096, 4096);
|
|
assert_eq!(large.width, 4096);
|
|
assert_eq!(large.height, 4096);
|
|
}
|
|
}
|