From 608ebe9ec57af6cd27206b3717332e71daa0b579 Mon Sep 17 00:00:00 2001 From: saji Date: Mon, 29 Jul 2024 12:51:51 -0500 Subject: [PATCH] add serde/strum, modify EIinkPanel API --- Cargo.lock | 24 +++++++++ Cargo.toml | 2 + src/api.rs | 57 +++++++++++++-------- src/display.rs | 20 +++++--- src/imageproc.rs | 127 +++++++---------------------------------------- src/main.rs | 18 +++---- 6 files changed, 101 insertions(+), 147 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c906adb..f458a8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1309,6 +1309,8 @@ dependencies = [ "linux-embedded-hal", "minijinja", "palette", + "serde", + "strum", "thiserror", "tokio", "tower-http", @@ -1756,6 +1758,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.72" diff --git a/Cargo.toml b/Cargo.toml index e951b7f..c46bb1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ image = "0.25.1" linux-embedded-hal = { version = "0.4.0"} minijinja = "2.1.0" palette = "0.7.6" +serde = { version = "1.0.204", features = ["derive"] } +strum = { version = "0.26.3", features = ["derive"] } thiserror = "1.0.63" tokio = { version = "1.38.1", features = ["full"] } tower-http = { version = "0.5.2", features = ["trace"] } diff --git a/src/api.rs b/src/api.rs index fc7a77d..21867d5 100644 --- a/src/api.rs +++ b/src/api.rs @@ -7,6 +7,7 @@ use image::ImageReader; use std::io::Cursor; use std::time::Duration; use tracing::{debug, error, info, instrument}; +use serde::{Serialize, Deserialize}; use crate::display::EInkPanel; use std::sync::Arc; @@ -36,25 +37,6 @@ impl Context { } } -#[derive(Debug)] -pub struct DisplaySetCommand { - img: Box, -} -#[instrument(skip_all)] -pub async fn display_task( - mut rx: Receiver, - mut display: Box, -) { - while let Some(cmd) = rx.recv().await { - info!("Got a display set command"); - let raw_buf = cmd.img.into_display_buffer(); - if let Err(e) = display.display(&raw_buf) { - error!("Error displaying command {e}"); - } - info!("Done setting display"); - } -} - // Make our own error that wraps `anyhow::Error`. struct AppError(anyhow::Error); @@ -80,11 +62,38 @@ where } } +#[derive(Debug)] +pub struct DisplaySetCommand { + img: Box, +} + +#[instrument(skip_all)] +pub async fn display_task( + mut rx: Receiver, + mut display: Box, +) { + while let Some(cmd) = rx.recv().await { + info!("Got a display set command"); + if let Err(e) = display.display(&cmd.img) { + error!("Error displaying command {e}"); + } + info!("Done setting display"); + } +} + /// API routes for axum /// Start with the basics: Send an image, crop it, dither, and upload. /// we defer the upload to a separate task. pub fn router() -> Router { - Router::new().route("/setimage", post(set_image)) + Router::new() + .route("/setimage", post(set_image)) + .route("/process_image", post(process_image)) +} + + + +#[derive(Serialize, Deserialize)] +pub struct ImageRequest { } #[instrument(skip(ctx))] @@ -103,8 +112,8 @@ async fn set_image( debug!("Guessed format: {:?}", reader.format()); let image = reader.decode()?; - let mut buf = EInkImage::new(800, 480); - let dither = ErrorDiffusionDither::new(DiffusionMatrix::Atkinson); + let mut buf = EInkImage::default(); + let mut dither = ErrorDiffusionDither::new(DiffusionMatrix::Atkinson); dither.dither(&image.into(), &mut buf); let cmd = DisplaySetCommand { img: Box::new(buf) }; ctx.display_channel @@ -114,3 +123,7 @@ async fn set_image( } Ok(()) } + +async fn process_image(mut parts: Multipart) -> Result { + Ok(StatusCode::OK) +} diff --git a/src/display.rs b/src/display.rs index 0a12350..72898b0 100644 --- a/src/display.rs +++ b/src/display.rs @@ -8,8 +8,10 @@ use tracing::{debug, warn}; use anyhow::Result; use linux_embedded_hal::gpio_cdev::{Chip, LineRequestFlags}; +use crate::imageproc::EInkImage; + pub trait EInkPanel { - fn display(&mut self, buf: &[u8]) -> Result<()>; + fn display(&mut self, buf: &EInkImage) -> Result<()>; } pub struct Wrapper { @@ -64,9 +66,10 @@ impl Wrapper { impl EInkPanel for Wrapper { #[instrument(skip_all)] - fn display(&mut self, buf: &[u8]) -> Result<()> { + fn display(&mut self, img: &EInkImage) -> Result<()> { + let buf = img.into_display_buffer(); self.panel - .update_and_display_frame(&mut self.spi, buf, &mut self.delay)?; + .update_and_display_frame(&mut self.spi, &buf, &mut self.delay)?; debug!("Finished updating frame"); self.panel.sleep(&mut self.spi, &mut self.delay)?; debug!("Display entered sleep mode"); @@ -74,11 +77,16 @@ impl EInkPanel for Wrapper { } } + +/// A Fake EInk display for testing purposes. +/// Saves the output as `display.bmp` pub struct FakeEInk(); impl EInkPanel for FakeEInk { - fn display(&mut self, _buf: &[u8]) -> Result<()> { - // Do nothing. - warn!("Fake display was called"); + fn display(&mut self, img: &EInkImage) -> Result<()> { + + warn!("Fake display was called: saving to display.bmp"); + img.into_rgbimage().save("display.bmp"); + Ok(()) } } diff --git a/src/imageproc.rs b/src/imageproc.rs index b2eacb7..f3484a9 100644 --- a/src/imageproc.rs +++ b/src/imageproc.rs @@ -1,4 +1,4 @@ -use image::{GenericImageView, GrayImage, ImageBuffer, Luma, RgbImage}; +use image::{GrayImage, ImageBuffer, Luma, RgbImage}; use palette::FromColor; use tracing::instrument; @@ -19,66 +19,20 @@ const DISPLAY_PALETTE: [Srgb; 7] = [ Srgb::new(0.757, 0.443, 0.165), // Orange ]; -pub enum Error { +pub enum ProcessingError { DitherError, PaletteIndexError(usize), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DisplayColor { - Black, - White, - Green, - Blue, - Red, - Yellow, - Orange, -} - -impl From for Srgb { - fn from(value: DisplayColor) -> Self { - DISPLAY_PALETTE[value as usize] - } -} - -impl DisplayColor { - fn from_u8(value: u8) -> Self { - match value { - 0 => Self::Black, - 1 => Self::White, - 2 => Self::Green, - 3 => Self::Blue, - 4 => Self::Red, - 5 => Self::Yellow, - 6 => Self::Orange, - _ => panic!("unexpected DisplayColor {value}"), - } - } - - fn into_byte(color1: Self, color2: Self) -> u8 { - let upper: u8 = color1.into(); - let lower: u8 = color2.into(); - upper << 4 | lower - } -} -impl From for u8 { - fn from(value: DisplayColor) -> Self { - value as Self - } -} /// Buffer to be sent to the ``EInk`` display. #[derive(Debug)] pub struct EInkImage { - data: Vec, - width: u32, - height: u32, -} - -pub struct TestEInkImage { buf: ImageBuffer, Vec>, palette: Vec, } -impl TestEInkImage { + +impl EInkImage { + #[must_use] 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) { @@ -87,6 +41,8 @@ impl TestEInkImage { buf } + /// Convert the EInk-palette image into an RGB image to be viewed on a regular screen. + #[must_use] 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]; @@ -106,69 +62,20 @@ impl TestEInkImage { } } } -// 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); - - for colors in self.data.chunks_exact(2) { - buf.push(DisplayColor::into_byte(colors[0], colors[1])); - } - - buf - } - #[must_use] - pub fn new(width: u32, height: u32) -> Self { - let v = vec![DisplayColor::Black; (width * height) as usize]; - Self { - data: v, - width, - height, - } - } - - /// Produces a regular RGB image from this image buffer using the given - /// color palette. - pub fn make_image(&self) -> RgbImage { - RgbImage::from_fn(self.width, self.height, |x, y| { - let srgb = Srgb::from(self.data[(y * self.width + x) as usize]); - let arr: [u8; 3] = srgb.into_format().into(); - imgRgb(arr) - }) - } - - /// Returns the dimensions (width, height) of the image buffer. - pub const fn dimensions(&self) -> (u32, u32) { - (self.width, self.height) +impl Default for EInkImage { + fn default() -> Self { + Self::new(DISPLAY_PALETTE.to_vec()) } } pub trait Ditherer { 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. -#[must_use] -pub fn nearest_neighbor(input_color: Lab) -> (DisplayColor, Lab) { - let (nearest, _, color_diff) = DISPLAY_PALETTE - .iter() - .enumerate() - .map(|(idx, p_color)| { - let c: Lab = (*p_color).into_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) -} - -fn nearest_neighbor2(input_color: Lab, palette:&[Srgb]) -> (DisplayColor, Lab) { +fn nearest_neighbor(input_color: Lab, palette: &[Srgb]) -> (u8, Lab) { let (nearest, _, color_diff) = palette .iter() .enumerate() @@ -178,7 +85,7 @@ fn nearest_neighbor2(input_color: Lab, palette:&[Srgb]) -> (DisplayColor, Lab) { }) .min_by(|(_, a, _), (_, b, _)| a.total_cmp(b)) .expect("Should always find a color"); - (DisplayColor::from_u8(nearest as u8), color_diff) + (nearest as u8, color_diff) } pub struct NNDither(); @@ -191,9 +98,9 @@ impl Ditherer for NNDither { // sRGB view into the given image. zero copy! let srgb = <&[Srgb]>::from_components(&**img); - for (idx, pixel) in srgb.iter().enumerate() { - let (n, _) = nearest_neighbor(pixel.into_format().into_color()); - output.data[idx] = n; + for (idx, pix) in output.buf.iter_mut().enumerate() { + let (n, _) = nearest_neighbor(srgb[idx].into_format().into_color(), &output.palette); + *pix = n; } } } @@ -325,9 +232,9 @@ impl Ditherer for ErrorDiffusionDither { for x in 0..xsize { let index = coord_to_idx(x, y, xsize); let curr_pix = temp_img[index]; - let (nearest, err) = nearest_neighbor(curr_pix); + let (nearest, err) = nearest_neighbor(curr_pix, &output.palette); // set the color in the output buffer. - output.data[index] = nearest; + *output.buf.get_mut(index).unwrap() = nearest; // take the error, and propagate it. for point in self.0.value() { let Some(target_x) = x.checked_add_signed(point.xshift) else { diff --git a/src/main.rs b/src/main.rs index 5ecc1b1..a388653 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,13 +3,13 @@ pub mod display; pub mod errors; pub mod imageproc; -use crate::display::{EInkPanel, Wrapper}; +use crate::display::{FakeEInk, EInkPanel, Wrapper}; use crate::imageproc::{DiffusionMatrix, Ditherer, EInkImage, ErrorDiffusionDither}; use clap::{Parser, Subcommand}; use image::RgbImage; -use tracing::info; +use tracing::{error, info}; -/// Display images over +/// Display images on E-Ink Displays #[derive(Debug, Parser)] #[command(version, about)] struct Cli { @@ -34,15 +34,15 @@ async fn main() -> anyhow::Result<()> { println!("CLI {cli:?}"); if matches!(cli.command, Command::Show) { - let img: RgbImage = image::ImageReader::open("myimage.png")?.decode()?.into(); - let mut display = Wrapper::new()?; + let img: RgbImage = image::ImageReader::open("image.png")?.decode()?.into(); + error!("HI"); + let mut display = FakeEInk {}; - let mut eink_buf = EInkImage::new(800, 480); - let dither = ErrorDiffusionDither::new(DiffusionMatrix::Atkinson); + let mut eink_buf = EInkImage::default(); + let mut dither = ErrorDiffusionDither::new(DiffusionMatrix::Atkinson); dither.dither(&img, &mut eink_buf); - let raw_buf = eink_buf.into_display_buffer(); - display.display(&raw_buf)?; + display.display(&eink_buf)?; } if matches!(cli.command, Command::Test) { let mut display = Wrapper::new()?;