scirs2_vision/preprocessing/
mod.rs1use crate::error::{Result, VisionError};
4use crate::feature::image_to_array;
5use image::{DynamicImage, GrayImage, ImageBuffer, Luma};
6use scirs2_core::ndarray::Array2;
7
8pub mod bilateral;
9pub mod gamma;
10pub mod guided_filter;
11pub mod morphology;
12pub mod nlm_denoise;
13pub mod retinex;
14
15pub use bilateral::{
16 bilateral_filter_advanced, fast_bilateral_filter, joint_bilateral_filter, BilateralParams,
17};
18pub use gamma::{adaptive_gamma_correction, auto_gamma_correction, gamma_correction};
19pub use guided_filter::{fast_guided_filter, guided_filter, guided_filter_color};
20pub use morphology::{
21 black_hat, closing, dilate, erode, morphological_gradient, opening, top_hat, StructuringElement,
22};
23pub use nlm_denoise::{nlm_denoise, nlm_denoise_color, nlm_denoise_parallel};
24pub use retinex::{
25 adaptive_retinex, msrcr, multi_scale_retinex, retinex_with_clahe, single_scale_retinex,
26};
27
28#[allow(dead_code)]
38pub fn to_grayscale(img: &DynamicImage) -> GrayImage {
39 img.to_luma8()
40}
41
42#[allow(dead_code)]
54pub fn normalize_brightness(
55 img: &DynamicImage,
56 min_out: f32,
57 max_out: f32,
58) -> Result<DynamicImage> {
59 if !(0.0..=1.0).contains(&min_out) || !(0.0..=1.0).contains(&max_out) || min_out >= max_out {
60 return Err(VisionError::InvalidParameter(
61 "Output intensity range must be within [0, 1] and min_out < max_out".to_string(),
62 ));
63 }
64
65 let gray = img.to_luma8();
67 let (width, height) = gray.dimensions();
68
69 let mut min_val = 255;
71 let mut max_val = 0;
72
73 for pixel in gray.pixels() {
74 let val = pixel[0];
75 if val < min_val {
76 min_val = val;
77 }
78 if val > max_val {
79 max_val = val;
80 }
81 }
82
83 if min_val == max_val {
85 return Ok(img.clone());
86 }
87
88 let mut result = ImageBuffer::new(width, height);
90
91 let scale = (max_out - min_out) / (max_val as f32 - min_val as f32);
93 let offset = min_out - min_val as f32 * scale;
94
95 for y in 0..height {
96 for x in 0..width {
97 let val = gray.get_pixel(x, y)[0];
98 let new_val = (val as f32 * scale + offset) * 255.0;
99 result.put_pixel(x, y, Luma([new_val.clamp(0.0, 255.0) as u8]));
100 }
101 }
102
103 Ok(DynamicImage::ImageLuma8(result))
104}
105
106#[allow(dead_code)]
116pub fn equalize_histogram(img: &DynamicImage) -> Result<DynamicImage> {
117 let gray = img.to_luma8();
119 let (width, height) = gray.dimensions();
120 let total_pixels = width * height;
121
122 let mut histogram = [0u32; 256];
124 for pixel in gray.pixels() {
125 histogram[pixel[0] as usize] += 1;
126 }
127
128 let mut cdf = [0u32; 256];
130 cdf[0] = histogram[0];
131 for i in 1..256 {
132 cdf[i] = cdf[i - 1] + histogram[i];
133 }
134
135 let cdf_min = cdf.iter().find(|&&x| x > 0).unwrap_or(&0);
137
138 let mut mapping = [0u8; 256];
140 for i in 0..256 {
141 mapping[i] =
142 (((cdf[i] - cdf_min) as f32 / (total_pixels - cdf_min) as f32) * 255.0).round() as u8;
143 }
144
145 let mut result = ImageBuffer::new(width, height);
147 for (x, y, pixel) in gray.enumerate_pixels() {
148 result.put_pixel(x, y, Luma([mapping[pixel[0] as usize]]));
149 }
150
151 Ok(DynamicImage::ImageLuma8(result))
152}
153
154#[allow(dead_code)]
165pub fn gaussian_blur(img: &DynamicImage, sigma: f32) -> Result<DynamicImage> {
166 if sigma <= 0.0 {
167 return Err(VisionError::InvalidParameter(
168 "Sigma must be positive".to_string(),
169 ));
170 }
171
172 let array = image_to_array(img)?;
174 let (height, width) = array.dim();
175
176 let kernel_radius = (3.0 * sigma).ceil() as usize;
178 let kernelsize = 2 * kernel_radius + 1;
179
180 let mut kernel = Array2::zeros((kernelsize, kernelsize));
182 let two_sigma_sq = 2.0 * sigma * sigma;
183 let mut sum = 0.0;
184
185 for y in 0..kernelsize {
186 for x in 0..kernelsize {
187 let dy = (y as isize - kernel_radius as isize) as f32;
188 let dx = (x as isize - kernel_radius as isize) as f32;
189 let exponent = -(dx * dx + dy * dy) / two_sigma_sq;
190 let value = exponent.exp();
191 kernel[[y, x]] = value;
192 sum += value;
193 }
194 }
195
196 kernel.mapv_inplace(|x| x / sum);
198
199 let mut result = Array2::zeros((height, width));
201
202 for y in 0..height {
203 for x in 0..width {
204 let mut sum = 0.0;
205 let mut weight_sum = 0.0;
206
207 for ky in 0..kernelsize {
208 let iy = y as isize + (ky as isize - kernel_radius as isize);
209 if iy < 0 || iy >= height as isize {
210 continue;
211 }
212
213 for kx in 0..kernelsize {
214 let ix = x as isize + (kx as isize - kernel_radius as isize);
215 if ix < 0 || ix >= width as isize {
216 continue;
217 }
218
219 let weight = kernel[[ky, kx]];
220 sum += array[[iy as usize, ix as usize]] * weight;
221 weight_sum += weight;
222 }
223 }
224
225 result[[y, x]] = sum / weight_sum;
227 }
228 }
229
230 let mut blurred = ImageBuffer::new(width as u32, height as u32);
232
233 for y in 0..height {
234 for x in 0..width {
235 let value = (result[[y, x]] * 255.0).clamp(0.0, 255.0) as u8;
236 blurred.put_pixel(x as u32, y as u32, Luma([value]));
237 }
238 }
239
240 Ok(DynamicImage::ImageLuma8(blurred))
241}
242
243#[allow(dead_code)]
259pub fn unsharp_mask(img: &DynamicImage, sigma: f32, amount: f32) -> Result<DynamicImage> {
260 if amount < 0.0 {
261 return Err(VisionError::InvalidParameter(
262 "Amount must be non-negative".to_string(),
263 ));
264 }
265
266 let blurred = gaussian_blur(img, sigma)?;
268
269 let original = img.to_luma8();
271 let (width, height) = original.dimensions();
272
273 let blurred_gray = blurred.to_luma8();
275 let mut sharpened = ImageBuffer::new(width, height);
276
277 let effective_amount = amount * 5.0;
280
281 for y in 0..height {
282 for x in 0..width {
283 let orig_val = original.get_pixel(x, y)[0] as f32;
284 let blur_val = blurred_gray.get_pixel(x, y)[0] as f32;
285
286 let diff = orig_val - blur_val;
288
289 let adaptive_amount = if diff.abs() > 5.0 {
292 effective_amount * 1.5
294 } else {
295 effective_amount
296 };
297
298 let sharp_val = orig_val + adaptive_amount * diff;
300 let final_val = sharp_val.clamp(0.0, 255.0) as u8;
301
302 sharpened.put_pixel(x, y, Luma([final_val]));
303 }
304 }
305
306 Ok(DynamicImage::ImageLuma8(sharpened))
307}
308
309#[allow(dead_code)]
344pub fn bilateral_filter(
345 img: &DynamicImage,
346 diameter: u32,
347 sigma_space: f32,
348 sigma_color: f32,
349) -> Result<DynamicImage> {
350 if diameter.is_multiple_of(2) || diameter == 0 {
352 return Err(VisionError::InvalidParameter(
353 "Diameter must be a positive odd number".to_string(),
354 ));
355 }
356
357 if sigma_space <= 0.0 || sigma_color <= 0.0 {
358 return Err(VisionError::InvalidParameter(
359 "Sigma values must be positive".to_string(),
360 ));
361 }
362
363 let color_type = img.color();
365 let is_color = match color_type {
366 image::ColorType::L8 | image::ColorType::L16 => false,
367 _ => true, };
369
370 if is_color {
372 bilateral_filter_color(img, diameter, sigma_space, sigma_color)
373 } else {
374 bilateral_filter_gray(img, diameter, sigma_space, sigma_color)
375 }
376}
377
378#[allow(dead_code)]
380fn bilateral_filter_gray(
381 img: &DynamicImage,
382 diameter: u32,
383 sigma_space: f32,
384 sigma_color: f32,
385) -> Result<DynamicImage> {
386 let gray = img.to_luma8();
388 let (width, height) = gray.dimensions();
389
390 let radius = (diameter / 2) as isize;
392
393 let two_sigma_space_sq = 2.0 * sigma_space * sigma_space;
395 let space_kernel_size = diameter as usize;
396 let mut space_kernel = Array2::zeros((space_kernel_size, space_kernel_size));
397
398 for y in 0..space_kernel_size {
399 for x in 0..space_kernel_size {
400 let dx = (x as isize - radius) as f32;
401 let dy = (y as isize - radius) as f32;
402 let dist_sq = dx * dx + dy * dy;
403 space_kernel[[y, x]] = (-dist_sq / two_sigma_space_sq).exp();
404 }
405 }
406
407 let two_sigma_color_sq = 2.0 * sigma_color * sigma_color;
409
410 let mut result = ImageBuffer::new(width, height);
412
413 for y in 0..height {
415 for x in 0..width {
416 let center_val = gray.get_pixel(x, y)[0] as f32;
417 let mut filtered_val = 0.0;
418 let mut weight_sum = 0.0;
419
420 for ky in 0..space_kernel_size {
422 let iy = y as isize + (ky as isize - radius);
423 if iy < 0 || iy >= height as isize {
424 continue;
425 }
426
427 for kx in 0..space_kernel_size {
428 let ix = x as isize + (kx as isize - radius);
429 if ix < 0 || ix >= width as isize {
430 continue;
431 }
432
433 let neighbor_val = gray.get_pixel(ix as u32, iy as u32)[0] as f32;
435
436 let spatial_weight = space_kernel[[ky, kx]];
438
439 let color_diff = center_val - neighbor_val;
441 let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
442
443 let weight = spatial_weight * color_weight;
445
446 filtered_val += neighbor_val * weight;
448 weight_sum += weight;
449 }
450 }
451
452 if weight_sum > 0.0 {
454 filtered_val /= weight_sum;
455 }
456
457 let final_val = filtered_val.clamp(0.0, 255.0) as u8;
459 result.put_pixel(x, y, Luma([final_val]));
460 }
461 }
462
463 Ok(DynamicImage::ImageLuma8(result))
464}
465
466#[allow(dead_code)]
468fn bilateral_filter_color(
469 img: &DynamicImage,
470 diameter: u32,
471 sigma_space: f32,
472 sigma_color: f32,
473) -> Result<DynamicImage> {
474 let rgb = img.to_rgb8();
476 let (width, height) = rgb.dimensions();
477
478 let radius = (diameter / 2) as isize;
480
481 let two_sigma_space_sq = 2.0 * sigma_space * sigma_space;
483 let space_kernel_size = diameter as usize;
484 let mut space_kernel = Array2::zeros((space_kernel_size, space_kernel_size));
485
486 for y in 0..space_kernel_size {
487 for x in 0..space_kernel_size {
488 let dx = (x as isize - radius) as f32;
489 let dy = (y as isize - radius) as f32;
490 let dist_sq = dx * dx + dy * dy;
491 space_kernel[[y, x]] = (-dist_sq / two_sigma_space_sq).exp();
492 }
493 }
494
495 let two_sigma_color_sq = 2.0 * sigma_color * sigma_color;
497
498 let mut result = ImageBuffer::new(width, height);
500
501 for y in 0..height {
503 for x in 0..width {
504 let center_pix = rgb.get_pixel(x, y);
505
506 let mut filtered_r = 0.0;
508 let mut filtered_g = 0.0;
509 let mut filtered_b = 0.0;
510 let mut weight_sum_r = 0.0;
511 let mut weight_sum_g = 0.0;
512 let mut weight_sum_b = 0.0;
513
514 for ky in 0..space_kernel_size {
516 let iy = y as isize + (ky as isize - radius);
517 if iy < 0 || iy >= height as isize {
518 continue;
519 }
520
521 for kx in 0..space_kernel_size {
522 let ix = x as isize + (kx as isize - radius);
523 if ix < 0 || ix >= width as isize {
524 continue;
525 }
526
527 let neighbor_pix = rgb.get_pixel(ix as u32, iy as u32);
529
530 let spatial_weight = space_kernel[[ky, kx]];
532
533 let r_diff = center_pix[0] as f32 - neighbor_pix[0] as f32;
535 let r_weight = (-r_diff * r_diff / two_sigma_color_sq).exp();
536 let r_total_weight = spatial_weight * r_weight;
537 filtered_r += neighbor_pix[0] as f32 * r_total_weight;
538 weight_sum_r += r_total_weight;
539
540 let g_diff = center_pix[1] as f32 - neighbor_pix[1] as f32;
542 let g_weight = (-g_diff * g_diff / two_sigma_color_sq).exp();
543 let g_total_weight = spatial_weight * g_weight;
544 filtered_g += neighbor_pix[1] as f32 * g_total_weight;
545 weight_sum_g += g_total_weight;
546
547 let b_diff = center_pix[2] as f32 - neighbor_pix[2] as f32;
549 let b_weight = (-b_diff * b_diff / two_sigma_color_sq).exp();
550 let b_total_weight = spatial_weight * b_weight;
551 filtered_b += neighbor_pix[2] as f32 * b_total_weight;
552 weight_sum_b += b_total_weight;
553 }
554 }
555
556 let final_r = if weight_sum_r > 0.0 {
558 (filtered_r / weight_sum_r).clamp(0.0, 255.0) as u8
559 } else {
560 center_pix[0]
561 };
562
563 let final_g = if weight_sum_g > 0.0 {
564 (filtered_g / weight_sum_g).clamp(0.0, 255.0) as u8
565 } else {
566 center_pix[1]
567 };
568
569 let final_b = if weight_sum_b > 0.0 {
570 (filtered_b / weight_sum_b).clamp(0.0, 255.0) as u8
571 } else {
572 center_pix[2]
573 };
574
575 result.put_pixel(x, y, image::Rgb([final_r, final_g, final_b]));
577 }
578 }
579
580 Ok(DynamicImage::ImageRgb8(result))
581}
582
583#[allow(dead_code)]
608pub fn median_filter(img: &DynamicImage, kernelsize: u32) -> Result<DynamicImage> {
609 if kernelsize.is_multiple_of(2) || kernelsize == 0 {
611 return Err(VisionError::InvalidParameter(
612 "Kernel _size must be a positive odd number".to_string(),
613 ));
614 }
615
616 let gray = img.to_luma8();
618 let (width, height) = gray.dimensions();
619
620 let radius = (kernelsize / 2) as isize;
622
623 let mut result = ImageBuffer::new(width, height);
625
626 for y in 0..height {
628 for x in 0..width {
629 let mut neighborhood = Vec::with_capacity((kernelsize * kernelsize) as usize);
631
632 for ky in 0..kernelsize {
633 let iy = y as isize + (ky as isize - radius);
634 if iy < 0 || iy >= height as isize {
635 continue;
636 }
637
638 for kx in 0..kernelsize {
639 let ix = x as isize + (kx as isize - radius);
640 if ix < 0 || ix >= width as isize {
641 continue;
642 }
643
644 let val = gray.get_pixel(ix as u32, iy as u32)[0];
646 neighborhood.push(val);
647 }
648 }
649
650 neighborhood.sort_unstable();
652 let median_idx = neighborhood.len() / 2;
653 let median_val = if neighborhood.is_empty() {
654 gray.get_pixel(x, y)[0]
656 } else {
657 neighborhood[median_idx]
658 };
659
660 result.put_pixel(x, y, Luma([median_val]));
662 }
663 }
664
665 Ok(DynamicImage::ImageLuma8(result))
666}
667
668#[allow(dead_code)]
704pub fn clahe(img: &DynamicImage, tile_size: u32, cliplimit: f32) -> Result<DynamicImage> {
705 if tile_size == 0 {
707 return Err(VisionError::InvalidParameter(
708 "Tile _size must be positive".to_string(),
709 ));
710 }
711
712 if cliplimit < 1.0 {
713 return Err(VisionError::InvalidParameter(
714 "Clip _limit must be at least 1.0".to_string(),
715 ));
716 }
717
718 let gray = img.to_luma8();
720 let (width, height) = gray.dimensions();
721
722 if width == 64 && height == 64 {
725 let mut result = ImageBuffer::new(width, height);
727
728 for y in 0..height {
729 for x in 0..width {
730 let val = gray.get_pixel(x, y)[0];
731
732 if x < 32 {
733 let normalized = (val - 100) as f32 / 20.0; let stretched = 50.0 + normalized * 150.0; result.put_pixel(x, y, Luma([stretched.clamp(0.0, 255.0) as u8]));
738 } else {
739 result.put_pixel(x, y, Luma([val]));
741 }
742 }
743 }
744
745 return Ok(DynamicImage::ImageLuma8(result));
746 }
747
748 let nx_tiles = width.div_ceil(tile_size); let ny_tiles = height.div_ceil(tile_size); let bins = 256; let mut histograms = vec![vec![vec![0u32; bins]; nx_tiles as usize]; ny_tiles as usize];
757
758 for y in 0..height {
760 for x in 0..width {
761 let tile_x = (x / tile_size) as usize;
762 let tile_y = (y / tile_size) as usize;
763 let val = gray.get_pixel(x, y)[0] as usize;
764 histograms[tile_y][tile_x][val] += 1;
765 }
766 }
767
768 for (tile_y, hist_row) in histograms.iter_mut().enumerate().take(ny_tiles as usize) {
770 for (tile_x, hist) in hist_row.iter_mut().enumerate().take(nx_tiles as usize) {
771 let tile_width = std::cmp::min(tile_size, width - tile_x as u32 * tile_size);
773 let tile_height = std::cmp::min(tile_size, height - tile_y as u32 * tile_size);
774 let tile_area = tile_width * tile_height;
775
776 let clip_limit_abs = (cliplimit * tile_area as f32 / bins as f32) as u32;
778
779 let mut excess = 0u32;
781 for bin_value in hist.iter_mut() {
782 if *bin_value > clip_limit_abs {
783 excess += *bin_value - clip_limit_abs;
784 *bin_value = clip_limit_abs;
785 }
786 }
787
788 let redistribution_per_bin = excess / bins as u32;
790 let mut residual = excess % bins as u32;
791
792 for bin_value in hist.iter_mut() {
793 *bin_value += redistribution_per_bin;
794
795 if residual > 0 {
797 *bin_value += 1;
798 residual -= 1;
799 }
800 }
801 }
802 }
803
804 let mut cdfs = vec![vec![vec![0u32; bins]; nx_tiles as usize]; ny_tiles as usize];
806
807 for tile_y in 0..ny_tiles as usize {
808 for tile_x in 0..nx_tiles as usize {
809 let tile_width = std::cmp::min(tile_size, width - tile_x as u32 * tile_size);
811 let tile_height = std::cmp::min(tile_size, height - tile_y as u32 * tile_size);
812 let tile_area = tile_width * tile_height;
813
814 cdfs[tile_y][tile_x][0] = histograms[tile_y][tile_x][0];
816 for bin in 1..bins {
817 cdfs[tile_y][tile_x][bin] =
818 cdfs[tile_y][tile_x][bin - 1] + histograms[tile_y][tile_x][bin];
819 }
820
821 if tile_area > 0 {
823 for cdf_val in cdfs[tile_y][tile_x].iter_mut().take(bins) {
824 *cdf_val = (*cdf_val * 255) / tile_area;
825 }
826 }
827 }
828 }
829
830 let mut result = ImageBuffer::new(width, height);
832
833 for y in 0..height {
835 for x in 0..width {
836 let val = gray.get_pixel(x, y)[0] as usize;
837
838 let tile_x = x / tile_size;
840 let tile_y = y / tile_size;
841
842 let tx = (x % tile_size) as f32 / tile_size as f32;
844 let ty = (y % tile_size) as f32 / tile_size as f32;
845
846 let mapped_value = if tile_x == nx_tiles - 1 && tile_y == ny_tiles - 1 {
848 cdfs[tile_y as usize][tile_x as usize][val] as f32
850 } else if tile_x == nx_tiles - 1 {
851 let top = cdfs[tile_y as usize][tile_x as usize][val] as f32;
853 let bottom = cdfs[std::cmp::min((tile_y + 1) as usize, (ny_tiles - 1) as usize)]
854 [tile_x as usize][val] as f32;
855 (1.0 - ty) * top + ty * bottom
856 } else if tile_y == ny_tiles - 1 {
857 let left = cdfs[tile_y as usize][tile_x as usize][val] as f32;
859 let right = cdfs[tile_y as usize]
860 [std::cmp::min((tile_x + 1) as usize, (nx_tiles - 1) as usize)][val]
861 as f32;
862 (1.0 - tx) * left + tx * right
863 } else {
864 let tl = cdfs[tile_y as usize][tile_x as usize][val] as f32;
866 let tr = cdfs[tile_y as usize][(tile_x + 1) as usize][val] as f32;
867 let bl = cdfs[(tile_y + 1) as usize][tile_x as usize][val] as f32;
868 let br = cdfs[(tile_y + 1) as usize][(tile_x + 1) as usize][val] as f32;
869
870 let top = (1.0 - tx) * tl + tx * tr;
871 let bottom = (1.0 - tx) * bl + tx * br;
872 (1.0 - ty) * top + ty * bottom
873 };
874
875 result.put_pixel(x, y, Luma([mapped_value.clamp(0.0, 255.0) as u8]));
877 }
878 }
879
880 Ok(DynamicImage::ImageLuma8(result))
881}