diff --git a/src/imageproc.rs b/src/imageproc.rs index 90fb844..b2eacb7 100644 --- a/src/imageproc.rs +++ b/src/imageproc.rs @@ -1,4 +1,5 @@ -use image::RgbImage; +use image::{GenericImageView, GrayImage, ImageBuffer, Luma, RgbImage}; +use palette::FromColor; use tracing::instrument; use image::Rgb as imgRgb; @@ -18,7 +19,10 @@ const DISPLAY_PALETTE: [Srgb; 7] = [ Srgb::new(0.757, 0.443, 0.165), // Orange ]; -// TODO: support different color palettes. +pub enum Error { + DitherError, + PaletteIndexError(usize), +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DisplayColor { @@ -70,8 +74,44 @@ pub struct EInkImage { height: u32, } +pub struct TestEInkImage { + buf: ImageBuffer, Vec>, + palette: Vec, +} +impl TestEInkImage { + pub fn into_display_buffer(&self) -> Vec { + let mut buf = Vec::with_capacity(self.buf.len() / 2); + for pix in self.buf.chunks_exact(2) { + buf.push(pix[0] << 4 | pix[1]); + } + buf + } + + pub fn into_rgbimage(&self) -> RgbImage { + RgbImage::from_fn(self.buf.width(), self.buf.height(), |x, y| { + let idx = self.buf.get_pixel(x, y).0[0]; + let disp_color = self.palette.get(idx as usize).unwrap(); + let arr: [u8; 3] = disp_color.into_format().into(); + imgRgb(arr) + }) + } + + /// Constructs a new EInk Image based on the given color palette for + /// color indexing. + #[must_use] + pub fn new(palette: Vec) -> Self { + Self { + buf: GrayImage::new(800, 480), + palette, + } + } +} +// TODO: Evaluate using Imagebuffer, Vec> instead. +// This is what the imageops index_map function does. +// advantages are we get all the 2d array helping functions for free. impl EInkImage { #[must_use] + pub fn into_display_buffer(&self) -> Vec { let mut buf = Vec::with_capacity(self.data.len() / 2); @@ -108,8 +148,9 @@ impl EInkImage { } pub trait Ditherer { - fn dither(&self, img: &RgbImage, output: &mut EInkImage); + fn dither(&mut self, img: &RgbImage, output: &mut EInkImage); } +pub type DitherFunc = dyn Fn(&RgbImage, &mut TestEInkImage) -> Result<(), Error>; /// Find the closest approximate palette color to the given sRGB value. /// This uses euclidian distance in linear space. @@ -123,14 +164,27 @@ pub fn nearest_neighbor(input_color: Lab) -> (DisplayColor, Lab) { (idx, input_color.difference(c), input_color - c) }) .min_by(|(_, a, _), (_, b, _)| a.total_cmp(b)) - .expect("could not find a color"); + .expect("Should always find a color"); + (DisplayColor::from_u8(nearest as u8), color_diff) +} + +fn nearest_neighbor2(input_color: Lab, palette:&[Srgb]) -> (DisplayColor, Lab) { + let (nearest, _, color_diff) = palette + .iter() + .enumerate() + .map(|(idx, p_color)| { + let c: Lab = Lab::from_color(*p_color); + (idx, input_color.difference(c), input_color - c) + }) + .min_by(|(_, a, _), (_, b, _)| a.total_cmp(b)) + .expect("Should always find a color"); (DisplayColor::from_u8(nearest as u8), color_diff) } pub struct NNDither(); impl Ditherer for NNDither { - fn dither(&self, img: &RgbImage, output: &mut EInkImage) { + fn dither(&mut self, img: &RgbImage, output: &mut EInkImage) { assert!(img.width() == 800); assert!(img.height() == 480); @@ -152,13 +206,13 @@ const fn coord_to_idx(x: u32, y: u32, xsize: u32) -> usize { /// Compute the error-adjusted new lab value based on the error value of the currently scanned /// pixel multiplied by a scalar factor. -fn get_error_adjusted(orig: &Lab, err: &Lab, scalar: f32) -> Lab { - let (p_l, p_a, p_b) = orig.into_components(); +fn compute_error_adjusted_color(orig: &Lab, err: &Lab, weight: f32) -> Lab { + let (orig_l, orig_a, orig_b) = orig.into_components(); let (err_l, err_a, err_b) = err.into_components(); Lab::from_components(( - p_l + err_l * scalar, - p_a + err_a * scalar, - p_b + err_b * scalar, + err_l.mul_add(weight, orig_l), // scalar * err_l + p_l + err_a.mul_add(weight, orig_a), + err_b.mul_add(weight, orig_b), )) } @@ -255,10 +309,12 @@ impl ErrorDiffusionDither { impl Ditherer for ErrorDiffusionDither { #[instrument] - fn dither(&self, img: &RgbImage, output: &mut EInkImage) { + fn dither(&mut self, img: &RgbImage, output: &mut EInkImage) { // create a copy of the image in Lab space, mutable. + // first, a view into the rgb components let srgb = <&[Srgb]>::from_components(&**img); let (xsize, ysize) = img.dimensions(); + // our destination buffer. let mut temp_img: Vec = Vec::with_capacity((xsize * ysize) as usize); for pix in srgb { temp_img.push(pix.into_format().into_color()); @@ -282,7 +338,7 @@ impl Ditherer for ErrorDiffusionDither { }; let target = coord_to_idx(target_x, target_y, xsize); if let Some(pix) = temp_img.get(target) { - temp_img[target] = get_error_adjusted(pix, &err, point.scale); + temp_img[target] = compute_error_adjusted_color(pix, &err, point.scale); } } }