imageproc/
contrast.rs

1//! Functions for manipulating the contrast of images.
2
3use 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
11/// Applies an adaptive threshold to an image.
12///
13/// This algorithm compares each pixel's brightness with the average brightness of the pixels
14/// in the (2 * `block_radius` + 1) square block centered on it. If the pixel is at least as bright
15/// as the threshold then it will have a value of 255 in the output image, otherwise 0.
16pub 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            // Traverse all neighbors in (2 * block_radius + 1) x (2 * block_radius + 1)
25            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            // Number of pixels in the block, adjusted for edge cases.
35            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
46/// Returns the [Otsu threshold level] of an 8bpp image.
47///
48/// [Otsu threshold level]: https://en.wikipedia.org/wiki/Otsu%27s_method
49pub 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    // Sum of all pixel intensities, to use when calculating means.
55    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    // Sum of all pixel intensities in the background class.
61    let mut background_pixel_sum = 0f64;
62
63    // The weight of a class (background or foreground) is
64    // the number of pixels which belong to that class at
65    // the current threshold.
66    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
102/// Returns a binarized image from an input 8bpp grayscale image
103/// obtained by applying the given threshold. Pixels with intensity
104/// equal to the threshold are assigned to the background.
105///
106/// # Examples
107/// ```
108/// # extern crate image;
109/// # #[macro_use]
110/// # extern crate imageproc;
111/// # fn main() {
112/// use imageproc::contrast::threshold;
113///
114/// let image = gray_image!(
115///     10, 80, 20;
116///     50, 90, 70);
117///
118/// let thresholded = gray_image!(
119///     0, 255,   0;
120///     0, 255, 255);
121///
122/// assert_pixels_eq!(threshold(&image, 50), thresholded);
123/// # }
124/// ```
125pub fn threshold(image: &GrayImage, thresh: u8) -> GrayImage {
126    let mut out = image.clone();
127    threshold_mut(&mut out, thresh);
128    out
129}
130
131/// Mutates given image to form a binarized version produced by applying
132/// the given threshold. Pixels with intensity
133/// equal to the threshold are assigned to the background.
134///
135/// # Examples
136/// ```
137/// # extern crate image;
138/// # #[macro_use]
139/// # extern crate imageproc;
140/// # fn main() {
141/// use imageproc::contrast::threshold_mut;
142///
143/// let mut image = gray_image!(
144///     10, 80, 20;
145///     50, 90, 70);
146///
147/// let thresholded = gray_image!(
148///     0, 255,   0;
149///     0, 255, 255);
150///
151/// threshold_mut(&mut image, 50);
152///
153/// assert_pixels_eq!(image, thresholded);
154/// # }
155/// ```
156pub 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
162/// Equalises the histogram of an 8bpp grayscale image in place. See also
163/// [histogram equalization (wikipedia)](https://en.wikipedia.org/wiki/Histogram_equalization).
164pub 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        // JUSTIFICATION
175        //  Benefit
176        //      Using checked indexing here makes this function take 1.1x longer, as measured
177        //      by bench_equalize_histogram_mut
178        //  Correctness
179        //      Each channel of CumulativeChannelHistogram has length 256, and a GrayImage has 8 bits per pixel
180        let fraction = unsafe { *hist.get_unchecked(*p as usize) as f32 / total };
181        *p = (f32::min(255f32, 255f32 * fraction)) as u8;
182    });
183}
184
185/// Equalises the histogram of an 8bpp grayscale image. See also
186/// [histogram equalization (wikipedia)](https://en.wikipedia.org/wiki/Histogram_equalization).
187pub fn equalize_histogram(image: &GrayImage) -> GrayImage {
188    let mut out = image.clone();
189    equalize_histogram_mut(&mut out);
190    out
191}
192
193/// Adjusts contrast of an 8bpp grayscale image in place so that its
194/// histogram is as close as possible to that of the target image.
195pub 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
205/// Adjusts contrast of an 8bpp grayscale image so that its
206/// histogram is as close as possible to that of the target image.
207pub 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
213/// `l = histogram_lut(s, t)` is chosen so that `target_histc[l[i]] / sum(target_histc)`
214/// is as close as possible to `source_histc[i] / sum(source_histc)`.
215fn 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
249/// Linearly stretches the contrast in an image, sending `lower` to `0u8` and `upper` to `2558u8`.
250///
251/// Is it common to choose `upper` and `lower` values using image percentiles - see [`percentile`](../stats/fn.percentile.html).
252///
253/// # Examples
254/// ```
255/// # extern crate image;
256/// # #[macro_use]
257/// # extern crate imageproc;
258/// # fn main() {
259/// use imageproc::contrast::stretch_contrast;
260///
261/// let image = gray_image!(
262///      0,   20,  50;
263///     80,  100, 255);
264///
265/// let lower = 20;
266/// let upper = 100;
267///
268/// // Pixel intensities between 20 and 100 are linearly
269/// // scaled so that 20 is mapped to 0 and 100 is mapped to 255.
270/// // Pixel intensities less than 20 are sent to 0 and pixel
271/// // intensities greater than 100 are sent to 255.
272/// let stretched = stretch_contrast(&image, lower, upper);
273///
274/// let expected = gray_image!(
275///       0,   0,  95;
276///     191, 255, 255);
277///
278/// assert_pixels_eq!(stretched, expected);
279/// # }
280/// ```
281pub 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
287/// Linearly stretches the contrast in an image in place, sending `lower` to `0u8` and `upper` to `2558u8`.
288///
289/// See the [`stretch_contrast`](fn.stretch_contrast.html) documentation for more.
290pub 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                // All except the dark pixel have brightness >= their local mean
329                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        // No black pixels in either image
428        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        // Variance is 0 at any threshold, and we
453        // only increase the current threshold if we
454        // see a strictly greater variance
455        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}