ruvector_scipix/preprocess/
transforms.rs1use super::{PreprocessError, Result};
4use image::{DynamicImage, GrayImage, Luma};
5use imageproc::filter::gaussian_blur_f32;
6use std::f32;
7
8pub fn to_grayscale(image: &DynamicImage) -> GrayImage {
16 image.to_luma8()
17}
18
19pub fn gaussian_blur(image: &GrayImage, sigma: f32) -> Result<GrayImage> {
36 if sigma <= 0.0 {
37 return Err(PreprocessError::InvalidParameters(
38 "Sigma must be positive".to_string(),
39 ));
40 }
41
42 Ok(gaussian_blur_f32(image, sigma))
43}
44
45pub fn sharpen(image: &GrayImage, sigma: f32, amount: f32) -> Result<GrayImage> {
55 if sigma <= 0.0 || amount < 0.0 {
56 return Err(PreprocessError::InvalidParameters(
57 "Invalid sharpening parameters".to_string(),
58 ));
59 }
60
61 let blurred = gaussian_blur_f32(image, sigma);
62 let (width, height) = image.dimensions();
63 let mut result = GrayImage::new(width, height);
64
65 for y in 0..height {
66 for x in 0..width {
67 let original = image.get_pixel(x, y)[0] as f32;
68 let blur = blurred.get_pixel(x, y)[0] as f32;
69
70 let sharpened = original + amount * (original - blur);
72 let clamped = sharpened.clamp(0.0, 255.0) as u8;
73
74 result.put_pixel(x, y, Luma([clamped]));
75 }
76 }
77
78 Ok(result)
79}
80
81pub fn otsu_threshold(image: &GrayImage) -> Result<u8> {
101 let mut histogram = [0u32; 256];
103 for pixel in image.pixels() {
104 histogram[pixel[0] as usize] += 1;
105 }
106
107 let total_pixels = (image.width() * image.height()) as f64;
108
109 let mut sum_total = 0.0;
111 for (i, &count) in histogram.iter().enumerate() {
112 sum_total += (i as f64) * (count as f64);
113 }
114
115 let mut sum_background = 0.0;
116 let mut weight_background = 0.0;
117 let mut max_variance = 0.0;
118 let mut threshold = 0u8;
119
120 for (t, &count) in histogram.iter().enumerate() {
122 weight_background += count as f64;
123 if weight_background == 0.0 {
124 continue;
125 }
126
127 let weight_foreground = total_pixels - weight_background;
128 if weight_foreground == 0.0 {
129 break;
130 }
131
132 sum_background += (t as f64) * (count as f64);
133
134 let mean_background = sum_background / weight_background;
135 let mean_foreground = (sum_total - sum_background) / weight_foreground;
136
137 let variance = weight_background * weight_foreground *
139 (mean_background - mean_foreground).powi(2);
140
141 if variance > max_variance {
142 max_variance = variance;
143 threshold = t as u8;
144 }
145 }
146
147 Ok(threshold)
148}
149
150pub fn threshold(image: &GrayImage, threshold_val: u8) -> GrayImage {
159 let (width, height) = image.dimensions();
160 let mut result = GrayImage::new(width, height);
161
162 for y in 0..height {
163 for x in 0..width {
164 let pixel = image.get_pixel(x, y)[0];
165 let value = if pixel >= threshold_val { 255 } else { 0 };
166 result.put_pixel(x, y, Luma([value]));
167 }
168 }
169
170 result
171}
172
173pub fn adaptive_threshold(image: &GrayImage, window_size: u32) -> Result<GrayImage> {
193 if window_size % 2 == 0 {
194 return Err(PreprocessError::InvalidParameters(
195 "Window size must be odd".to_string(),
196 ));
197 }
198
199 let (width, height) = image.dimensions();
200 let mut result = GrayImage::new(width, height);
201 let half_window = (window_size / 2) as i32;
202
203 let integral = compute_integral_image(image);
205
206 for y in 0..height as i32 {
207 for x in 0..width as i32 {
208 let x1 = (x - half_window).max(0);
210 let y1 = (y - half_window).max(0);
211 let x2 = (x + half_window + 1).min(width as i32);
212 let y2 = (y + half_window + 1).min(height as i32);
213
214 let area = ((x2 - x1) * (y2 - y1)) as f64;
216 let sum = get_integral_sum(&integral, x1, y1, x2, y2);
217 let mean = (sum as f64 / area) as u8;
218
219 let pixel = image.get_pixel(x as u32, y as u32)[0];
221 let bias = 5; let value = if pixel >= mean.saturating_sub(bias) { 255 } else { 0 };
223
224 result.put_pixel(x as u32, y as u32, Luma([value]));
225 }
226 }
227
228 Ok(result)
229}
230
231fn compute_integral_image(image: &GrayImage) -> Vec<Vec<u64>> {
233 let (width, height) = image.dimensions();
234 let mut integral = vec![vec![0u64; width as usize + 1]; height as usize + 1];
235
236 for y in 1..=height as usize {
237 for x in 1..=width as usize {
238 let pixel = image.get_pixel(x as u32 - 1, y as u32 - 1)[0] as u64;
239 integral[y][x] = pixel
240 + integral[y - 1][x]
241 + integral[y][x - 1]
242 - integral[y - 1][x - 1];
243 }
244 }
245
246 integral
247}
248
249fn get_integral_sum(integral: &[Vec<u64>], x1: i32, y1: i32, x2: i32, y2: i32) -> u64 {
251 let x1 = x1 as usize;
252 let y1 = y1 as usize;
253 let x2 = x2 as usize;
254 let y2 = y2 as usize;
255
256 integral[y2][x2] + integral[y1][x1] - integral[y1][x2] - integral[y2][x1]
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use approx::assert_relative_eq;
263
264 fn create_gradient_image(width: u32, height: u32) -> GrayImage {
265 let mut img = GrayImage::new(width, height);
266 for y in 0..height {
267 for x in 0..width {
268 let val = ((x + y) * 255 / (width + height)) as u8;
269 img.put_pixel(x, y, Luma([val]));
270 }
271 }
272 img
273 }
274
275 #[test]
276 fn test_to_grayscale() {
277 let img = DynamicImage::new_rgb8(100, 100);
278 let gray = to_grayscale(&img);
279 assert_eq!(gray.dimensions(), (100, 100));
280 }
281
282 #[test]
283 fn test_gaussian_blur() {
284 let img = create_gradient_image(50, 50);
285 let blurred = gaussian_blur(&img, 1.0);
286 assert!(blurred.is_ok());
287
288 let result = blurred.unwrap();
289 assert_eq!(result.dimensions(), img.dimensions());
290 }
291
292 #[test]
293 fn test_gaussian_blur_invalid_sigma() {
294 let img = create_gradient_image(50, 50);
295 let result = gaussian_blur(&img, -1.0);
296 assert!(result.is_err());
297 }
298
299 #[test]
300 fn test_sharpen() {
301 let img = create_gradient_image(50, 50);
302 let sharpened = sharpen(&img, 1.0, 1.5);
303 assert!(sharpened.is_ok());
304
305 let result = sharpened.unwrap();
306 assert_eq!(result.dimensions(), img.dimensions());
307 }
308
309 #[test]
310 fn test_otsu_threshold() {
311 let mut img = GrayImage::new(100, 100);
313 for y in 0..100 {
314 for x in 0..100 {
315 let val = if x < 50 { 50 } else { 200 };
316 img.put_pixel(x, y, Luma([val]));
317 }
318 }
319
320 let threshold = otsu_threshold(&img);
321 assert!(threshold.is_ok());
322
323 let t = threshold.unwrap();
324 assert!(t >= 50 && t <= 200, "threshold {} should be between 50 and 200", t);
327 }
328
329 #[test]
330 fn test_threshold() {
331 let img = create_gradient_image(100, 100);
332 let binary = threshold(&img, 128);
333
334 assert_eq!(binary.dimensions(), img.dimensions());
335
336 for pixel in binary.pixels() {
338 let val = pixel[0];
339 assert!(val == 0 || val == 255);
340 }
341 }
342
343 #[test]
344 fn test_adaptive_threshold() {
345 let img = create_gradient_image(100, 100);
346 let binary = adaptive_threshold(&img, 15);
347 assert!(binary.is_ok());
348
349 let result = binary.unwrap();
350 assert_eq!(result.dimensions(), img.dimensions());
351
352 for pixel in result.pixels() {
354 let val = pixel[0];
355 assert!(val == 0 || val == 255);
356 }
357 }
358
359 #[test]
360 fn test_adaptive_threshold_invalid_window() {
361 let img = create_gradient_image(50, 50);
362 let result = adaptive_threshold(&img, 16); assert!(result.is_err());
364 }
365
366 #[test]
367 fn test_integral_image() {
368 let mut img = GrayImage::new(3, 3);
369 for y in 0..3 {
370 for x in 0..3 {
371 img.put_pixel(x, y, Luma([1]));
372 }
373 }
374
375 let integral = compute_integral_image(&img);
376
377 let sum = get_integral_sum(&integral, 0, 0, 3, 3);
379 assert_eq!(sum, 9); }
381
382 #[test]
383 fn test_threshold_extremes() {
384 let img = create_gradient_image(100, 100);
385
386 let binary = threshold(&img, 0);
388 assert!(binary.pixels().all(|p| p[0] == 255));
389
390 let binary = threshold(&img, 255);
392 assert!(binary.pixels().all(|p| p[0] == 0));
393 }
394}