use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage};
use imageproc::filter::median_filter;
use palette::{cast, color_difference::EuclideanDistance, FromColor, Lab, Srgb};
use rayon::prelude::*;
pub struct ColorReducer {
palette: Vec<[u8; 3]>,
}
impl ColorReducer {
pub fn new(palette: Vec<[u8; 3]>) -> Self {
ColorReducer { palette }
}
pub fn reduce(&self, img: &DynamicImage) -> Result<DynamicImage, Box<dyn std::error::Error>> {
if self.palette.is_empty() {
return Err("Palette is empty".into());
}
let rgba_image = img.to_rgba8();
let (width, height) = rgba_image.dimensions();
let pixels: Vec<Rgba<u8>> = rgba_image.pixels().copied().collect();
let palette_lab: Vec<Lab> = self
.palette
.iter()
.map(|&rgb| {
let srgb = cast::from_array_ref::<Srgb<u8>>(&rgb);
Lab::from_color(srgb.into_linear())
})
.collect();
let simplified_pixels: Vec<Rgba<u8>> = pixels
.par_iter()
.map(|pixel| {
let rgb = [pixel[0], pixel[1], pixel[2]];
let srgb = cast::from_array_ref::<Srgb<u8>>(&rgb);
let lab = Lab::from_color(srgb.into_linear());
let closest_option = palette_lab
.iter()
.zip(self.palette.iter())
.map(|(palette_lab, &palette_rgb)| {
let distance = lab.distance(*palette_lab);
(palette_rgb, distance)
})
.min_by(|(_, dist1), (_, dist2)| dist1.total_cmp(dist2));
match closest_option {
Some((closest_color, _)) => Rgba([
closest_color[0],
closest_color[1],
closest_color[2],
pixel[3],
]),
None => {
*pixel
}
}
})
.collect();
let new_image: RgbaImage = ImageBuffer::from_fn(width, height, |x, y| {
simplified_pixels[(y * width + x) as usize]
});
let denoise_img = median_filter(&new_image, 3, 3);
Ok(DynamicImage::ImageRgba8(denoise_img))
}
}