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

View file

@ -6,14 +6,13 @@ use axum::extract::{FromRequest, Multipart, State};
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
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::str;
use std::str::FromStr;
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::Duration;
use tracing::{error, info, instrument};
#[derive(thiserror::Error, Debug)]
@ -128,21 +127,20 @@ where
}
}
#[instrument(skip(ctx))]
#[instrument(skip_all)]
async fn set_image(
State(ctx): State<AppState>,
img_req: ImageRequest,
) -> Result<impl IntoResponse, AppError> {
// FIXME: resize image to 800x480 to match the eink panel.
let mut buf = DitheredImage::new(
img_req.image.width(),
img_req.image.height(),
img_req.palette.value().to_vec(),
);
info!("Got image");
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
{
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))?;
Ok(StatusCode::OK)
}
@ -150,14 +148,11 @@ async fn set_image(
/// 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.
async fn preview_image(img_req: ImageRequest) -> Result<impl IntoResponse, AppError> {
let mut buf = DitheredImage::new(
img_req.image.width(),
img_req.image.height(),
img_req.palette.value().to_vec(),
);
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
{
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.
let img = buf.into_rgbimage();

View file

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

View file

@ -3,8 +3,8 @@ pub mod display;
pub mod dither;
pub mod eink;
use std::path::PathBuf;
use serde::Deserialize;
use std::path::PathBuf;
use toml;
use crate::display::{EInkPanel, FakeEInk, Wrapper};
@ -12,16 +12,13 @@ use crate::dither::{DitherMethod, DitheredImage};
use crate::eink::Palette;
use clap::{Args, Parser, Subcommand};
use image::RgbImage;
use tracing::{error, info};
use tracing::{error, event, info, warn, Level};
/// Application config, including sqlite db path, scan folders, and scheduling.
#[derive(Deserialize, Debug)]
struct Config {
database_path: PathBuf,
search_paths: Vec<PathBuf>,
}
/// Display images on E-Ink Displays
@ -38,8 +35,6 @@ enum Command {
Convert(ConvertArgs),
/// Load a single image
Show,
/// Display a test pattern
Test,
/// Start the HTTP server
Serve,
}
@ -57,6 +52,18 @@ async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
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 {
Command::Convert(a) => {
let input = image::ImageReader::open(a.input_file)?
@ -76,7 +83,6 @@ async fn main() -> anyhow::Result<()> {
Command::Show => {
let img: RgbImage = image::ImageReader::open("image.png")?.decode()?.into();
error!("HI");
let mut display = FakeEInk {};
let mut eink_buf = crate::eink::new_image();
let mut dither = DitherMethod::Atkinson.get_ditherer();
@ -84,15 +90,8 @@ async fn main() -> anyhow::Result<()> {
dither.dither(&img, &mut eink_buf);
display.display(&eink_buf)?;
}
Command::Test => {
let mut display = Wrapper::new()?;
display.test()?;
}
Command::Serve => {
let display = FakeEInk {};
let ctx = api::AppState::new(Box::new(display));
let ctx = api::AppState::new(display);
let app = api::router().with_state(ctx);
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
[MultipartFormData]
image: file,image.png;
image: file,{{image}};
dither_method: Atkinson