Skip to content

Commit

Permalink
Added scale_linear and min_max functions (#587)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Charles Schleich <[email protected]>
  • Loading branch information
ripytide and Charles-Schleich authored May 5, 2024
1 parent 43aca37 commit 4f24927
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 70 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Change Log

## Unreleased

Changes:
- `contrast::stretch_contrast{_mut}` had been extended and renamed to `contrast::scale_linear{_mut}`

Added:
- Added a new `min_max()` function and `MinMax` return struct for finding the
minimum and maximum value pixel in an image

## [0.24.0] - 2024-03-16

New features:
Expand Down
169 changes: 101 additions & 68 deletions src/contrast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use rayon::prelude::*;

use crate::definitions::{HasBlack, HasWhite};
use crate::integral_image::{integral_image, sum_image_pixels};
use crate::map::map_subpixels_mut;
use crate::stats::{cumulative_histogram, histogram};

/// Applies an adaptive threshold to an image.
Expand Down Expand Up @@ -266,6 +267,85 @@ pub fn equalize_histogram(image: &GrayImage) -> GrayImage {
out
}

/// Scales each pixel in the image using the scaling effect to take the input_min and input_max
/// pair to the output_min and output_max pair.
///
/// # Example
/// (50, 100) -> (0, 255) would make 8 -> 0 since it saturates outside the input range as 8 < 50.
/// 200 -> 255 since it also saturates outside the input range as 200 > 100.
/// 50 -> 0, and 100 -> 255 by definition of the scaling pairs.
/// All values between 50 and 100 are then linearly interpolated into the output range.
/// Such as 75 -> 128
///
///
/// # Panic
/// This function panics if `input_min` >= `input_max` or `output_min` > `output_max`.
pub fn scale_linear(
image: &GrayImage,
input_min: u8,
input_max: u8,
output_min: u8,
output_max: u8,
) -> GrayImage {
let mut out = image.clone();
scale_linear_mut(&mut out, input_min, input_max, output_min, output_max);
out
}

/// Scales each pixel in the image using the scaling effect to take the input_min and input_max
/// pair to the output_min and output_max pair.
///
/// # Example
/// (50, 100) -> (0, 255) would make 8 -> 0 since it saturates outside the input range as 8 < 50.
/// 200 -> 255 since it also saturates outside the input range as 200 > 100.
/// 50 -> 0, and 100 -> 255 by definition of the scaling pairs.
/// All values between 50 and 100 are then linearly interpolated into the output range.
/// Such as 75 -> 128
///
///
/// # Panic
/// This function panics if `input_min` >= `input_max` or `output_min` > `output_max`.
pub fn scale_linear_mut(
image: &mut GrayImage,
input_min: u8,
input_max: u8,
output_min: u8,
output_max: u8,
) {
assert!(
input_min < input_max,
"input_min must be smaller than input_max"
);
assert!(
output_min <= output_max,
"output_min must be smaller or equal to output_max"
);

let input_min: u16 = input_min.into();
let input_max: u16 = input_max.into();
let output_min: u16 = output_min.into();
let output_max: u16 = output_max.into();

let input_width = input_max - input_min;
let output_width = output_max - output_min;

let f = |p: u8| {
let p: u16 = p.into();

let output = if p <= input_min {
output_min
} else if p >= input_max {
output_max
} else {
(((p - input_min) * output_width) / input_width) + output_min
};

output as u8
};

map_subpixels_mut(image, f);
}

/// Adjusts contrast of an 8bpp grayscale image in place so that its
/// histogram is as close as possible to that of the target image.
pub fn match_histogram_mut(image: &mut GrayImage, target: &GrayImage) {
Expand Down Expand Up @@ -322,62 +402,6 @@ fn histogram_lut(source_histc: &[u32; 256], target_histc: &[u32; 256]) -> [usize
lut
}

/// Linearly stretches the contrast in an image, sending `lower` to `0u8` and `upper` to `2558u8`.
///
/// Is it common to choose `upper` and `lower` values using image percentiles - see [`percentile`](../stats/fn.percentile.html).
///
/// # Examples
/// ```
/// # extern crate image;
/// # #[macro_use]
/// # extern crate imageproc;
/// # fn main() {
/// use imageproc::contrast::stretch_contrast;
///
/// let image = gray_image!(
/// 0, 20, 50;
/// 80, 100, 255);
///
/// let lower = 20;
/// let upper = 100;
///
/// // Pixel intensities between 20 and 100 are linearly
/// // scaled so that 20 is mapped to 0 and 100 is mapped to 255.
/// // Pixel intensities less than 20 are sent to 0 and pixel
/// // intensities greater than 100 are sent to 255.
/// let stretched = stretch_contrast(&image, lower, upper);
///
/// let expected = gray_image!(
/// 0, 0, 95;
/// 191, 255, 255);
///
/// assert_pixels_eq!(stretched, expected);
/// # }
/// ```
pub fn stretch_contrast(image: &GrayImage, lower: u8, upper: u8) -> GrayImage {
let mut out = image.clone();
stretch_contrast_mut(&mut out, lower, upper);
out
}

/// Linearly stretches the contrast in an image in place, sending `lower` to `0u8` and `upper` to `2558u8`.
///
/// See the [`stretch_contrast`](fn.stretch_contrast.html) documentation for more.
pub fn stretch_contrast_mut(image: &mut GrayImage, lower: u8, upper: u8) {
assert!(upper > lower, "upper must be strictly greater than lower");
let len = (upper - lower) as u16;
for p in image.iter_mut() {
if *p >= upper {
*p = 255;
} else if *p <= lower {
*p = 0;
} else {
let scaled = (255 * (*p as u16 - lower as u16)) / len;
*p = scaled as u8;
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -544,6 +568,15 @@ mod tests {
let actual = threshold(&original, 125u8, ThresholdType::Binary);
assert_pixels_eq!(expected, actual);
}

#[test]
fn test_scale_linear() {
let input = gray_image!(1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 255);

let expected = gray_image!(10u8, 10, 10, 11, 11, 12, 12, 13, 13, 13, 52, 120);

assert_pixels_eq!(scale_linear(&input, 1, 255, 10, 120), expected);
}
}

#[cfg(not(miri))]
Expand Down Expand Up @@ -621,29 +654,29 @@ mod benches {
}

#[bench]
fn bench_stretch_contrast(b: &mut Bencher) {
let image = gray_bench_image(500, 500);
fn bench_otsu_level(b: &mut Bencher) {
let image = gray_bench_image(200, 200);
b.iter(|| {
let stretched = stretch_contrast(&image, 20, 80);
black_box(stretched);
let level = otsu_level(&image);
black_box(level);
});
}

#[bench]
fn bench_stretch_contrast_mut(b: &mut Bencher) {
let mut image = gray_bench_image(500, 500);
fn bench_scale_linear(b: &mut Bencher) {
let image = gray_bench_image(200, 200);
b.iter(|| {
stretch_contrast_mut(&mut image, 20, 80);
black_box(());
let scaled = scale_linear(&image, 0, 255, 0, 255);
black_box(scaled);
});
}

#[bench]
fn bench_otsu_level(b: &mut Bencher) {
let image = gray_bench_image(200, 200);
fn bench_scale_linear_mut(b: &mut Bencher) {
let mut image = gray_bench_image(200, 200);
b.iter(|| {
let level = otsu_level(&image);
black_box(level);
scale_linear_mut(&mut image, 0, 255, 0, 255);
black_box(());
});
}
}
3 changes: 1 addition & 2 deletions src/edges.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Functions for detecting edges in images.
use crate::contrast;
use crate::definitions::{HasBlack, HasWhite};
use crate::filter::{gaussian_blur_f32, laplacian_filter};
use crate::filter::gaussian_blur_f32;
use crate::gradients::{horizontal_sobel, vertical_sobel};
use image::{GenericImageView, GrayImage, ImageBuffer, Luma};
use std::f32;
Expand Down
63 changes: 63 additions & 0 deletions src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,49 @@ use crate::definitions::Image;
use image::{GenericImageView, GrayImage, Pixel, Primitive};
use num::Bounded;

/// A minimum and maximum value returned by [`min_max()`]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MinMax<T> {
/// The minimum value
pub min: T,
/// The maximum value
pub max: T,
}

/// Returns the minimum and maximum values per channel in an image.
pub fn min_max<P, T>(image: &Image<P>) -> Vec<MinMax<T>>
where
P: Pixel<Subpixel = T>,
T: Ord + Copy,
{
if image.is_empty() {
panic!("cannot find the range of an empty image");
}

let mut ranges = vec![(None, None); P::CHANNEL_COUNT as usize];

for pix in image.pixels() {
for (i, c) in pix.channels().iter().enumerate() {
let (current_min, current_max) = &mut ranges[i];

if current_min.map_or(true, |x| c < x) {
*current_min = Some(c);
}
if current_max.map_or(true, |x| c > x) {
*current_max = Some(c);
}
}
}

ranges
.into_iter()
.map(|(min, max)| MinMax {
min: *min.unwrap(),
max: *max.unwrap(),
})
.collect()
}

/// A set of per-channel histograms from an image with 8 bits per channel.
pub struct ChannelHistogram {
/// Per-channel histograms.
Expand Down Expand Up @@ -155,6 +198,26 @@ mod tests {
use super::*;
use image::{GrayImage, Luma, Rgb, RgbImage};

#[test]
fn test_range() {
let image = rgb_image!(
[1u8, 10u8, 0u8],
[2u8, 20u8, 3u8],
[3u8, 30u8, 255u8],
[2u8, 20u8, 7u8],
[1u8, 10u8, 8u8]
);

assert_eq!(
min_max(&image),
vec![
MinMax { min: 1, max: 3 },
MinMax { min: 10, max: 30 },
MinMax { min: 0, max: 255 }
]
)
}

#[test]
fn test_cumulative_histogram() {
let image = gray_image!(1u8, 2u8, 3u8, 2u8, 1u8);
Expand Down

0 comments on commit 4f24927

Please sign in to comment.