1use crate::definitions::{HasBlack, HasWhite};
4use crate::integral_image::{integral_image, sum_image_pixels};
5use crate::stats::{cumulative_histogram, histogram};
6use image::{GrayImage, ImageBuffer, Luma};
7#[cfg(feature = "rayon")]
8use rayon::prelude::*;
9use std::cmp::{max, min};
10
11pub fn adaptive_threshold(image: &GrayImage, block_radius: u32) -> GrayImage {
17 assert!(block_radius > 0);
18 let integral = integral_image::<_, u32>(image);
19 let mut out = ImageBuffer::from_pixel(image.width(), image.height(), Luma::black());
20
21 for y in 0..image.height() {
22 for x in 0..image.width() {
23 let current_pixel = image.get_pixel(x, y);
24 let (y_low, y_high) = (
26 max(0, y as i32 - (block_radius as i32)) as u32,
27 min(image.height() - 1, y + block_radius),
28 );
29 let (x_low, x_high) = (
30 max(0, x as i32 - (block_radius as i32)) as u32,
31 min(image.width() - 1, x + block_radius),
32 );
33
34 let w = (y_high - y_low + 1) * (x_high - x_low + 1);
36 let mean = sum_image_pixels(&integral, x_low, y_low, x_high, y_high)[0] / w;
37
38 if current_pixel[0] as u32 >= mean as u32 {
39 out.put_pixel(x, y, Luma::white());
40 }
41 }
42 }
43 out
44}
45
46pub fn otsu_level(image: &GrayImage) -> u8 {
50 let hist = histogram(image);
51 let (width, height) = image.dimensions();
52 let total_weight = width * height;
53
54 let total_pixel_sum = hist.channels[0]
56 .iter()
57 .enumerate()
58 .fold(0f64, |sum, (t, h)| sum + (t as u32 * h) as f64);
59
60 let mut background_pixel_sum = 0f64;
62
63 let mut background_weight = 0u32;
67 let mut foreground_weight;
68
69 let mut largest_variance = 0f64;
70 let mut best_threshold = 0u8;
71
72 for (threshold, hist_count) in hist.channels[0].iter().enumerate() {
73 background_weight += hist_count;
74 if background_weight == 0 {
75 continue;
76 };
77
78 foreground_weight = total_weight - background_weight;
79 if foreground_weight == 0 {
80 break;
81 };
82
83 background_pixel_sum += (threshold as u32 * hist_count) as f64;
84 let foreground_pixel_sum = total_pixel_sum - background_pixel_sum;
85
86 let background_mean = background_pixel_sum / (background_weight as f64);
87 let foreground_mean = foreground_pixel_sum / (foreground_weight as f64);
88
89 let mean_diff_squared = (background_mean - foreground_mean).powi(2);
90 let intra_class_variance =
91 (background_weight as f64) * (foreground_weight as f64) * mean_diff_squared;
92
93 if intra_class_variance > largest_variance {
94 largest_variance = intra_class_variance;
95 best_threshold = threshold as u8;
96 }
97 }
98
99 best_threshold
100}
101
102pub fn threshold(image: &GrayImage, thresh: u8) -> GrayImage {
126 let mut out = image.clone();
127 threshold_mut(&mut out, thresh);
128 out
129}
130
131pub fn threshold_mut(image: &mut GrayImage, thresh: u8) {
157 for p in image.iter_mut() {
158 *p = if *p <= thresh { 0 } else { 255 };
159 }
160}
161
162pub fn equalize_histogram_mut(image: &mut GrayImage) {
165 let hist = cumulative_histogram(image).channels[0];
166 let total = hist[255] as f32;
167
168 #[cfg(feature = "rayon")]
169 let iter = image.par_iter_mut();
170 #[cfg(not(feature = "rayon"))]
171 let iter = image.iter_mut();
172
173 iter.for_each(|p| {
174 let fraction = unsafe { *hist.get_unchecked(*p as usize) as f32 / total };
181 *p = (f32::min(255f32, 255f32 * fraction)) as u8;
182 });
183}
184
185pub fn equalize_histogram(image: &GrayImage) -> GrayImage {
188 let mut out = image.clone();
189 equalize_histogram_mut(&mut out);
190 out
191}
192
193pub fn match_histogram_mut(image: &mut GrayImage, target: &GrayImage) {
196 let image_histc = cumulative_histogram(image).channels[0];
197 let target_histc = cumulative_histogram(target).channels[0];
198 let lut = histogram_lut(&image_histc, &target_histc);
199
200 for p in image.iter_mut() {
201 *p = lut[*p as usize] as u8;
202 }
203}
204
205pub fn match_histogram(image: &GrayImage, target: &GrayImage) -> GrayImage {
208 let mut out = image.clone();
209 match_histogram_mut(&mut out, target);
210 out
211}
212
213fn histogram_lut(source_histc: &[u32; 256], target_histc: &[u32; 256]) -> [usize; 256] {
216 let source_total = source_histc[255] as f32;
217 let target_total = target_histc[255] as f32;
218
219 let mut lut = [0usize; 256];
220 let mut y = 0usize;
221 let mut prev_target_fraction = 0f32;
222
223 for s in 0..256 {
224 let source_fraction = source_histc[s] as f32 / source_total;
225 let mut target_fraction = target_histc[y] as f32 / target_total;
226
227 while source_fraction > target_fraction && y < 255 {
228 y += 1;
229 prev_target_fraction = target_fraction;
230 target_fraction = target_histc[y] as f32 / target_total;
231 }
232
233 if y == 0 {
234 lut[s] = y;
235 } else {
236 let prev_dist = f32::abs(prev_target_fraction - source_fraction);
237 let dist = f32::abs(target_fraction - source_fraction);
238 if prev_dist < dist {
239 lut[s] = y - 1;
240 } else {
241 lut[s] = y;
242 }
243 }
244 }
245
246 lut
247}
248
249pub fn stretch_contrast(image: &GrayImage, lower: u8, upper: u8) -> GrayImage {
282 let mut out = image.clone();
283 stretch_contrast_mut(&mut out, lower, upper);
284 out
285}
286
287pub fn stretch_contrast_mut(image: &mut GrayImage, lower: u8, upper: u8) {
291 assert!(upper > lower, "upper must be strictly greater than lower");
292 let len = (upper - lower) as u16;
293 for p in image.iter_mut() {
294 if *p >= upper {
295 *p = 255;
296 } else if *p <= lower {
297 *p = 0;
298 } else {
299 let scaled = (255 * (*p as u16 - lower as u16)) / len;
300 *p = scaled as u8;
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::definitions::{HasBlack, HasWhite};
309 use crate::utils::gray_bench_image;
310 use image::{GrayImage, Luma};
311 use test::{black_box, Bencher};
312
313 #[test]
314 fn adaptive_threshold_constant() {
315 let image = GrayImage::from_pixel(3, 3, Luma([100u8]));
316 let binary = adaptive_threshold(&image, 1);
317 let expected = GrayImage::from_pixel(3, 3, Luma::white());
318 assert_pixels_eq!(expected, binary);
319 }
320
321 #[test]
322 fn adaptive_threshold_one_darker_pixel() {
323 for y in 0..3 {
324 for x in 0..3 {
325 let mut image = GrayImage::from_pixel(3, 3, Luma([200u8]));
326 image.put_pixel(x, y, Luma([100u8]));
327 let binary = adaptive_threshold(&image, 1);
328 let mut expected = GrayImage::from_pixel(3, 3, Luma::white());
330 expected.put_pixel(x, y, Luma::black());
331 assert_pixels_eq!(binary, expected);
332 }
333 }
334 }
335
336 #[test]
337 fn adaptive_threshold_one_lighter_pixel() {
338 for y in 0..5 {
339 for x in 0..5 {
340 let mut image = GrayImage::from_pixel(5, 5, Luma([100u8]));
341 image.put_pixel(x, y, Luma([200u8]));
342
343 let binary = adaptive_threshold(&image, 1);
344
345 for yb in 0..5 {
346 for xb in 0..5 {
347 let output_intensity = binary.get_pixel(xb, yb)[0];
348
349 let is_light_pixel = xb == x && yb == y;
350
351 let local_mean_includes_light_pixel =
352 (yb as i32 - y as i32).abs() <= 1 && (xb as i32 - x as i32).abs() <= 1;
353
354 if is_light_pixel {
355 assert_eq!(output_intensity, 255);
356 } else if local_mean_includes_light_pixel {
357 assert_eq!(output_intensity, 0);
358 } else {
359 assert_eq!(output_intensity, 255);
360 }
361 }
362 }
363 }
364 }
365 }
366
367 #[bench]
368 fn bench_adaptive_threshold(b: &mut Bencher) {
369 let image = gray_bench_image(200, 200);
370 let block_radius = 10;
371 b.iter(|| {
372 let thresholded = adaptive_threshold(&image, block_radius);
373 black_box(thresholded);
374 });
375 }
376
377 #[bench]
378 fn bench_match_histogram(b: &mut Bencher) {
379 let target = GrayImage::from_pixel(200, 200, Luma([150]));
380 let image = gray_bench_image(200, 200);
381 b.iter(|| {
382 let matched = match_histogram(&image, &target);
383 black_box(matched);
384 });
385 }
386
387 #[bench]
388 fn bench_match_histogram_mut(b: &mut Bencher) {
389 let target = GrayImage::from_pixel(200, 200, Luma([150]));
390 let mut image = gray_bench_image(200, 200);
391 b.iter(|| {
392 match_histogram_mut(&mut image, &target);
393 });
394 }
395
396 #[test]
397 fn test_histogram_lut_source_and_target_equal() {
398 let mut histc = [0u32; 256];
399 for i in 1..histc.len() {
400 histc[i] = 2 * i as u32;
401 }
402
403 let lut = histogram_lut(&histc, &histc);
404 let expected = (0..256).collect::<Vec<_>>();
405
406 assert_eq!(&lut[0..256], &expected[0..256]);
407 }
408
409 #[test]
410 fn test_histogram_lut_gradient_to_step_contrast() {
411 let mut grad_histc = [0u32; 256];
412 for i in 0..grad_histc.len() {
413 grad_histc[i] = i as u32;
414 }
415
416 let mut step_histc = [0u32; 256];
417 for i in 30..130 {
418 step_histc[i] = 100;
419 }
420 for i in 130..256 {
421 step_histc[i] = 200;
422 }
423
424 let lut = histogram_lut(&grad_histc, &step_histc);
425 let mut expected = [0usize; 256];
426
427 expected[0] = 0;
429
430 for i in 1..64 {
431 expected[i] = 29;
432 }
433 for i in 64..128 {
434 expected[i] = 30;
435 }
436 for i in 128..192 {
437 expected[i] = 129;
438 }
439 for i in 192..256 {
440 expected[i] = 130;
441 }
442
443 assert_eq!(&lut[0..256], &expected[0..256]);
444 }
445
446 fn constant_image(width: u32, height: u32, intensity: u8) -> GrayImage {
447 GrayImage::from_pixel(width, height, Luma([intensity]))
448 }
449
450 #[test]
451 fn test_otsu_constant() {
452 assert_eq!(otsu_level(&constant_image(10, 10, 0)), 0);
456 assert_eq!(otsu_level(&constant_image(10, 10, 128)), 0);
457 assert_eq!(otsu_level(&constant_image(10, 10, 255)), 0);
458 }
459
460 #[test]
461 fn test_otsu_level_gradient() {
462 let contents = (0u8..26u8).map(|x| x * 10u8).collect();
463 let image = GrayImage::from_raw(26, 1, contents).unwrap();
464 let level = otsu_level(&image);
465 assert_eq!(level, 120);
466 }
467
468 #[bench]
469 fn bench_otsu_level(b: &mut Bencher) {
470 let image = gray_bench_image(200, 200);
471 b.iter(|| {
472 let level = otsu_level(&image);
473 black_box(level);
474 });
475 }
476
477 #[test]
478 fn test_threshold_0_image_0() {
479 let expected = 0u8;
480 let actual = threshold(&constant_image(10, 10, 0), 0);
481 assert_pixels_eq!(actual, constant_image(10, 10, expected));
482 }
483
484 #[test]
485 fn test_threshold_0_image_1() {
486 let expected = 255u8;
487 let actual = threshold(&constant_image(10, 10, 1), 0);
488 assert_pixels_eq!(actual, constant_image(10, 10, expected));
489 }
490
491 #[test]
492 fn test_threshold_threshold_255_image_255() {
493 let expected = 0u8;
494 let actual = threshold(&constant_image(10, 10, 255), 255);
495 assert_pixels_eq!(actual, constant_image(10, 10, expected));
496 }
497
498 #[test]
499 fn test_threshold() {
500 let original_contents = (0u8..26u8).map(|x| x * 10u8).collect();
501 let original = GrayImage::from_raw(26, 1, original_contents).unwrap();
502
503 let expected_contents = vec![0u8; 13].into_iter().chain(vec![255u8; 13]).collect();
504
505 let expected = GrayImage::from_raw(26, 1, expected_contents).unwrap();
506
507 let actual = threshold(&original, 125u8);
508 assert_pixels_eq!(expected, actual);
509 }
510
511 #[bench]
512 fn bench_equalize_histogram(b: &mut Bencher) {
513 let image = gray_bench_image(500, 500);
514 b.iter(|| {
515 let equalized = equalize_histogram(&image);
516 black_box(equalized);
517 });
518 }
519
520 #[bench]
521 fn bench_equalize_histogram_mut(b: &mut Bencher) {
522 let mut image = gray_bench_image(500, 500);
523 b.iter(|| {
524 black_box(equalize_histogram_mut(&mut image));
525 });
526 }
527
528 #[bench]
529 fn bench_threshold(b: &mut Bencher) {
530 let image = gray_bench_image(500, 500);
531 b.iter(|| {
532 let thresholded = threshold(&image, 125);
533 black_box(thresholded);
534 });
535 }
536
537 #[bench]
538 fn bench_threshold_mut(b: &mut Bencher) {
539 let mut image = gray_bench_image(500, 500);
540 b.iter(|| {
541 black_box(threshold_mut(&mut image, 125));
542 });
543 }
544
545 #[bench]
546 fn bench_stretch_contrast(b: &mut Bencher) {
547 let image = gray_bench_image(500, 500);
548 b.iter(|| {
549 let stretched = stretch_contrast(&image, 20, 80);
550 black_box(stretched);
551 });
552 }
553
554 #[bench]
555 fn bench_stretch_contrast_mut(b: &mut Bencher) {
556 let mut image = gray_bench_image(500, 500);
557 b.iter(|| {
558 black_box(stretch_contrast_mut(&mut image, 20, 80));
559 });
560 }
561}