Add dither command and threshold dithering
This commit is contained in:
parent
618d6ac4e9
commit
d332a2560e
3 changed files with 292 additions and 1 deletions
136
src/dither.rs
Normal file
136
src/dither.rs
Normal file
|
|
@ -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<C> {
|
||||||
|
fn diff(a: C, b: C) -> f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiffClamp<D> {
|
||||||
|
_phantom: PhantomData<D>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Clamp, D: Difference<C>> Difference<C> for DiffClamp<D> {
|
||||||
|
fn diff(a: C, b: C) -> f32 {
|
||||||
|
D::diff(a.clamp(), b.clamp())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiffEuclid;
|
||||||
|
|
||||||
|
impl<C: EuclideanDistance<Scalar = f32>> Difference<C> for DiffEuclid {
|
||||||
|
fn diff(a: C, b: C) -> f32 {
|
||||||
|
a.distance(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiffHyAb;
|
||||||
|
|
||||||
|
impl<C: IntoColor<Lab>> Difference<C> 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<C: IntoColor<Lab>> Difference<C> 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<C: AsRef<[f32; 3]>> Difference<C> 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<C: AsRef<[f32; 3]>> Difference<C> 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<C> {
|
||||||
|
colors: Vec<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Palette<C> {
|
||||||
|
pub fn new(colors: Vec<C>) -> Self {
|
||||||
|
Self { colors }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nearest<D>(&self, to: C) -> C
|
||||||
|
where
|
||||||
|
C: Copy,
|
||||||
|
D: Difference<C>,
|
||||||
|
{
|
||||||
|
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<C, D> {
|
||||||
|
fn run(image: RgbaImage, palette: Palette<C>) -> RgbaImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AlgoThreshold;
|
||||||
|
|
||||||
|
impl<C, D> Algorithm<C, D> for AlgoThreshold
|
||||||
|
where
|
||||||
|
Srgb: IntoColor<C>,
|
||||||
|
C: IntoColor<Srgb> + Copy,
|
||||||
|
D: Difference<C>,
|
||||||
|
{
|
||||||
|
fn run(mut image: RgbaImage, palette: Palette<C>) -> RgbaImage {
|
||||||
|
for pixel in image.pixels_mut() {
|
||||||
|
let color: C = util::pixel_to_color(*pixel);
|
||||||
|
let color = palette.nearest::<D>(color);
|
||||||
|
util::update_pixel_with_color(pixel, color);
|
||||||
|
}
|
||||||
|
image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,4 +10,5 @@
|
||||||
#![warn(clippy::use_self)]
|
#![warn(clippy::use_self)]
|
||||||
|
|
||||||
pub mod bw;
|
pub mod bw;
|
||||||
|
pub mod dither;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
|
||||||
156
src/main.rs
156
src/main.rs
|
|
@ -10,13 +10,24 @@
|
||||||
#![warn(clippy::use_self)]
|
#![warn(clippy::use_self)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fmt,
|
||||||
io::{Cursor, Read, Write},
|
io::{Cursor, Read, Write},
|
||||||
|
num::ParseIntError,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use image::{ImageFormat, RgbaImage};
|
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)]
|
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||||
enum BwMethod {
|
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<u8>);
|
||||||
|
|
||||||
|
#[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<ParseIntError> for ParseSrgbColorError {
|
||||||
|
fn from(value: ParseIntError) -> Self {
|
||||||
|
Self::ParseIntError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for SrgbColor {
|
||||||
|
type Err = ParseSrgbColorError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let parts = s.split(',').collect::<Vec<_>>();
|
||||||
|
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<SrgbColor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DitherCmd {
|
||||||
|
fn run(self, image: RgbaImage) -> RgbaImage {
|
||||||
|
match self.color_space {
|
||||||
|
DitherColorSpace::Srgb => self.run_c::<Srgb>(image),
|
||||||
|
DitherColorSpace::LinSrgb => self.run_c::<LinSrgb>(image),
|
||||||
|
DitherColorSpace::Cielab => self.run_c::<Lab>(image),
|
||||||
|
DitherColorSpace::Oklab => self.run_c::<Oklab>(image),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_c<C>(self, image: RgbaImage) -> RgbaImage
|
||||||
|
where
|
||||||
|
Srgb: IntoColor<C>,
|
||||||
|
C: AsRef<[f32; 3]>,
|
||||||
|
C: Clamp,
|
||||||
|
C: Copy,
|
||||||
|
C: EuclideanDistance<Scalar = f32>,
|
||||||
|
C: IntoColor<Lab>,
|
||||||
|
C: IntoColor<Srgb>,
|
||||||
|
{
|
||||||
|
match self.difference {
|
||||||
|
DitherDifference::Euclid => self.run_cd::<C, DiffEuclid>(image),
|
||||||
|
DitherDifference::EuclidClamp => self.run_cd::<C, DiffClamp<DiffEuclid>>(image),
|
||||||
|
DitherDifference::HyAb => self.run_cd::<C, DiffHyAb>(image),
|
||||||
|
DitherDifference::HyAbClamp => self.run_cd::<C, DiffClamp<DiffHyAb>>(image),
|
||||||
|
DitherDifference::Ciede2000 => self.run_cd::<C, DiffCiede2000>(image),
|
||||||
|
DitherDifference::Ciede2000Clamp => self.run_cd::<C, DiffClamp<DiffCiede2000>>(image),
|
||||||
|
DitherDifference::Manhattan => self.run_cd::<C, DiffManhattan>(image),
|
||||||
|
DitherDifference::ManhattanClamp => self.run_cd::<C, DiffClamp<DiffManhattan>>(image),
|
||||||
|
DitherDifference::ManhattanSquare => self.run_cd::<C, DiffManhattanSquare>(image),
|
||||||
|
DitherDifference::ManhattanSquareClamp => {
|
||||||
|
self.run_cd::<C, DiffClamp<DiffManhattanSquare>>(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_cd<C, D>(self, image: RgbaImage) -> RgbaImage
|
||||||
|
where
|
||||||
|
Srgb: IntoColor<C>,
|
||||||
|
C: IntoColor<Srgb> + Clamp + Copy,
|
||||||
|
D: Difference<C>,
|
||||||
|
{
|
||||||
|
match self.algorithm {
|
||||||
|
DitherAlgorithm::Threshold => self.run_acd::<AlgoThreshold, C, D>(image),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_acd<A, C, D>(self, image: RgbaImage) -> RgbaImage
|
||||||
|
where
|
||||||
|
Srgb: IntoColor<C>,
|
||||||
|
A: Algorithm<C, D>,
|
||||||
|
{
|
||||||
|
let colors = self
|
||||||
|
.palette
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| c.0.into_format().into_color())
|
||||||
|
.collect::<Vec<C>>();
|
||||||
|
let palette = Palette::<C>::new(colors);
|
||||||
|
A::run(image, palette)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
enum Cmd {
|
enum Cmd {
|
||||||
Bw(BwCmd),
|
Bw(BwCmd),
|
||||||
|
Dither(DitherCmd),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd {
|
impl Cmd {
|
||||||
fn run(self, image: RgbaImage) -> RgbaImage {
|
fn run(self, image: RgbaImage) -> RgbaImage {
|
||||||
match self {
|
match self {
|
||||||
Self::Bw(cmd) => cmd.run(image),
|
Self::Bw(cmd) => cmd.run(image),
|
||||||
|
Self::Dither(cmd) => cmd.run(image),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue