add regression test, sample images, reference dithers

This commit is contained in:
saji 2024-07-30 21:46:15 -05:00
parent eb422b87fb
commit 0d444e9701
12 changed files with 132 additions and 44 deletions

View file

@ -13,4 +13,4 @@ fn criterion_benchmark(c: &mut Criterion) {
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
criterion_main!(benches);

BIN
samples/sample_0.tiff Normal file

Binary file not shown.

BIN
samples/sample_1.tiff Normal file

Binary file not shown.

BIN
samples/sample_2.tiff Normal file

Binary file not shown.

View file

@ -1,5 +1,5 @@
use crate::display::EInkPanel;
use crate::dither::{DitherMethod, DitherPalette, DitheredImage};
use crate::dither::{DitherMethod, Palette, DitheredImage};
use axum::async_trait;
use axum::extract::{FromRequest, Multipart, State};
use axum::http::{header, StatusCode};
@ -96,7 +96,7 @@ pub fn router() -> Router<Context> {
struct ImageRequest {
image: Box<RgbImage>,
dither_method: DitherMethod,
palette: DitherPalette,
palette: Palette,
}
#[async_trait]
@ -124,7 +124,7 @@ where
Some("palette") => {
let data = field.bytes().await?;
let val = str::from_utf8(&data)?;
palette = Some(DitherPalette::from_str(val)?);
palette = Some(Palette::from_str(val)?);
}
Some("dither_method") => {
let data = field.bytes().await?;
@ -140,7 +140,7 @@ where
Ok(Self {
image: i,
dither_method: dither_method.unwrap_or(DitherMethod::NearestNeighbor),
palette: palette.unwrap_or(DitherPalette::Default),
palette: palette.unwrap_or(Palette::Default),
})
},
)

View file

@ -31,12 +31,12 @@ const SIMPLE_PALETTE: [Srgb; 7] = [
];
#[derive(strum::EnumString, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub enum DitherPalette {
pub enum Palette {
Default,
Simple,
}
impl DitherPalette {
impl Palette {
#[must_use]
pub const fn value(&self) -> &[Srgb] {
match self {
@ -46,7 +46,9 @@ impl DitherPalette {
}
}
#[derive(strum::EnumString, Serialize, Deserialize, PartialEq, Eq, Debug)]
#[derive(
strum::EnumString, strum::Display, Serialize, Deserialize, PartialEq, Eq, Debug, Clone,
)]
pub enum DitherMethod {
NearestNeighbor,
FloydSteinberg,
@ -59,11 +61,11 @@ impl DitherMethod {
#[must_use]
pub fn get_ditherer(&self) -> Box<dyn Ditherer> {
match self {
Self::NearestNeighbor => Box::new(NNDither {}),
Self::Atkinson => Box::new(ErrorDiffusionDither::new(ATKINSON_DITHER_POINTS)),
Self::FloydSteinberg => Box::new(ErrorDiffusionDither::new(FLOYD_STEINBERG_POINTS)),
Self::Stuki => Box::new(ErrorDiffusionDither::new(STUKI_DITHER_POINTS)),
Self::Sierra => Box::new(ErrorDiffusionDither::new(SIERRA_DITHER_POINTS)),
Self::NearestNeighbor => Box::new(NearestNeighbor {}),
Self::Atkinson => Box::new(ErrorDiffusion::new(ATKINSON_DITHER_POINTS)),
Self::FloydSteinberg => Box::new(ErrorDiffusion::new(FLOYD_STEINBERG_POINTS)),
Self::Stuki => Box::new(ErrorDiffusion::new(STUKI_DITHER_POINTS)),
Self::Sierra => Box::new(ErrorDiffusion::new(SIERRA_DITHER_POINTS)),
}
}
}
@ -144,12 +146,10 @@ fn nearest_neighbor(input_color: Lab, palette: &[Srgb]) -> (u8, Lab) {
(nearest as u8, color_diff)
}
pub struct NNDither();
pub struct NearestNeighbor();
impl Ditherer for NNDither {
impl Ditherer for NearestNeighbor {
fn dither(&mut self, img: &RgbImage, output: &mut DitheredImage) {
assert!(img.width() == 800);
assert!(img.height() == 480);
// sRGB view into the given image. zero copy!
let srgb = <&[Srgb<u8>]>::from_components(&**img);
@ -246,16 +246,16 @@ static STUKI_DITHER_POINTS: &[DiffusionPoint] = &[
pub type DiffusionMatrix<'a> = &'a [DiffusionPoint];
#[derive(Debug)]
pub struct ErrorDiffusionDither<'a>(&'a [DiffusionPoint]);
pub struct ErrorDiffusion<'a>(&'a [DiffusionPoint]);
impl<'a> ErrorDiffusionDither<'a> {
impl<'a> ErrorDiffusion<'a> {
#[must_use]
pub const fn new(dm: DiffusionMatrix<'a>) -> Self {
Self(dm)
}
}
impl<'a> Ditherer for ErrorDiffusionDither<'a> {
impl<'a> Ditherer for ErrorDiffusion<'a> {
#[instrument]
fn dither(&mut self, img: &RgbImage, output: &mut DitheredImage) {
// create a copy of the image in Lab space, mutable.
@ -275,7 +275,10 @@ impl<'a> Ditherer for ErrorDiffusionDither<'a> {
let curr_pix = temp_img[index];
let (nearest, err) = nearest_neighbor(curr_pix, &output.palette);
// set the color in the output buffer.
*output.buf.get_mut(index).unwrap() = nearest;
*output
.buf
.get_mut(index)
.expect("always in bounds of image") = nearest;
// take the error, and propagate it.
for point in self.0 {
// bounds checking.

View file

@ -0,0 +1 @@
pub mod dither;

View file

@ -1,11 +1,13 @@
pub mod api;
pub mod display;
pub mod errors;
pub mod dither;
pub mod errors;
use std::path::PathBuf;
use crate::display::{EInkPanel, FakeEInk, Wrapper};
use crate::dither::{DitherMethod, DitheredImage};
use clap::{Parser, Subcommand};
use crate::dither::{DitherMethod, DitheredImage, Palette};
use clap::{Args, Parser, Subcommand};
use image::RgbImage;
use tracing::{error, info};
@ -19,6 +21,8 @@ struct Cli {
#[derive(Debug, Subcommand)]
enum Command {
/// Convert an image into a dithered form.
Convert(ConvertArgs),
/// Load a single image
Show,
/// Display a test pattern
@ -27,38 +31,62 @@ enum Command {
Serve,
}
#[derive(Args, Debug)]
struct ConvertArgs {
#[arg(short, long, default_value_t = DitherMethod::NearestNeighbor)]
dither_method: DitherMethod,
input_file: PathBuf,
output_file: PathBuf,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
println!("CLI {cli:?}");
if matches!(cli.command, Command::Show) {
let img: RgbImage = image::ImageReader::open("image.png")?.decode()?.into();
error!("HI");
let mut display = FakeEInk {};
match cli.command {
Command::Convert(a) => {
let input = image::ImageReader::open(a.input_file)?
.decode()?
.into_rgb8();
let mut method = a.dither_method.get_ditherer();
let mut eink_buf = DitheredImage::default();
let mut dither = DitherMethod::Atkinson.get_ditherer();
let mut result = DitheredImage::new(
input.width(),
input.height(),
Palette::Default.value().to_vec(),
);
method.dither(&input, &mut result);
dither.dither(&img, &mut eink_buf);
display.display(&eink_buf)?;
}
if matches!(cli.command, Command::Test) {
let mut display = Wrapper::new()?;
result.into_rgbimage().save(a.output_file)?;
}
Command::Show => {
let img: RgbImage = image::ImageReader::open("image.png")?.decode()?.into();
error!("HI");
let mut display = FakeEInk {};
display.test()?;
}
let mut eink_buf = DitheredImage::default();
let mut dither = DitherMethod::Atkinson.get_ditherer();
if matches!(cli.command, Command::Serve) {
let display = FakeEInk {};
dither.dither(&img, &mut eink_buf);
display.display(&eink_buf)?;
}
Command::Test => {
let mut display = Wrapper::new()?;
let ctx = api::Context::new(Box::new(display));
display.test()?;
}
Command::Serve => {
let display = FakeEInk {};
let app = api::router().with_state(ctx);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
info!("Listening on 0.0.0.0:3000");
axum::serve(listener, app).await?;
let ctx = api::Context::new(Box::new(display));
let app = api::router().with_state(ctx);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
info!("Listening on 0.0.0.0:3000");
axum::serve(listener, app).await?;
}
}
Ok(())

56
tests/dither.rs Normal file
View file

@ -0,0 +1,56 @@
// Regression tests for dithering methods. This file ensures that changes made to how the
// algorithms are implemented does not affect the actual algorithm. This is done with
// 'known-good' sample images (see `samples/`). If changes to the algorithms are made,
// these tests will fail, and that is to be expected. In that case the reference images
// will need to be updated as well.
use anyhow::Result;
use image::{ImageReader, RgbImage};
use pi_frame_server::dither::{DitherMethod, DitheredImage, Palette};
fn compare_original(sample_file: &str, reference_file: &str, method: &DitherMethod) -> Result<()> {
let image: RgbImage = ImageReader::open(format!("samples/{sample_file}"))?
.decode()?
.into_rgb8();
// process
let mut method = method.get_ditherer();
let mut result = DitheredImage::new(
image.width(),
image.height(),
Palette::Default.value().to_vec(),
);
method.dither(&image, &mut result);
let expected = ImageReader::open(format!("tests/known_samples/{reference_file}"))?
.decode()?
.into_rgb8()
.into_raw();
let output = result.into_rgbimage().into_raw();
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_sample_images() -> Result<()> {
compare_original(
"sample_0.tiff",
"sample_0_atkinson.tiff",
&DitherMethod::Atkinson,
)?;
compare_original(
"sample_1.tiff",
"sample_1_atkinson.tiff",
&DitherMethod::Atkinson,
)?;
compare_original(
"sample_1.tiff",
"sample_1_floydsteinberg.tiff",
&DitherMethod::FloydSteinberg,
)?;
Ok(())
}

Binary file not shown.

Binary file not shown.

Binary file not shown.