From d332a2560e0c44e51bf062deb66300a0393f6e39 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 30 Jul 2023 20:00:03 +0200 Subject: [PATCH] Add dither command and threshold dithering --- src/dither.rs | 136 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 156 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 src/dither.rs diff --git a/src/dither.rs b/src/dither.rs new file mode 100644 index 0000000..a6ebc0d --- /dev/null +++ b/src/dither.rs @@ -0,0 +1,136 @@ +//! Various dithering algorithms and supporting types. +//! +//! The cumbersome types in this module are there for performance. For example, +//! the program should not switch on the configured difference whenever it +//! compares two colors. Instead, a version of each algorithm should be compiled +//! for each color space and difference combination. + +use std::marker::PhantomData; + +use image::RgbaImage; +use palette::{ + color_difference::{Ciede2000, EuclideanDistance, HyAb}, + Clamp, IntoColor, Lab, Srgb, +}; + +use crate::util; + +////////////////////// +// Color difference // +////////////////////// + +pub trait Difference { + fn diff(a: C, b: C) -> f32; +} + +pub struct DiffClamp { + _phantom: PhantomData, +} + +impl> Difference for DiffClamp { + fn diff(a: C, b: C) -> f32 { + D::diff(a.clamp(), b.clamp()) + } +} + +pub struct DiffEuclid; + +impl> Difference for DiffEuclid { + fn diff(a: C, b: C) -> f32 { + a.distance(b) + } +} + +pub struct DiffHyAb; + +impl> Difference for DiffHyAb { + fn diff(a: C, b: C) -> f32 { + let a: Lab = a.into_color(); + let b: Lab = b.into_color(); + a.hybrid_distance(b) + } +} + +pub struct DiffCiede2000; + +impl> Difference for DiffCiede2000 { + fn diff(a: C, b: C) -> f32 { + let a: Lab = a.into_color(); + let b: Lab = b.into_color(); + a.difference(b) + } +} + +pub struct DiffManhattan; + +impl> Difference for DiffManhattan { + fn diff(a: C, b: C) -> f32 { + let [a1, a2, a3] = a.as_ref(); + let [b1, b2, b3] = b.as_ref(); + (a1 - b1).abs() + (a2 - b2).abs() + (a3 - b3).abs() + } +} + +pub struct DiffManhattanSquare; + +impl> Difference for DiffManhattanSquare { + fn diff(a: C, b: C) -> f32 { + let [a1, a2, a3] = a.as_ref(); + let [b1, b2, b3] = b.as_ref(); + (a1 - b1).powi(2) + (a2 - b2).powi(2) + (a3 - b3).powi(3) + } +} + +///////////// +// Palette // +///////////// + +pub struct Palette { + colors: Vec, +} + +impl Palette { + pub fn new(colors: Vec) -> Self { + Self { colors } + } + + fn nearest(&self, to: C) -> C + where + C: Copy, + D: Difference, + { + self.colors + .iter() + .copied() + .map(|c| (c, D::diff(c, to))) + .min_by(|(_, a), (_, b)| a.total_cmp(b)) + .expect("palette was empty") + .0 + } +} + +//////////////// +// Algorithms // +//////////////// + +pub trait Algorithm { + fn run(image: RgbaImage, palette: Palette) -> RgbaImage; +} + +pub struct AlgoThreshold; + +impl Algorithm for AlgoThreshold +where + Srgb: IntoColor, + C: IntoColor + Copy, + D: Difference, +{ + fn run(mut image: RgbaImage, palette: Palette) -> RgbaImage { + for pixel in image.pixels_mut() { + let color: C = util::pixel_to_color(*pixel); + let color = palette.nearest::(color); + util::update_pixel_with_color(pixel, color); + } + image + } +} diff --git a/src/lib.rs b/src/lib.rs index d06b29c..c602e7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,4 +10,5 @@ #![warn(clippy::use_self)] pub mod bw; +pub mod dither; mod util; diff --git a/src/main.rs b/src/main.rs index 226d992..ec39d37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,13 +10,24 @@ #![warn(clippy::use_self)] use std::{ + error::Error, + fmt, io::{Cursor, Read, Write}, + num::ParseIntError, path::PathBuf, + str::FromStr, }; use clap::Parser; use image::{ImageFormat, RgbaImage}; -use mark::bw; +use mark::{ + bw, + dither::{ + AlgoThreshold, Algorithm, DiffCiede2000, DiffClamp, DiffEuclid, DiffHyAb, DiffManhattan, + DiffManhattanSquare, Difference, Palette, + }, +}; +use palette::{color_difference::EuclideanDistance, Clamp, IntoColor, Lab, LinSrgb, Oklab, Srgb}; #[derive(Debug, Clone, Copy, clap::ValueEnum)] enum BwMethod { @@ -55,15 +66,158 @@ impl BwCmd { } } +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +enum DitherAlgorithm { + Threshold, +} + +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +enum DitherColorSpace { + Srgb, + LinSrgb, + Cielab, + Oklab, +} + +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +enum DitherDifference { + Euclid, + EuclidClamp, + HyAb, + HyAbClamp, + Ciede2000, + Ciede2000Clamp, + Manhattan, + ManhattanClamp, + ManhattanSquare, + ManhattanSquareClamp, +} + +#[derive(Debug, Clone, Copy)] +struct SrgbColor(Srgb); + +#[derive(Debug)] +enum ParseSrgbColorError { + ThreeValuesRequired, + ParseIntError(ParseIntError), +} + +impl fmt::Display for ParseSrgbColorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ThreeValuesRequired => write!(f, "exactly three values must be specified"), + Self::ParseIntError(e) => e.fmt(f), + } + } +} + +impl Error for ParseSrgbColorError {} + +impl From for ParseSrgbColorError { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +impl FromStr for SrgbColor { + type Err = ParseSrgbColorError; + + fn from_str(s: &str) -> Result { + let parts = s.split(',').collect::>(); + if let [r, g, b] = &*parts { + Ok(Self(Srgb::new(r.parse()?, g.parse()?, b.parse()?))) + } else { + Err(ParseSrgbColorError::ThreeValuesRequired) + } + } +} + +#[derive(Debug, clap::Parser)] +/// Dither images. +struct DitherCmd { + #[arg(long, short)] + algorithm: DitherAlgorithm, + #[arg(long, short)] + color_space: DitherColorSpace, + #[arg(long, short)] + difference: DitherDifference, + #[arg(long, short)] + palette: Vec, +} + +impl DitherCmd { + fn run(self, image: RgbaImage) -> RgbaImage { + match self.color_space { + DitherColorSpace::Srgb => self.run_c::(image), + DitherColorSpace::LinSrgb => self.run_c::(image), + DitherColorSpace::Cielab => self.run_c::(image), + DitherColorSpace::Oklab => self.run_c::(image), + } + } + + fn run_c(self, image: RgbaImage) -> RgbaImage + where + Srgb: IntoColor, + C: AsRef<[f32; 3]>, + C: Clamp, + C: Copy, + C: EuclideanDistance, + C: IntoColor, + C: IntoColor, + { + match self.difference { + DitherDifference::Euclid => self.run_cd::(image), + DitherDifference::EuclidClamp => self.run_cd::>(image), + DitherDifference::HyAb => self.run_cd::(image), + DitherDifference::HyAbClamp => self.run_cd::>(image), + DitherDifference::Ciede2000 => self.run_cd::(image), + DitherDifference::Ciede2000Clamp => self.run_cd::>(image), + DitherDifference::Manhattan => self.run_cd::(image), + DitherDifference::ManhattanClamp => self.run_cd::>(image), + DitherDifference::ManhattanSquare => self.run_cd::(image), + DitherDifference::ManhattanSquareClamp => { + self.run_cd::>(image) + } + } + } + + fn run_cd(self, image: RgbaImage) -> RgbaImage + where + Srgb: IntoColor, + C: IntoColor + Clamp + Copy, + D: Difference, + { + match self.algorithm { + DitherAlgorithm::Threshold => self.run_acd::(image), + } + } + + fn run_acd(self, image: RgbaImage) -> RgbaImage + where + Srgb: IntoColor, + A: Algorithm, + { + let colors = self + .palette + .into_iter() + .map(|c| c.0.into_format().into_color()) + .collect::>(); + let palette = Palette::::new(colors); + A::run(image, palette) + } +} + #[derive(Debug, clap::Parser)] enum Cmd { Bw(BwCmd), + Dither(DitherCmd), } impl Cmd { fn run(self, image: RgbaImage) -> RgbaImage { match self { Self::Bw(cmd) => cmd.run(image), + Self::Dither(cmd) => cmd.run(image), } } }