supprt multiple dithering matrix options

This commit is contained in:
saji 2024-07-17 12:37:43 -05:00
parent 8417562e31
commit 0a7668b0a7
2 changed files with 80 additions and 46 deletions

View file

@ -1,7 +1,7 @@
use image::RgbImage; use image::RgbImage;
use palette::{cast::FromComponents, IntoColor, Lab, Oklch, Srgb};
use palette::color_difference::{Ciede2000, EuclideanDistance}; use palette::color_difference::{Ciede2000, EuclideanDistance};
use palette::{cast::FromComponents, IntoColor, Lab, Oklch, Srgb};
/// Palette used on the display; pixels can be one of these colors. /// Palette used on the display; pixels can be one of these colors.
/// ///
@ -16,18 +16,7 @@ const DISPLAY_PALETTE: [Srgb; 7] = [
Srgb::new(0.757, 0.443, 0.165), // Orange Srgb::new(0.757, 0.443, 0.165), // Orange
]; ];
// fn octcolor_rgb(color: &OctColor) -> &Srgb<u8> { // TODO: support different color palettes.
// match color {
// OctColor::Black => &DISPLAY_PALETTE[0],
// OctColor::White => &DISPLAY_PALETTE[1],
// OctColor::Green => &DISPLAY_PALETTE[2],
// OctColor::Blue => &DISPLAY_PALETTE[3],
// OctColor::Red => &DISPLAY_PALETTE[4],
// OctColor::Yellow => &DISPLAY_PALETTE[5],
// OctColor::Orange => &DISPLAY_PALETTE[6],
// OctColor::HiZ => &DISPLAY_PALETTE[1],
// }
// }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayColor { pub enum DisplayColor {
@ -94,7 +83,7 @@ impl EInkBuffer {
// pub fn make_image(&self) -> RgbImage { // pub fn make_image(&self) -> RgbImage {
// RgbImage::from_fn(800, 480, |x, y| { // RgbImage::from_fn(800, 480, |x, y| {
// let srgb = Srgb::from(self.0[y * 800 + x]); // let srgb = Srgb::from(self.0[y * 800 + x]);
// }) // })
// } // }
} }
@ -121,17 +110,9 @@ impl EInkBuffer {
// } // }
pub trait Ditherer { pub trait Ditherer {
fn dither(&mut self, img: &RgbImage, output: &mut EInkBuffer); fn dither(&self, img: &RgbImage, output: &mut EInkBuffer);
} }
// fn color_distance(c1: LinSrgb, c2: LinSrgb) -> f32 {
// let r2 = (c2.red - c1.red).powf(2.0);
// let g2 = (c2.green - c1.green).powf(2.0);
// let b2 = (c2.blue - c1.blue).powf(2.0);
//
// (r2 + g2 + b2).sqrt()
// }
/// Find the closest approximate palette color to the given sRGB value. /// Find the closest approximate palette color to the given sRGB value.
/// This uses euclidian distance in linear space. /// This uses euclidian distance in linear space.
#[must_use] #[must_use]
@ -151,7 +132,7 @@ pub fn nearest_neighbor(input_color: Lab) -> (DisplayColor, Lab) {
pub struct NNDither(); pub struct NNDither();
impl Ditherer for NNDither { impl Ditherer for NNDither {
fn dither(&mut self, img: &RgbImage, output: &mut EInkBuffer) { fn dither(&self, img: &RgbImage, output: &mut EInkBuffer) {
assert!(img.width() == 800); assert!(img.width() == 800);
assert!(img.height() == 480); assert!(img.height() == 480);
@ -165,8 +146,6 @@ impl Ditherer for NNDither {
} }
} }
pub struct FloydSteinbergDither();
/// Compute the vector index for a given image by using the size of rows. Assumes that images /// Compute the vector index for a given image by using the size of rows. Assumes that images
/// are indexed in row-major order. /// are indexed in row-major order.
const fn coord_to_idx(x: u32, y: u32, xsize: u32) -> usize { const fn coord_to_idx(x: u32, y: u32, xsize: u32) -> usize {
@ -174,7 +153,7 @@ 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 /// Compute the error-adjusted new lab value based on the error value of the currently scanned
/// pixel, plus a scalar factor. /// pixel multiplied by a scalar factor.
fn get_error_adjusted(orig: &Lab, err: &Lab, scalar: f32) -> Lab { fn get_error_adjusted(orig: &Lab, err: &Lab, scalar: f32) -> Lab {
let (p_l, p_a, p_b) = orig.into_components(); let (p_l, p_a, p_b) = orig.into_components();
let (err_l, err_a, err_b) = err.into_components(); let (err_l, err_a, err_b) = err.into_components();
@ -185,6 +164,9 @@ fn get_error_adjusted(orig: &Lab, err: &Lab, scalar: f32) -> Lab {
)) ))
} }
/// ``DiffusionPoint`` is part of the diffusion matrix, represented by a shift in x and y and an error
/// scaling factor.
struct DiffusionPoint { struct DiffusionPoint {
xshift: i32, xshift: i32,
yshift: i32, yshift: i32,
@ -192,6 +174,7 @@ struct DiffusionPoint {
} }
impl DiffusionPoint { impl DiffusionPoint {
/// Creates a new ``DiffusionPoint``
const fn new(xshift: i32, yshift: i32, scale: f32) -> Self { const fn new(xshift: i32, yshift: i32, scale: f32) -> Self {
Self { Self {
xshift, xshift,
@ -201,23 +184,78 @@ impl DiffusionPoint {
} }
} }
const FLOYD_STEINBERG: [DiffusionPoint; 4] = [ static FLOYD_STEINBERG_POINTS: &[DiffusionPoint] = &[
DiffusionPoint::new(1, 0, 7.0 / 16.0), DiffusionPoint::new(1, 0, 7.0 / 16.0),
DiffusionPoint::new(-1, 1, 3.0 / 16.0), DiffusionPoint::new(-1, 1, 3.0 / 16.0),
DiffusionPoint::new(0, 1, 5.0 / 16.0), DiffusionPoint::new(0, 1, 5.0 / 16.0),
DiffusionPoint::new(1, 1, 1.0 / 16.0), DiffusionPoint::new(1, 1, 1.0 / 16.0),
]; ];
const ATKINSON_DITHER: [DiffusionPoint; 6] = [
DiffusionPoint::new(1, 0, 1.0/8.0), static ATKINSON_DITHER_POINTS: &[DiffusionPoint] = &[
DiffusionPoint::new(2, 0, 1.0/8.0), DiffusionPoint::new(1, 0, 1.0 / 8.0),
DiffusionPoint::new(-1, 1, 1.0/8.0), DiffusionPoint::new(2, 0, 1.0 / 8.0),
DiffusionPoint::new(0, 1, 1.0/8.0), DiffusionPoint::new(-1, 1, 1.0 / 8.0),
DiffusionPoint::new(1, 1, 1.0/8.0), DiffusionPoint::new(0, 1, 1.0 / 8.0),
DiffusionPoint::new(0, 2, 1.0/8.0), DiffusionPoint::new(1, 1, 1.0 / 8.0),
DiffusionPoint::new(0, 2, 1.0 / 8.0),
]; ];
impl Ditherer for FloydSteinbergDither { static SIERRA_DITHER_POINTS: &[DiffusionPoint] = &[
fn dither(&mut self, img: &RgbImage, output: &mut EInkBuffer) { DiffusionPoint::new(1, 0, 5.0 / 32.0),
DiffusionPoint::new(2, 0, 3.0 / 32.0),
DiffusionPoint::new(-2, 1, 2.0 / 32.0),
DiffusionPoint::new(-1, 1, 4.0 / 32.0),
DiffusionPoint::new(0, 1, 5.0 / 32.0),
DiffusionPoint::new(1, 1, 4.0 / 32.0),
DiffusionPoint::new(2, 1, 2.0 / 32.0),
DiffusionPoint::new(-1, 2, 2.0 / 32.0),
DiffusionPoint::new(0, 2, 3.0 / 32.0),
DiffusionPoint::new(1, 2, 2.0 / 32.0),
];
static STUKI_DITHER_POINTS: &[DiffusionPoint] = &[
DiffusionPoint::new(1, 0, 8.0 / 42.0),
DiffusionPoint::new(2, 0, 4.0 / 42.0),
DiffusionPoint::new(-2, 1, 2.0 / 42.0),
DiffusionPoint::new(-1, 1, 4.0 / 42.0),
DiffusionPoint::new(0, 1, 8.0 / 42.0),
DiffusionPoint::new(1, 1, 4.0 / 42.0),
DiffusionPoint::new(2, 1, 2.0 / 42.0),
DiffusionPoint::new(-2, 2, 1.0 / 42.0),
DiffusionPoint::new(-1, 2, 2.0 / 42.0),
DiffusionPoint::new(0, 2, 4.0 / 42.0),
DiffusionPoint::new(1, 2, 2.0 / 42.0),
DiffusionPoint::new(1, 2, 1.0 / 42.0),
];
pub enum DiffusionMatrix {
FloydSteinberg,
Atkinson,
Sierra,
Stuki,
}
impl DiffusionMatrix {
fn value(&self) -> &'static [DiffusionPoint] {
match *self {
Self::FloydSteinberg => FLOYD_STEINBERG_POINTS,
Self::Atkinson => ATKINSON_DITHER_POINTS,
Self::Sierra => SIERRA_DITHER_POINTS,
Self::Stuki => STUKI_DITHER_POINTS,
}
}
}
pub struct ErrorDiffusionDither(DiffusionMatrix);
impl ErrorDiffusionDither {
#[must_use]
pub const fn new(dm: DiffusionMatrix) -> Self {
Self(dm)
}
}
impl Ditherer for ErrorDiffusionDither {
fn dither(&self, img: &RgbImage, output: &mut EInkBuffer) {
// create a copy of the image in Lab space, mutable. // create a copy of the image in Lab space, mutable.
let srgb = <&[Srgb<u8>]>::from_components(&**img); let srgb = <&[Srgb<u8>]>::from_components(&**img);
let (xsize, ysize) = img.dimensions(); let (xsize, ysize) = img.dimensions();
@ -235,18 +273,14 @@ impl Ditherer for FloydSteinbergDither {
// set the color in the output buffer. // set the color in the output buffer.
output.0[index] = nearest; output.0[index] = nearest;
// take the error, and propagate it. // take the error, and propagate it.
for point in ATKINSON_DITHER { for point in self.0.value() {
let Some(target_x) = x.checked_add_signed(point.xshift) else { let Some(target_x) = x.checked_add_signed(point.xshift) else {
continue; continue;
}; };
let Some(target_y) = y.checked_add_signed(point.yshift) else { let Some(target_y) = y.checked_add_signed(point.yshift) else {
continue; continue;
}; };
let target = coord_to_idx( let target = coord_to_idx(target_x, target_y, xsize);
target_x,
target_y,
xsize,
);
if let Some(pix) = temp_img.get(target) { if let Some(pix) = temp_img.get(target) {
temp_img[target] = get_error_adjusted(pix, &err, point.scale); temp_img[target] = get_error_adjusted(pix, &err, point.scale);
} }

View file

@ -3,7 +3,7 @@ pub mod display;
pub mod imageproc; pub mod imageproc;
use crate::display::Wrapper; use crate::display::Wrapper;
use crate::imageproc::{Ditherer, EInkBuffer, FloydSteinbergDither, NNDither}; use crate::imageproc::{DiffusionMatrix, EInkBuffer, ErrorDiffusionDither, Ditherer};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use image::RgbImage; use image::RgbImage;
@ -36,7 +36,7 @@ fn main() -> anyhow::Result<()> {
let mut display = Wrapper::new()?; let mut display = Wrapper::new()?;
let mut eink_buf = EInkBuffer::new(800, 480); let mut eink_buf = EInkBuffer::new(800, 480);
let mut dither = FloydSteinbergDither{}; let dither = ErrorDiffusionDither::new(DiffusionMatrix::Atkinson);
dither.dither(&img, &mut eink_buf); dither.dither(&img, &mut eink_buf);
let raw_buf = eink_buf.into_display_buffer(); let raw_buf = eink_buf.into_display_buffer();
@ -52,7 +52,7 @@ fn main() -> anyhow::Result<()> {
if matches!(cli.command, Command::Convert) { if matches!(cli.command, Command::Convert) {
let img: RgbImage = image::io::Reader::open("myimage.png")?.decode()?.into(); let img: RgbImage = image::io::Reader::open("myimage.png")?.decode()?.into();
let mut eink_buf = EInkBuffer::new(800, 480); let mut eink_buf = EInkBuffer::new(800, 480);
let mut dither = FloydSteinbergDither{}; let dither = ErrorDiffusionDither::new(DiffusionMatrix::Atkinson);
dither.dither(&img, &mut eink_buf); dither.dither(&img, &mut eink_buf);
let raw_buf = eink_buf.into_display_buffer(); let raw_buf = eink_buf.into_display_buffer();