use image::imageops::{resize, FilterType};
use image::{GrayImage, Luma};
use imageproc::contrast::stretch_contrast_mut;
use imageproc::stats::percentile;
pub fn convert(image: GrayImage, params: &ConvertParams) -> GrayImage {
let (width, height) =
resize_dimensions(image.width(), image.height(), params.width, params.height);
let mut image = if width == image.width() || (width > image.width() && !params.upscale) {
image
} else {
resize(&image, width, height, params.filter)
};
let lower = percentile(&image, params.cutoff);
let upper = percentile(&image, 100_u8 - params.cutoff);
if upper > lower && !(lower == 0 && upper == 255) {
stretch_contrast_mut(&mut image, lower, upper);
}
if (params.gamma - 1_f64).abs() > 0.001 {
apply_lut(&mut image, ¶ms.gamma_lut);
}
image
}
#[derive(Debug)]
pub struct ConvertParams {
width: u32,
height: u32,
upscale: bool,
cutoff: u8,
filter: FilterType,
gamma: f64,
gamma_lut: [u8; 256],
}
impl Default for ConvertParams {
fn default() -> Self {
ConvertParamsBuilder::default().build()
}
}
impl ConvertParams {
pub fn builder() -> ConvertParamsBuilder {
ConvertParamsBuilder::default()
}
}
pub struct ConvertParamsBuilder {
width: u32,
height: u32,
upscale: bool,
cutoff: u8,
filter: FilterType,
gamma: f64,
}
impl Default for ConvertParamsBuilder {
fn default() -> Self {
ConvertParamsBuilder {
width: 1920,
height: 1920,
upscale: false,
cutoff: 1,
filter: FilterType::CatmullRom,
gamma: 0.75,
}
}
}
impl ConvertParamsBuilder {
pub fn width(&mut self, width: u32) -> &mut Self {
self.width = width;
self
}
pub fn height(&mut self, height: u32) -> &mut Self {
self.height = height;
self
}
pub fn upscale(&mut self, upscale: bool) -> &mut Self {
self.upscale = upscale;
self
}
pub fn cutoff(&mut self, upscale_percentile: u8) -> &mut Self {
self.cutoff = upscale_percentile;
self
}
pub fn filter(&mut self, filter: FilterType) -> &mut Self {
self.filter = filter;
self
}
pub fn gamma(&mut self, gamma: f64) -> &mut Self {
self.gamma = gamma;
self
}
pub fn build(&self) -> ConvertParams {
ConvertParams {
width: self.width,
height: self.height,
upscale: self.upscale,
cutoff: self.cutoff,
filter: self.filter,
gamma: self.gamma,
gamma_lut: generate_gamma_lut(self.gamma),
}
}
}
fn generate_gamma_lut(gamma: f64) -> [u8; 256] {
let mut lut = [0; 256];
for (i, x) in lut.iter_mut().enumerate() {
*x = clamp((i as f64 / 255_f64).powf(1_f64 / gamma) * 255_f64)
}
lut
}
fn clamp(i: f64) -> u8 {
if i > 255_f64 {
return 255;
}
if i > 0_f64 {
return i as u8;
}
0
}
fn apply_lut(image: &mut GrayImage, lut: &[u8; 256]) {
for p in image.pixels_mut() {
*p = Luma([lut[p[0] as usize]]);
}
}
fn resize_dimensions(x: u32, y: u32, nx: u32, ny: u32) -> (u32, u32) {
let nx = if nx > 0 { nx } else { x };
let ny = if ny > 0 { ny } else { y };
let ratio = u64::from(x) * u64::from(ny);
let nratio = u64::from(nx) * u64::from(y);
let use_y = nratio <= ratio;
let intermediate = if use_y {
u64::from(y) * u64::from(nx) / u64::from(x)
} else {
u64::from(x) * u64::from(ny) / u64::from(y)
};
let intermediate = std::cmp::max(1, intermediate);
if use_y {
(nx, intermediate as u32)
} else {
(intermediate as u32, ny)
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! dimensions_tests {
($($name:ident: $value:expr,)*) => {
$(
#[test]
fn $name() {
let (x, y, nx, ny, expected) = $value;
assert_eq!(expected, resize_dimensions(x, y, nx, ny));
}
)*
}
}
dimensions_tests! {
resize_dimensions_x_gt_y: (100, 100, 70, 50, (50, 50)),
resize_dimensions_y_gt_x: (100, 100, 50, 70, (50, 50)),
resize_dimensions_0nx: (100, 100, 0, 50, (50, 50)),
resize_dimensions_0ny: (100, 100, 50, 0, (50, 50)),
}
}