Compare commits

..

2 commits

Author SHA1 Message Date
saji ad2426561f image resizing; auto-detection of hardware
All checks were successful
cargo_test_bench / Run Tests (push) Successful in 1m48s
cargo_test_bench / Run Benchmarks (push) Successful in 3m20s
2024-08-01 21:15:13 -05:00
saji de20da4e86 comments, make DitheredImage fields public 2024-08-01 19:55:12 -05:00
5 changed files with 83 additions and 64 deletions

77
Cargo.lock generated
View file

@ -26,7 +26,7 @@ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy", "zerocopy 0.7.35",
] ]
[[package]] [[package]]
@ -311,9 +311,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.16.1" version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@ -329,9 +329,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.1" version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
[[package]] [[package]]
name = "cast" name = "cast"
@ -341,9 +341,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.6" version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -394,9 +394,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -404,9 +404,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -416,9 +416,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -910,9 +910,9 @@ checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.2.6" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -1143,9 +1143,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]] [[package]]
name = "minijinja" name = "minijinja"
version = "2.1.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45f7e8e35b6c7b169bf40b0176d2c79291ab8ee53290b84e0668ab21d841aa9d" checksum = "f4bf71af278c578cbcc91d0b1ff092910208bc86f7b3750364642bd424e3dcd3"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -1555,9 +1555,12 @@ dependencies = [
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f"
dependencies = [
"zerocopy 0.6.6",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
@ -1826,11 +1829,12 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.120" version = "1.0.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr",
"ryu", "ryu",
"serde", "serde",
] ]
@ -2034,9 +2038,9 @@ dependencies = [
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.15" version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
@ -2091,9 +2095,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.39.1" version = "1.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -2510,13 +2514,34 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "zerocopy"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
dependencies = [
"byteorder",
"zerocopy-derive 0.6.6",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy-derive"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]

View file

@ -6,14 +6,13 @@ use axum::extract::{FromRequest, Multipart, State};
use axum::http::{header, StatusCode}; use axum::http::{header, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::{response::Response, routing::post, Router}; use axum::{response::Response, routing::post, Router};
use image::{ImageReader, RgbImage}; use image::{imageops::resize, imageops::FilterType, ImageReader, RgbImage};
use std::io::Cursor; use std::io::Cursor;
use std::str; use std::str;
use std::str::FromStr; use std::str::FromStr;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::Arc; use std::sync::Arc;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use std::time::Duration;
use tracing::{error, info, instrument}; use tracing::{error, info, instrument};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -128,21 +127,20 @@ where
} }
} }
#[instrument(skip(ctx))] #[instrument(skip_all)]
async fn set_image( async fn set_image(
State(ctx): State<AppState>, State(ctx): State<AppState>,
img_req: ImageRequest, img_req: ImageRequest,
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
// FIXME: resize image to 800x480 to match the eink panel. // FIXME: resize image to 800x480 to match the eink panel.
let mut buf = DitheredImage::new( info!("Got image");
img_req.image.width(), let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
img_req.image.height(), let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
img_req.palette.value().to_vec(),
);
{ {
let mut dither = img_req.dither_method.get_ditherer(); let mut dither = img_req.dither_method.get_ditherer();
dither.dither(&img_req.image, &mut buf); dither.dither(&resized, &mut buf);
} }
info!("image resized, pushing to channel");
ctx.display_channel.send(Box::new(buf))?; ctx.display_channel.send(Box::new(buf))?;
Ok(StatusCode::OK) Ok(StatusCode::OK)
} }
@ -150,14 +148,11 @@ async fn set_image(
/// generates a dithered image based on the given image and the dithering parameters. /// generates a dithered image based on the given image and the dithering parameters.
/// Can be used to see how the dithering and palette choices affect the result. /// Can be used to see how the dithering and palette choices affect the result.
async fn preview_image(img_req: ImageRequest) -> Result<impl IntoResponse, AppError> { async fn preview_image(img_req: ImageRequest) -> Result<impl IntoResponse, AppError> {
let mut buf = DitheredImage::new( let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
img_req.image.width(), let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
img_req.image.height(),
img_req.palette.value().to_vec(),
);
{ {
let mut dither = img_req.dither_method.get_ditherer(); let mut dither = img_req.dither_method.get_ditherer();
dither.dither(&img_req.image, &mut buf); dither.dither(&resized, &mut buf);
} }
// Convert buf into a png image. // Convert buf into a png image.
let img = buf.into_rgbimage(); let img = buf.into_rgbimage();

View file

@ -39,8 +39,8 @@ pub enum ProcessingError {
/// Buffer to be sent to the ``EInk`` display. /// Buffer to be sent to the ``EInk`` display.
#[derive(Debug)] #[derive(Debug)]
pub struct DitheredImage { pub struct DitheredImage {
buf: ImageBuffer<Luma<u8>, Vec<u8>>, pub buf: ImageBuffer<Luma<u8>, Vec<u8>>,
palette: Vec<Srgb>, pub palette: Vec<Srgb>,
} }
impl DitheredImage { impl DitheredImage {
@ -86,8 +86,7 @@ pub trait Ditherer {
fn dither(&mut self, img: &RgbImage, output: &mut DitheredImage); fn dither(&mut self, img: &RgbImage, output: &mut DitheredImage);
} }
/// Find the closest approximate palette color to the given sRGB value. /// Find the closest approximate palette color
/// This uses euclidian distance in linear space.
fn nearest_neighbor(input_color: Lab, palette: &[Lab]) -> (u8, Lab) { fn nearest_neighbor(input_color: Lab, palette: &[Lab]) -> (u8, Lab) {
let (nearest, _, color_diff) = palette let (nearest, _, color_diff) = palette
.iter() .iter()
@ -95,7 +94,7 @@ fn nearest_neighbor(input_color: Lab, palette: &[Lab]) -> (u8, Lab) {
.map(|(idx, p_color)| { .map(|(idx, p_color)| {
( (
idx, idx,
input_color.difference(*p_color), input_color.difference(*p_color), // this is CIEDIE2000 based and highly accurate.
input_color - *p_color, input_color - *p_color,
) )
}) })
@ -199,7 +198,7 @@ static STUKI_DITHER_POINTS: &[DiffusionPoint] = &[
pub type DiffusionMatrix<'a> = &'a [DiffusionPoint]; pub type DiffusionMatrix<'a> = &'a [DiffusionPoint];
#[derive(Debug)] #[derive(Debug)]
pub struct ErrorDiffusion<'a>(&'a [DiffusionPoint]); pub struct ErrorDiffusion<'a>(DiffusionMatrix<'a>);
impl<'a> ErrorDiffusion<'a> { impl<'a> ErrorDiffusion<'a> {
#[must_use] #[must_use]

View file

@ -3,8 +3,8 @@ pub mod display;
pub mod dither; pub mod dither;
pub mod eink; pub mod eink;
use std::path::PathBuf;
use serde::Deserialize; use serde::Deserialize;
use std::path::PathBuf;
use toml; use toml;
use crate::display::{EInkPanel, FakeEInk, Wrapper}; use crate::display::{EInkPanel, FakeEInk, Wrapper};
@ -12,16 +12,13 @@ use crate::dither::{DitherMethod, DitheredImage};
use crate::eink::Palette; use crate::eink::Palette;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use image::RgbImage; use image::RgbImage;
use tracing::{error, info}; use tracing::{error, event, info, warn, Level};
/// Application config, including sqlite db path, scan folders, and scheduling. /// Application config, including sqlite db path, scan folders, and scheduling.
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct Config { struct Config {
database_path: PathBuf, database_path: PathBuf,
search_paths: Vec<PathBuf>, search_paths: Vec<PathBuf>,
} }
/// Display images on E-Ink Displays /// Display images on E-Ink Displays
@ -38,8 +35,6 @@ enum Command {
Convert(ConvertArgs), Convert(ConvertArgs),
/// Load a single image /// Load a single image
Show, Show,
/// Display a test pattern
Test,
/// Start the HTTP server /// Start the HTTP server
Serve, Serve,
} }
@ -57,6 +52,18 @@ async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let cli = Cli::parse(); let cli = Cli::parse();
let mut display: Box<dyn EInkPanel + Send> = match Wrapper::new() {
Ok(w) => {
info!("Found real hardware, using it");
Box::new(w)
}
Err(e) => {
event!(Level::WARN, "Error opening display SPI interface: {e}");
warn!("Falling back to fake display");
Box::new(FakeEInk {})
}
};
match cli.command { match cli.command {
Command::Convert(a) => { Command::Convert(a) => {
let input = image::ImageReader::open(a.input_file)? let input = image::ImageReader::open(a.input_file)?
@ -76,7 +83,6 @@ async fn main() -> anyhow::Result<()> {
Command::Show => { Command::Show => {
let img: RgbImage = image::ImageReader::open("image.png")?.decode()?.into(); let img: RgbImage = image::ImageReader::open("image.png")?.decode()?.into();
error!("HI"); error!("HI");
let mut display = FakeEInk {};
let mut eink_buf = crate::eink::new_image(); let mut eink_buf = crate::eink::new_image();
let mut dither = DitherMethod::Atkinson.get_ditherer(); let mut dither = DitherMethod::Atkinson.get_ditherer();
@ -84,15 +90,8 @@ async fn main() -> anyhow::Result<()> {
dither.dither(&img, &mut eink_buf); dither.dither(&img, &mut eink_buf);
display.display(&eink_buf)?; display.display(&eink_buf)?;
} }
Command::Test => {
let mut display = Wrapper::new()?;
display.test()?;
}
Command::Serve => { Command::Serve => {
let display = FakeEInk {}; let ctx = api::AppState::new(display);
let ctx = api::AppState::new(Box::new(display));
let app = api::router().with_state(ctx); let app = api::router().with_state(ctx);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;

View file

@ -1,3 +1,4 @@
POST http://192.168.0.185:3000/setimage POST http://192.168.0.185:3000/setimage
[MultipartFormData] [MultipartFormData]
image: file,image.png; image: file,{{image}};
dither_method: Atkinson