diff options
| author | ache <ache@ache.one> | 2025-04-23 17:12:17 +0200 |
|---|---|---|
| committer | ache <ache@ache.one> | 2025-04-23 17:12:17 +0200 |
| commit | d9630d3342ebdbf4eb952a6090e68021de2acd3d (patch) | |
| tree | 9b8518d0fd467fe9cdbca6f73d731f7b45843c55 | |
| parent | Implement sierra (diff) | |
Worst atomic commit ever
| -rw-r--r-- | Cargo.toml | 23 | ||||
| -rw-r--r-- | benches/all_samples.rs | 255 | ||||
| -rw-r--r-- | benches/random.rs | 44 | ||||
| -rw-r--r-- | benches/shift_kernels.rs | 173 | ||||
| -rw-r--r-- | samples/david.png (renamed from david.png) | bin | 27218 -> 27218 bytes | |||
| -rw-r--r-- | samples/david2.jpg | bin | 0 -> 86914 bytes | |||
| -rw-r--r-- | samples/diderot_111.png | bin | 0 -> 4734542 bytes | |||
| -rw-r--r-- | samples/jane.jpg | bin | 0 -> 386231 bytes | |||
| -rw-r--r-- | samples/jane2.webp | bin | 0 -> 316512 bytes | |||
| -rw-r--r-- | samples/karel_käos.jpg | bin | 0 -> 114462 bytes | |||
| -rw-r--r-- | samples/us.png | bin | 0 -> 361839 bytes | |||
| -rw-r--r-- | src/kernel.rs | 925 | ||||
| -rw-r--r-- | src/lib.rs | 750 | ||||
| -rw-r--r-- | src/main.rs | 175 | ||||
| -rw-r--r-- | src/test_all.rs | 147 |
15 files changed, 2423 insertions, 69 deletions
@@ -1,7 +1,30 @@ [package] name = "dither" version = "0.1.0" +authors = ["ache <ache@ache.one>"] edition = "2024" [dependencies] image = "0.25.6" +palette = "0.7.6" +rand = { version = "0.9.1", features = ["small_rng"] } + +[dev-dependencies] +criterion = "0.5.1" +divan = "0.1.21" + +[features] +check-bench = [] + +[[bench]] +name = "shift_kernels" +harness = false + + +[[bench]] +name = "all_samples" +harness = false + +[[bench]] +name = "random" +harness = false diff --git a/benches/all_samples.rs b/benches/all_samples.rs new file mode 100644 index 0000000..7a8b6b8 --- /dev/null +++ b/benches/all_samples.rs @@ -0,0 +1,255 @@ +use divan::{Bencher, black_box}; +use image::ImageReader; + +const LIST_OF_SAMPLES: [&str; 4] = [ + "samples/david.png", + "samples/karel_käos.jpg", + "samples/jane.jpg", + "samples/diderot_111.png", +]; + +fn main() { + // Run `add` benchmark: + divan::main(); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_burkes(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither(&mut img, dither::kernel::BURKES_KERNEL.to_vec()) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_atkinson_base(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_atkinson_base(&mut img) + }); +} +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_atkinson(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither(&mut img, dither::kernel::ATKINSON_KERNEL.to_vec()) + }); +} +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_atkinson_ref(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::ATKINSON_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_atkinson_int(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + let kernel = dither::kernel::ATKINSON_KERNEL_INTEGER.to_vec(); + dither::dither_shift( + &mut img, + dither::kernel::KernelShift { + kernel, + bit_shift: dither::kernel::ATKINSON_KERNEL_BIT_SHIFT, + }, + ); + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_floyd_steinberg(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::FLOYD_STEINBERG_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_fedoseev(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::FEDOSEEV_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_fedoseev2(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::FEDOSEEV2_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_fedoseev3(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::FEDOSEEV3_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_fedoseev4(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::FEDOSEEV4_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_wong_allebach(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::WONG_ALLEBACH_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_jarvis_judice_ninke(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::JARVIS_JUDICE_NINKE_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_stucki(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::STUCKI_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_sierra(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::SIERRA3_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_sierra_two_rows(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::SIERRA2_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} + +#[divan::bench(args = LIST_OF_SAMPLES)] +fn test_sierra_lite(bencher: Bencher, img: &str) { + let sample_img = ImageReader::open(img) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + let kernel = dither::kernel::SIERRA_LITE_KERNEL.to_vec(); + + bencher.bench_local(|| { + let mut img = black_box(sample_img.clone()); + dither::dither_ref(&mut img, &kernel) + }); +} diff --git a/benches/random.rs b/benches/random.rs new file mode 100644 index 0000000..a7446c9 --- /dev/null +++ b/benches/random.rs @@ -0,0 +1,44 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use image::ImageReader; + +const LIST_OF_SAMPLES: [&str; 7] = [ + "samples/david.png", + "samples/david2.jpg", + "samples/us.png", + "samples/diderot_111.png", + "samples/jane.jpg", + "samples/jane2.webp", + "samples/karel_käos.jpg", +]; + +pub fn bench_random(c: &mut Criterion) { + let img = ImageReader::open(LIST_OF_SAMPLES[1]) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + c.bench_function("dither random", |b| { + b.iter(|| { + let mut img = img.clone(); + dither::dither_random(&mut img) + }) + }); +} +pub fn bench_random_color(c: &mut Criterion) { + let img = ImageReader::open(LIST_OF_SAMPLES[1]) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + c.bench_function("dither random", |b| { + b.iter(|| { + let mut img = img.clone(); + dither::dither_random_color(&mut img) + }) + }); +} + +criterion_group!(random, bench_random, bench_random_color); +criterion_main!(random); diff --git a/benches/shift_kernels.rs b/benches/shift_kernels.rs new file mode 100644 index 0000000..265bb20 --- /dev/null +++ b/benches/shift_kernels.rs @@ -0,0 +1,173 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use dither::kernel; +use image::ImageReader; + +const LIST_OF_SAMPLES: [&str; 7] = [ + "samples/david.png", + "samples/david2.jpg", + "samples/us.png", + "samples/diderot_111.png", + "samples/jane.jpg", + "samples/jane2.webp", + "samples/karel_käos.jpg", +]; + +pub fn bench_integer_variation(c: &mut Criterion) { + let img = ImageReader::open(LIST_OF_SAMPLES[0]) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + c.bench_function("dither atkinson integer only", |b| { + b.iter(|| { + let mut img = img.clone(); + dither::dither_shift( + &mut img, + kernel::KernelShift { + kernel: dither::kernel::ATKINSON_KERNEL_INTEGER.to_vec(), + bit_shift: kernel::ATKINSON_KERNEL_BIT_SHIFT, + }, + ) + }) + }); + c.bench_function("dither atkinson", |b| { + b.iter(|| { + let mut img = img.clone(); + dither::dither(&mut img, dither::kernel::ATKINSON_KERNEL.to_vec()) + }) + }); + /* + * This bench verify that the image loading isn't the only thing we benchmark. + * Indeed it isn't. + * + * Check it with: + * $ cargo bench --features check-bench + */ + + #[cfg(feature = "check-bench")] + c.bench_function("image loading", |b| { + b.iter(|| { + let mut img = img.clone(); + std::hint::black_box(&mut img); + }) + }); + #[cfg(feature = "check-bench")] + c.bench_function("image loading no black box", |b| { + b.iter(|| { + let _ = img.clone(); + }) + }); + #[cfg(feature = "check-bench")] + c.bench_function("image loading only black box", |b| { + b.iter(|| { + std::hint::black_box(11); + }) + }); +} +pub fn bench_dithers_on_david(c: &mut Criterion) { + let sample_img = ImageReader::open(LIST_OF_SAMPLES[0]) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + let algos: [(&str, dither::DitherFn); 14] = [ + ("kernel_atkinson", |img| { + dither::dither(img, kernel::ATKINSON_KERNEL.to_vec()) + }), + ("kernel_atkinson_integer", |img| { + dither::dither_shift( + img, + dither::kernel::KernelShift { + kernel: kernel::ATKINSON_KERNEL_INTEGER.to_vec(), + bit_shift: kernel::ATKINSON_KERNEL_BIT_SHIFT, + }, + ) + }), + ("kernel_floyd_steinberg", |img| { + dither::dither(img, dither::kernel::FLOYD_STEINBERG_KERNEL.to_vec()) + }), + ("kernel_fedoseev", |img| { + dither::dither(img, dither::kernel::FEDOSEEV_KERNEL.to_vec()) + }), + ("kernel_fedoseev2", |img| { + dither::dither(img, dither::kernel::FEDOSEEV2_KERNEL.to_vec()) + }), + ("kernel_fedoseev3", |img| { + dither::dither(img, dither::kernel::FEDOSEEV3_KERNEL.to_vec()) + }), + ("kernel_fedoseev4", |img| { + dither::dither(img, dither::kernel::FEDOSEEV4_KERNEL.to_vec()) + }), + ("kernel_wong_allebach", |img| { + dither::dither(img, dither::kernel::WONG_ALLEBACH_KERNEL.to_vec()) + }), + ("kernel_jarvis_judice_ninke", |img| { + dither::dither(img, dither::kernel::JARVIS_JUDICE_NINKE_KERNEL.to_vec()) + }), + ("kernel_stucki", |img| { + dither::dither(img, dither::kernel::STUCKI_KERNEL.to_vec()) + }), + ("kernel_burkes", |img| { + dither::dither(img, dither::kernel::BURKES_KERNEL.to_vec()) + }), + ("kernel_sierra3", |img| { + dither::dither(img, dither::kernel::SIERRA3_KERNEL.to_vec()) + }), + ("kernel_sierra2", |img| { + dither::dither(img, dither::kernel::SIERRA2_KERNEL.to_vec()) + }), + ("kernel_sierra-lite", |img| { + dither::dither(img, dither::kernel::SIERRA_LITE_KERNEL.to_vec()) + }), + /* + ("kernel_hallucination_prewitt", |img| { + dither::dither(img, dither::kernel::PREWITT_KERNEL.to_vec()) + }), + ("kernel_hallucination_roberts", |img| { + dither::dither(img, dither::kernel::ROBERTS_KERNEL.to_vec()) + }), + ("kernel_hallucination_sobel", |img| { + dither::dither(img, dither::kernel::SOBEL_KERNEL.to_vec()) + }), + */ + ]; + + let mut group = c.benchmark_group("every dithering algorithm on david"); + + for algo in &algos { + group.bench_function(&format!("{} on david", algo.0), |b| { + b.iter(|| { + let mut img = sample_img.clone(); + algo.1(&mut img) + }) + }); + } +} +pub fn bench_stucki_all_samples(c: &mut Criterion) { + let mut group = c.benchmark_group("stucki on all samples"); + + for sample in LIST_OF_SAMPLES { + let sample_img = ImageReader::open(sample) + .unwrap() + .decode() + .unwrap() + .into_rgb8(); + + group.bench_function(&format!("Stucki on {}", sample), |b| { + b.iter(|| { + let mut img = sample_img.clone(); + dither::dither(&mut img, kernel::STUCKI_KERNEL.to_vec()) + }) + }); + } + + group.finish(); +} + +criterion_group!(benche_int, bench_integer_variation); +criterion_group!(benche_all_samples, bench_stucki_all_samples); +criterion_group!(benche_all_kernels, bench_dithers_on_david); + +criterion_main!(benche_int, benche_all_samples, benche_all_kernels); diff --git a/david.png b/samples/david.png Binary files differindex 6cfa884..6cfa884 100644 --- a/david.png +++ b/samples/david.png diff --git a/samples/david2.jpg b/samples/david2.jpg Binary files differnew file mode 100644 index 0000000..4ae1200 --- /dev/null +++ b/samples/david2.jpg diff --git a/samples/diderot_111.png b/samples/diderot_111.png Binary files differnew file mode 100644 index 0000000..9197157 --- /dev/null +++ b/samples/diderot_111.png diff --git a/samples/jane.jpg b/samples/jane.jpg Binary files differnew file mode 100644 index 0000000..3d1ef81 --- /dev/null +++ b/samples/jane.jpg diff --git a/samples/jane2.webp b/samples/jane2.webp Binary files differnew file mode 100644 index 0000000..09fccb2 --- /dev/null +++ b/samples/jane2.webp diff --git a/samples/karel_käos.jpg b/samples/karel_käos.jpg Binary files differnew file mode 100644 index 0000000..71cbe92 --- /dev/null +++ b/samples/karel_käos.jpg diff --git a/samples/us.png b/samples/us.png Binary files differnew file mode 100644 index 0000000..96a090a --- /dev/null +++ b/samples/us.png diff --git a/src/kernel.rs b/src/kernel.rs new file mode 100644 index 0000000..f1cb78d --- /dev/null +++ b/src/kernel.rs @@ -0,0 +1,925 @@ +#[derive(Debug, Clone)] +pub struct WeightedNeighbor { + pub dx: i32, + pub dy: i32, + pub weight: f32, +} + +#[derive(Debug, Clone)] +pub struct WeightedNeighborInteger { + pub dx: i32, + pub dy: i32, + pub weight: i32, +} + +pub struct KernelShift { + pub kernel: Vec<WeightedNeighborInteger>, + pub bit_shift: u32, +} + +pub type Kernel = Vec<WeightedNeighbor>; + +/** +* Atkinson kernel. +* X Represents the current pixel. +* +* [ 0 ] [ X ] [ 1 ] +* [ 1 ] [ 1 ] [ 1 ] +* [ 0 ] [ 1 ] [ 0 ] +* +* Weight multiplicator: 1.0 / 8.0 +*/ +pub const ATKINSON_KERNEL: [WeightedNeighbor; 6] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 1.0 / 8.0, + }, + WeightedNeighbor { + dx: 2, + dy: 0, + weight: 1.0 / 8.0, + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 1.0 / 8.0, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 1.0 / 8.0, + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 1.0 / 8.0, + }, + WeightedNeighbor { + dx: 0, + dy: 2, + weight: 1.0 / 8.0, + }, +]; + +pub const ATKINSON_KERNEL_INTEGER: [WeightedNeighborInteger; 6] = [ + WeightedNeighborInteger { + dx: 1, + dy: 0, + weight: 1, + }, + WeightedNeighborInteger { + dx: 2, + dy: 0, + weight: 1, + }, + WeightedNeighborInteger { + dx: -1, + dy: 1, + weight: 1, + }, + WeightedNeighborInteger { + dx: 0, + dy: 1, + weight: 1, + }, + WeightedNeighborInteger { + dx: 1, + dy: 1, + weight: 1, + }, + WeightedNeighborInteger { + dx: 0, + dy: 2, + weight: 1, + }, +]; +pub const ATKINSON_KERNEL_BIT_SHIFT: u32 = 3; + +/** +* Floyd Steinberg kernel. + X Represent the current pixel. + + [ 0 ] [ X ] [ 7 ] + [ 3 ] [ 5 ] [ 1 ] + +* Weight multiplicator: 1.0 / 16.0 +*/ +pub const FLOYD_STEINBERG_KERNEL: [WeightedNeighbor; 4] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 7.0 / 16.0, + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 3.0 / 16.0, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 5.0 / 16.0, + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 1.0 / 16.0, + }, +]; + +pub const FLOYD_STEINBERG_KERNEL_INTEGER: [WeightedNeighborInteger; 4] = [ + WeightedNeighborInteger { + dx: 1, + dy: 0, + weight: 7, + }, + WeightedNeighborInteger { + dx: -1, + dy: 1, + weight: 3, + }, + WeightedNeighborInteger { + dx: 0, + dy: 1, + weight: 5, + }, + WeightedNeighborInteger { + dx: 1, + dy: 1, + weight: 1, + }, +]; +pub const FLOYD_STEINBERG_KERNEL_BIT_SHIFT: u32 = 4; + +/** + * Fedoseev kernel. + * X Represent the current pixel. + * + * [ ] [ 0 ] [ X ] [ 0.5423 ] [ 0.0533 ] + * [ 0.0246 ] [ 0.2191 ] [ 0.4715 ] [-0.0023 ] [-0.1241 ] + * [-0.0065 ] [-0.0692 ] [ 0.0168 ] [-0.0952 ] [-0.0304 ] + */ +pub const FEDOSEEV_KERNEL: [WeightedNeighbor; 12] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 0.5423, + }, + WeightedNeighbor { + dx: 2, + dy: 0, + weight: 0.0533, + }, + WeightedNeighbor { + dx: -2, + dy: 1, + weight: -0.0246, + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 0.2191, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 0.4715, + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: -0.0023, + }, + WeightedNeighbor { + dx: 2, + dy: 1, + weight: -0.1241, + }, + WeightedNeighbor { + dx: -2, + dy: 2, + weight: -0.0065, + }, + WeightedNeighbor { + dx: -1, + dy: 2, + weight: -0.0692, + }, + WeightedNeighbor { + dx: 0, + dy: 2, + weight: -0.0168, + }, + WeightedNeighbor { + dx: 1, + dy: 2, + weight: -0.0952, + }, + WeightedNeighbor { + dx: 2, + dy: 2, + weight: -0.0304, + }, +]; + +/** + * Fedoseev2 kernel. + * X Represent the current pixel. + + [ X ] [ 0.4364 ] + [ 0.5636 ] [ ] +*/ +pub const FEDOSEEV2_KERNEL: [WeightedNeighbor; 2] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 0.4364, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 0.5636, + }, +]; + +/** + * Fedoseev3 kernel. + * X Represent the current pixel. + * + * [ 0 ] [ X ] [ 0.5221 ] + * [ 0.1854 ] [ 0.4689 ] [-0.1763 ] + */ +pub const FEDOSEEV3_KERNEL: [WeightedNeighbor; 4] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 0.5221, + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 0.1854, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 0.4689, + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: -0.1763, + }, +]; + +/** + * Fedoseev kernel 4. +* X Represent the current pixel. +* +* [ 0 ] [ X ] [ 0.5221 ] +* [ 0.1854 ] [ 0.4689 ] [-0.1763 ] +*/ +pub const FEDOSEEV4_KERNEL: [WeightedNeighbor; 4] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 0.5221, + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 0.1854, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 0.4689, + }, + WeightedNeighbor { + dx: -1, + dy: 0, + weight: -0.1763, + }, +]; + +/** +* Wong-Allebach kernel. + X Represent the current pixel. + + [ 0 ] [ X ] [ 0.2911 ] + [ 0.1373 ] [ 0.3457 ] [ 0.2258 ] +*/ +pub const WONG_ALLEBACH_KERNEL: [WeightedNeighbor; 4] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 0.2911, + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 0.1373, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 0.3457, + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 0.2258, + }, +]; + +/** +* Jarvis-Judice-Ninke' kernel. + X Represent the current pixel. + + [ 0 ] [ 0 ] [ X ] [ 7 ] [ 5 ] + [ 3 ] [ 5 ] [ 7 ] [ 5 ] [ 3 ] + [ 1 ] [ 3 ] [ 5 ] [ 3 ] [ 1 ] + +* Weight multiplicator: 1.0 / 48.0 +*/ +pub const JARVIS_JUDICE_NINKE_KERNEL: [WeightedNeighbor; 12] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 7. / 48., + }, + WeightedNeighbor { + dx: 2, + dy: 0, + weight: 5. / 48., + }, + WeightedNeighbor { + dx: -2, + dy: 1, + weight: 3. / 48., + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 5. / 48., + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 7. / 48., + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 5. / 48., + }, + WeightedNeighbor { + dx: 2, + dy: 1, + weight: 3. / 48., + }, + WeightedNeighbor { + dx: -2, + dy: 2, + weight: 1. / 48., + }, + WeightedNeighbor { + dx: -1, + dy: 2, + weight: 3. / 48., + }, + WeightedNeighbor { + dx: 0, + dy: 2, + weight: 5. / 48., + }, + WeightedNeighbor { + dx: 1, + dy: 2, + weight: 3. / 48., + }, + WeightedNeighbor { + dx: 2, + dy: 2, + weight: 1. / 48., + }, +]; + +/** + * Stucki kernel. + * X Represents the current pixel. + * + * [ 0 ] [ 0 ] [ X ] [ 8 ] [ 4 ] + * [ 2 ] [ 4 ] [ 8 ] [ 4 ] [ 2 ] + * [ 1 ] [ 2 ] [ 4 ] [ 2 ] [ 1 ] + * + * Weight multiplicator: 1.0 / 42.0 + */ +pub const STUCKI_KERNEL: [WeightedNeighbor; 12] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 8. / 42., + }, + WeightedNeighbor { + dx: 2, + dy: 0, + weight: 4. / 42., + }, + WeightedNeighbor { + dx: -2, + dy: 1, + weight: 2. / 42., + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 4. / 42., + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 8. / 42., + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 4. / 42., + }, + WeightedNeighbor { + dx: 2, + dy: 1, + weight: 2. / 42., + }, + WeightedNeighbor { + dx: -2, + dy: 2, + weight: 1. / 42., + }, + WeightedNeighbor { + dx: -1, + dy: 2, + weight: 2. / 42., + }, + WeightedNeighbor { + dx: 0, + dy: 2, + weight: 4. / 42., + }, + WeightedNeighbor { + dx: 1, + dy: 2, + weight: 2. / 42., + }, + WeightedNeighbor { + dx: 2, + dy: 2, + weight: 1. / 42., + }, +]; +/** + * The Burkes kernel. +* X Represents the current pixel. +* +* [ ] [ ] [ X ] [ 4 ] [ 2 ] +* [ 1 ] [ 2 ] [ 4 ] [ 2 ] [ 1 ] +* +* Weight multiplicator: 1.0 / 16.0 +*/ +pub const BURKES_KERNEL: [WeightedNeighbor; 7] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 4. / 16., + }, + WeightedNeighbor { + dx: 2, + dy: 0, + weight: 2. / 16., + }, + WeightedNeighbor { + dx: -2, + dy: 1, + weight: 1. / 16., + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 2. / 16., + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 4. / 16., + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 2. / 16., + }, + WeightedNeighbor { + dx: 2, + dy: 1, + weight: 1. / 16., + }, +]; + +pub const BURKES_KERNEL_INTEGER: [WeightedNeighborInteger; 7] = [ + WeightedNeighborInteger { + dx: 1, + dy: 0, + weight: 4, + }, + WeightedNeighborInteger { + dx: 2, + dy: 0, + weight: 2, + }, + WeightedNeighborInteger { + dx: -2, + dy: 1, + weight: 1, + }, + WeightedNeighborInteger { + dx: -1, + dy: 1, + weight: 2, + }, + WeightedNeighborInteger { + dx: 0, + dy: 1, + weight: 4, + }, + WeightedNeighborInteger { + dx: 1, + dy: 1, + weight: 2, + }, + WeightedNeighborInteger { + dx: 2, + dy: 1, + weight: 1, + }, +]; +pub const BURKES_KERNEL_BIT_SHIFT: u32 = 4; + +pub const KONG_KUI_KERNEL_INTEGER: [WeightedNeighborInteger; 2] = [ + WeightedNeighborInteger { + dx: 1, + dy: 0, + weight: 1, + }, + WeightedNeighborInteger { + dx: 0, + dy: 1, + weight: 1, + }, +]; +pub const KONG_KUI_KERNEL_BIT_SHIFT: u32 = 1; + +/** +* Sierra 3 kernel. +* X Represents the current pixel. +* +* [ ] [ ] [ X ] [ 5 ] [ 3 ] +* [ 2 ] [ 4 ] [ 5 ] [ 4 ] [ 2 ] +* [ ] [ 2 ] [ 3 ] [ 2 ] [ ] +* +* Weight multiplicator: 1.0 / 32.0 +*/ +pub const SIERRA3_KERNEL: [WeightedNeighbor; 10] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 5. / 32., + }, + WeightedNeighbor { + dx: 2, + dy: 0, + weight: 3. / 32., + }, + WeightedNeighbor { + dx: -2, + dy: 1, + weight: 2. / 32., + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 4. / 32., + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 5. / 32., + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 4. / 32., + }, + WeightedNeighbor { + dx: 2, + dy: 1, + weight: 2. / 32., + }, + WeightedNeighbor { + dx: -1, + dy: 2, + weight: 2. / 32., + }, + WeightedNeighbor { + dx: 0, + dy: 2, + weight: 3. / 32., + }, + WeightedNeighbor { + dx: 1, + dy: 2, + weight: 2. / 32., + }, +]; + +pub const SIERRA3_KERNEL_INTEGER: [WeightedNeighborInteger; 10] = [ + WeightedNeighborInteger { + dx: 1, + dy: 0, + weight: 5, + }, + WeightedNeighborInteger { + dx: 2, + dy: 0, + weight: 3, + }, + WeightedNeighborInteger { + dx: -2, + dy: 1, + weight: 2, + }, + WeightedNeighborInteger { + dx: -1, + dy: 1, + weight: 4, + }, + WeightedNeighborInteger { + dx: 0, + dy: 1, + weight: 5, + }, + WeightedNeighborInteger { + dx: 1, + dy: 1, + weight: 4, + }, + WeightedNeighborInteger { + dx: 2, + dy: 1, + weight: 2, + }, + WeightedNeighborInteger { + dx: -1, + dy: 2, + weight: 2, + }, + WeightedNeighborInteger { + dx: 0, + dy: 2, + weight: 3, + }, + WeightedNeighborInteger { + dx: 1, + dy: 2, + weight: 2, + }, +]; +pub const SIERRA3_KERNEL_BIT_SHIFT: u32 = 5; + +/** Sierra two-row kernel. Also known as Sierra2. +* X Represents the current pixel. +* +* [ ] [ ] [ X ] [ 4 ] [ 3 ] +* [ 1 ] [ 2 ] [ 3 ] [ 2 ] [ 1 ] +* +* Weight multiplicator: 1.0 / 16.0 +*/ +pub const SIERRA2_KERNEL: [WeightedNeighbor; 7] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 4. / 16., + }, + WeightedNeighbor { + dx: 2, + dy: 0, + weight: 3. / 16., + }, + WeightedNeighbor { + dx: -2, + dy: 1, + weight: 1. / 16., + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 2. / 16., + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 3. / 16., + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 2. / 16., + }, + WeightedNeighbor { + dx: 2, + dy: 1, + weight: 1. / 16., + }, +]; + +pub const SIERRA2_KERNEL_INTEGER: [WeightedNeighborInteger; 7] = [ + WeightedNeighborInteger { + dx: 1, + dy: 0, + weight: 4, + }, + WeightedNeighborInteger { + dx: 2, + dy: 0, + weight: 3, + }, + WeightedNeighborInteger { + dx: -2, + dy: 1, + weight: 1, + }, + WeightedNeighborInteger { + dx: -1, + dy: 1, + weight: 2, + }, + WeightedNeighborInteger { + dx: 0, + dy: 1, + weight: 3, + }, + WeightedNeighborInteger { + dx: 1, + dy: 1, + weight: 2, + }, + WeightedNeighborInteger { + dx: 2, + dy: 1, + weight: 1, + }, +]; +pub const SIERRA2_KERNEL_BIT_SHIFT: u32 = 4; + +/** Sierra Lite kernel +* X Represents the current pixel. Also known as Sierra2-4A. +* +* [ ] [ X ] [ 2 ] +* [ 1 ] [ 1 ] [ ] +* +* Weight multiplicator: 1.0 / 4.0 +*/ +pub const SIERRA_LITE_KERNEL: [WeightedNeighbor; 3] = [ + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 2. / 4., + }, + WeightedNeighbor { + dx: -1, + dy: 1, + weight: 1. / 4., + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 1. / 4., + }, +]; + +pub const SIERRA_LITE_KERNEL_INTEGER: [WeightedNeighborInteger; 3] = [ + WeightedNeighborInteger { + dx: 1, + dy: 0, + weight: 2, + }, + WeightedNeighborInteger { + dx: -1, + dy: 1, + weight: 1, + }, + WeightedNeighborInteger { + dx: 0, + dy: 1, + weight: 1, + }, +]; +pub const SIERRA_LITE_KERNEL_BIT_SHIFT: u32 = 2; + +/** +* Prewitt kernel. +* X Represent the current pixel. +* NOTE: Completly hallucinated. +* +* [ 0 0 0 ] [ 0.125 ] [ 0.125 ] +* [ 0 0 0 ] [ 0.00 ] [ 0.00 ] +* [ 0 1 0 ] [ 0.125 ] +* +*/ +pub const PREWITT_KERNEL: [WeightedNeighbor; 4] = [ + WeightedNeighbor { + // BUG: Useless + dx: 0, + dy: 0, + weight: 0.125, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 0.125, + }, + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 0.125, + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 0.125, + }, +]; + +/** +* Roberts kernel. +* X Represent the current pixel. +* Completly hallucinated. +* +* [ 0 0 0 ] [ 0.25 ] [ 0.25 ] +* [ 0 0 0 ] [ 0.00 ] [ 0.00 ] +* [ 0 1 0 ] [ 0.2258 ] [ 0.2258 ] +*/ +pub const ROBERTS_KERNEL: [WeightedNeighbor; 4] = [ + WeightedNeighbor { + // BUG: Useless + dx: 0, + dy: 0, + weight: 0.25, + }, + WeightedNeighbor { + dx: 0, + dy: 1, + weight: 0.25, + }, + WeightedNeighbor { + dx: 1, + dy: 0, + weight: 0.25, + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 0.25, + }, +]; + +/** +* Sobel kernel. + X Represent the current pixel. + Completly allucinated. + + [ -1 -2 -1 ] [ X ] [ 0.25 ] + [ 0 0 0 ] [ 0.50 ] [ 0.00 ] + [ 1 2 1 ] [ 0.25 ] [ 0.25 ] + */ +pub const SOBEL_KERNEL: [WeightedNeighbor; 4] = [ + WeightedNeighbor { + dx: -1, + dy: -1, + weight: -0.25, + }, + WeightedNeighbor { + dx: 0, + dy: -1, + weight: -0.5, + }, + WeightedNeighbor { + dx: 1, + dy: -1, + weight: -0.25, + }, + WeightedNeighbor { + dx: 1, + dy: 1, + weight: 0.25, + }, +]; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a479934 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,750 @@ +pub mod kernel; + +use image::{ImageBuffer, Rgb, RgbImage}; +use palette::{LinSrgb, Srgb, color_difference::EuclideanDistance}; +use rand::rngs::SmallRng; +use rand::{RngCore, SeedableRng}; + +const BLACK_SRGB: Srgb<f32> = Srgb::new(0., 0., 0.); +const WHITE_SRGB: Srgb<f32> = Srgb::new(1., 1., 1.); + +const BLACK_LINEAR_SRGB: LinSrgb<f32> = LinSrgb::new(0., 0., 0.); +const WHITE_LINEAR_SRGB: LinSrgb<f32> = LinSrgb::new(1., 1., 1.); + +#[allow(dead_code)] +fn dither_chose_color_linear_sqrt(diffused_error: f32, pixel: u8) -> (u8, f32) { + let grey_pixel = Srgb::new(pixel, pixel, pixel).into_linear(); + let distance_to_black = BLACK_LINEAR_SRGB.distance(grey_pixel).sqrt() + diffused_error; + let distance_to_white = WHITE_LINEAR_SRGB.distance(grey_pixel).sqrt() + diffused_error; + + if distance_to_black.abs() < distance_to_white.abs() { + (0, -distance_to_black) + } else { + (255, distance_to_white) + } +} + +#[allow(dead_code)] +fn dither_chose_color_linear(diffused_error: f32, pixel: u8) -> (u8, f32) { + let grey_pixel = Srgb::new(pixel, pixel, pixel).into_linear(); + let distance_to_black = BLACK_LINEAR_SRGB.distance(grey_pixel) + diffused_error; + let distance_to_white = WHITE_LINEAR_SRGB.distance(grey_pixel) + diffused_error; + + if distance_to_black.abs() < distance_to_white.abs() { + (0, -distance_to_black) + } else { + (255, distance_to_white) + } +} + +#[allow(dead_code)] +fn dither_chose_color(diffused_error: f32, pixel: u8) -> (u8, f32) { + let grey_pixel: Srgb<f32> = Srgb::new(pixel, pixel, pixel).into_format(); + let distance_to_black = BLACK_SRGB.distance(grey_pixel).sqrt().sqrt() + diffused_error; + let distance_to_white = WHITE_SRGB.distance(grey_pixel).sqrt().sqrt() + diffused_error; + + if distance_to_black.abs() < distance_to_white.abs() { + (0, distance_to_black.abs()) + } else { + (255, distance_to_white.abs()) + } +} + +#[allow(dead_code)] +fn dither_atkinson_chose_color(diffused_error: f32, pixel: f32) -> (u8, f32) { + let distance_to_black = -pixel + diffused_error; + let distance_to_white = (255. - pixel) + diffused_error; + + if distance_to_black.abs() < distance_to_white.abs() { + (0, distance_to_black) + } else { + (255, distance_to_white) + } +} + +#[allow(dead_code)] +fn dither_integer_chose_color(diffused_error: i32, pixel: i32) -> (u8, i32) { + let distance_to_black = -pixel + diffused_error; + let distance_to_white = 255 - pixel + diffused_error; + + if distance_to_black.abs() < distance_to_white.abs() { + (0, distance_to_black) + } else { + (255, distance_to_white) + } +} +fn dither_integer_chose_8colors( + diffused_error_r: i32, + diffused_error_g: i32, + diffused_error_b: i32, + pixel: Rgb<u8>, +) -> (Rgb<u8>, [i32; 3]) { + let Rgb([r, g, b]) = pixel; + let (red, green, blue): (i32, i32, i32) = (r as i32, g as i32, b as i32); + let dist_to_black_r = -red + diffused_error_r; + let dist_to_white_r = 255 - red + diffused_error_r; + + let dist_to_black_g = -green + diffused_error_g; + let dist_to_white_g = 255 - green + diffused_error_g; + + let dist_to_black_b = -blue + diffused_error_b; + let dist_to_white_b = 255 - blue + diffused_error_b; + + let mut ret_diffused_error: [i32; 3] = [0, 0, 0]; + let mut ret_pix = Rgb([255, 0, 0]); + ret_pix.0[0] = if dist_to_black_r.abs() < dist_to_white_r.abs() { + ret_diffused_error[0] = dist_to_black_r; + 0 + } else { + ret_diffused_error[0] = dist_to_white_r; + 255 + }; + ret_pix.0[1] = if dist_to_black_g.abs() < dist_to_white_g.abs() { + ret_diffused_error[1] = dist_to_black_g; + 0 + } else { + ret_diffused_error[1] = dist_to_white_g; + 255 + }; + ret_pix.0[2] = if dist_to_black_b.abs() < dist_to_white_b.abs() { + ret_diffused_error[2] = dist_to_black_b; + 0 + } else { + ret_diffused_error[2] = dist_to_white_b; + 255 + }; + + (ret_pix, ret_diffused_error) +} + +#[allow(dead_code)] +fn l_to_srgb(l: f32) -> f32 { + if l <= 0.0031308 { + l * 12.92 + } else { + f32::powf(l * 0.9478672986 + 0.0521327014, 0.41666) + } +} + +#[allow(dead_code)] +fn dither_float_chose_color(diffused_error: f32, pixel: f32) -> (u8, f32) { + let distance_to_black = -pixel; + let distance_to_white = 255. - pixel + diffused_error; + + if distance_to_black.abs() + diffused_error < distance_to_white.abs() { + (0, distance_to_black) + } else { + (255, distance_to_white) + } +} +#[inline] +fn threshold_1b(pix: u8) -> u8 { + if pix > 128 { 255 } else { 0 } +} + +#[inline] +fn threshold_3b(pix: &mut Rgb<u8>) { + pix.0[0] = threshold_1b(pix.0[0]); + pix.0[1] = threshold_1b(pix.0[1]); + pix.0[2] = threshold_1b(pix.0[2]); +} + +#[allow(dead_code)] +pub fn dither_threshold_1b(image: &mut RgbImage) { + for pixel_ in &mut image.pixels_mut() { + let Rgb([r, g, b]) = pixel_; + // The +1 is to round up to the "nearest" integer. + let pixel = if *r as u32 + *g as u32 + *b as u32 + 1 > 3 * 128 { + 255 + } else { + 0 + }; + *pixel_ = Rgb([pixel, pixel, pixel]); + } +} + +#[allow(dead_code)] +pub fn dither_threshold_3b(image: &mut RgbImage) { + for pixel_ in &mut image.pixels_mut() { + threshold_3b(pixel_); + } +} + +#[allow(dead_code)] +fn dither_float_chose_color_palette(diffused_error: f32, pixel: f32) -> (u8, f32) { + let distance_to_black = -pixel; + let distance_to_white = 255. - pixel + diffused_error; + + if distance_to_black.abs() + diffused_error < distance_to_white.abs() { + (0, distance_to_black) + } else { + (255, distance_to_white) + } +} +pub fn dither_random(image: &mut RgbImage) { + let mut rng = SmallRng::seed_from_u64(1); + let mut rand_number = rng.next_u64(); + + for pixel_ in &mut image.pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let pixel = (*r as u32 + *g as u32 + *b as u32 + 1) as u32 / 3; // The +1 is to round up to the "nearest" integer. + *pixel_ = if pixel < (rand_number & 0xFF) as u32 { + image::Rgb([0, 0, 0]) + } else { + image::Rgb([255, 255, 255]) + }; + + // Quick and dirty optimisation to not call next_u64 on every single pixel. + rand_number >>= 8; + if rand_number == 0 { + rand_number = rng.next_u64(); + } + } +} +pub fn dither_random_color(image: &mut RgbImage) { + let mut rng = SmallRng::seed_from_u64(1); + + for pixel_ in &mut image.pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let rand_number = rng.next_u64(); + *r = if *r < (rand_number & 0xFF) as u8 { + 0 + } else { + 255 + }; + *g = if *g < (rand_number & 0xFF00 >> 8) as u8 { + 0 + } else { + 255 + }; + *b = if *b < ((rand_number & 0xFF0000 >> 16) as u8) { + 0 + } else { + 255 + }; + } +} + +pub fn dither_kong_kui(image: &mut RgbImage) { + let (width, _height) = image.dimensions(); + let error_buffer_size = width as usize; + let mut error: Vec<i32> = vec![0; error_buffer_size]; + + for (x, _y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + + let err_pos = x as usize; + let sum = (*r as i32 + *g as i32 + *b as i32 + 1) / 3; + let pix: i32 = sum + error[err_pos]; + + let (err, pixel) = if pix > 127 { + ((pix - 255) >> 1, 255) + } else { + (pix >> 1, 0) + }; + assert!(pixel == 0 || pixel == 255); + + error[err_pos] = err; + error[(err_pos + 1) % error_buffer_size] += err; + + *pixel_ = Rgb([pixel, pixel, pixel]); + } +} + +pub fn dither_shift(image: &mut RgbImage, kernel: kernel::KernelShift) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let mut error: Vec<i32> = vec![0; error_buffer_size]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let pixel = (*r as i32 + *g as i32 + *b as i32 + 1) as i32 / 3; // The +1 is to round up to the "nearest" integer. + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + + let (pixel, err) = dither_integer_chose_color(error[err_pos as usize], pixel); + + error[err_pos as usize] = 0; + for e in &kernel.kernel { + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize] += (err * e.weight) >> kernel.bit_shift; + } + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} +pub fn dither_shift_8colors(image: &mut RgbImage, kernel: kernel::KernelShift) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let (mut error_r, mut error_g, mut error_b): (Vec<i32>, Vec<i32>, Vec<i32>) = ( + vec![0; error_buffer_size], + vec![0; error_buffer_size], + vec![0; error_buffer_size], + ); + + for (x, y, pixel) in &mut image.enumerate_pixels_mut() { + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + + let (p, err) = dither_integer_chose_8colors( + error_r[err_pos as usize], + error_g[err_pos as usize], + error_b[err_pos as usize], + *pixel, + ); + + for e in &kernel.kernel { + let e_pos = (err_pos as i32 + e.dx + width as i32 * e.dy) + .rem_euclid(error_buffer_size as i32) as usize; + error_r[e_pos] += (err[0] * e.weight) >> kernel.bit_shift; + error_g[e_pos] += (err[1] * e.weight) >> kernel.bit_shift; + error_b[e_pos] += (err[2] * e.weight) >> kernel.bit_shift; + } + error_r[err_pos as usize] = 0; + error_g[err_pos as usize] = 0; + error_b[err_pos as usize] = 0; + + *pixel = p; + } +} + +pub fn dither_ref(image: &mut RgbImage, kernel: &kernel::Kernel) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let mut error: Vec<f32> = vec![0.; error_buffer_size]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let pixel = (*r as u32 + *g as u32 + *b as u32 + 1) as i32 / 3; // The +1 is to round up to the "nearest" integer. + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + + let (pixel, err) = dither_float_chose_color(error[err_pos as usize], pixel as f32); + + for e in kernel { + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize] += err as f32 * e.weight; + } + error[err_pos as usize] = 0.; + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} +pub fn dither_linear_3b(image: &mut RgbImage, kernel: kernel::Kernel) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let mut error: Vec<f32> = vec![0.; error_buffer_size * 3]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + for i in [0, 1, 2].iter() { + let c = pixel_.0[*i]; + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + let err_biais = (*i as u32) * (error_buffer_size as u32); + + let linear: LinSrgb<f32> = + Srgb::new(c as f32 / 255., c as f32 / 255., c as f32 / 255.).into_linear(); + + let (pixel, err) = dither_atkinson_linear_chose_color_sqrt( + error[err_pos as usize + err_biais as usize], + &linear, + ); + + error[err_pos as usize + err_biais as usize] = 0.; + for e in &kernel { + error[(err_pos as i32 + e.dx + width as i32 * e.dy) + .rem_euclid(error_buffer_size as i32) as usize + + err_biais as usize] += err as f32 * e.weight; + } + pixel_.0[*i] = pixel; + } + } +} +pub fn dither_linear(image: &mut RgbImage, kernel: kernel::Kernel) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let mut error: Vec<f32> = vec![0.; error_buffer_size]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let pixel = (*r as u32 + *g as u32 + *b as u32 + 1) as i32 / 3; // The +1 is to round up to the "nearest" integer. + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + + let linear: LinSrgb<f32> = Srgb::new( + pixel as f32 / 255., + pixel as f32 / 255., + pixel as f32 / 255., + ) + .into_linear(); + + let (pixel, err) = + dither_atkinson_linear_chose_color_sqrt(error[err_pos as usize], &linear); + + for e in &kernel { + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize] += err as f32 * e.weight; + } + error[err_pos as usize] = 0.; + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} +pub fn dither(image: &mut RgbImage, kernel: kernel::Kernel) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let mut error: Vec<f32> = vec![0.; error_buffer_size]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let pixel = (*r as u32 + *g as u32 + *b as u32 + 1) as i32 / 3; // The +1 is to round up to the "nearest" integer. + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + + let (pixel, err) = dither_float_chose_color(error[err_pos as usize], pixel as f32); + + for e in &kernel { + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize] += err as f32 * e.weight; + } + error[err_pos as usize] = 0.; + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} +pub fn dither_float(image: &mut RgbImage, kernel: kernel::Kernel) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let mut error: Vec<f32> = vec![0.; error_buffer_size]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let pixel = (*r as f32 + *g as f32 + *b as f32) as f32 / 3.; + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + + let (pixel, err) = dither_float_chose_color(error[err_pos as usize], pixel); + + for e in &kernel { + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize] += err * e.weight; + } + error[err_pos as usize] = 0.; + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} + +pub type DitherFn = for<'a> fn(&'a mut ImageBuffer<image::Rgb<u8>, Vec<u8>>); + +#[allow(dead_code)] +pub fn dither_atkinson_chose_color_base(diffused_error: f32, pixel: f32) -> (u8, f32) { + if (pixel + diffused_error) > 127. { + (255, (pixel + diffused_error - 255.)) + } else { + (0, (pixel + diffused_error)) + } +} + +pub fn dither_atkinson(image: &mut RgbImage) { + let (width, _height) = image.dimensions(); + let mut error: Vec<f32> = vec![0.; (2 * width) as usize]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let pixel = (*r as u32 + *g as u32 + *b as u32) as f32 / 3.; + let err_pos: u32 = (y * width + x) % (2 * width); + + let (pixel, err) = dither_atkinson_chose_color(error[err_pos as usize], pixel); + + error[err_pos as usize] = 0.; + error[((err_pos + 1) % (2 * width)) as usize] += err / 8.; + error[((err_pos + 2) % (2 * width)) as usize] += err / 8.; + error[((err_pos + width + 1) % (2 * width)) as usize] += err / 8.; + error[((err_pos + width - 1) % (2 * width)) as usize] += err / 8.; + error[((err_pos + width) % (2 * width)) as usize] += err / 8.; + error[((err_pos + 2 * width) % (2 * width)) as usize] += err / 8.; + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} + +pub fn dither_atkinson_base(image: &mut RgbImage) { + let (width, _height) = image.dimensions(); + let mut error: Vec<i32> = vec![0; (2 * width) as usize]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let pixel = (*r as u32 + *g as u32 + *b as u32 + 1) as i32 / 3; + let err_pos: u32 = (y * width + x) % (2 * width); + + let (pixel, err) = if (pixel + error[err_pos as usize]) > 128 { + (255, (pixel + error[err_pos as usize] - 255) >> 3) + } else { + (0, (pixel + error[err_pos as usize]) >> 3) + }; + + error[err_pos as usize] = 0; + error[((err_pos + 1) % (2 * width)) as usize] += err; + error[((err_pos + 2) % (2 * width)) as usize] += err; + error[((err_pos + width + 1) % (2 * width)) as usize] += err; + error[((err_pos + width - 1) % (2 * width)) as usize] += err; + error[((err_pos + width) % (2 * width)) as usize] += err; + error[((err_pos + 2 * width) % (2 * width)) as usize] += err; + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} + +pub fn dither_atkinson_linear_chose_color(diffused_error: f32, pixel: &LinSrgb<f32>) -> (u8, f32) { + let black: LinSrgb<f32> = Srgb::new(0u8, 0u8, 0u8).into_linear(); + let white: LinSrgb<f32> = Srgb::new(255, 255, 255).into_linear(); + + let error_black = diffused_error - black.distance(*pixel); + let error_white = diffused_error + white.distance(*pixel); + + if error_black.abs() < error_white.abs() { + (0, error_black) + } else { + (255, error_white) + } +} + +pub fn dither_atkinson_linear_chose_color_sqrt( + diffused_error: f32, + pixel: &LinSrgb<f32>, +) -> (u8, f32) { + let black: LinSrgb<f32> = Srgb::new(0u8, 0u8, 0u8).into_linear(); + let white: LinSrgb<f32> = Srgb::new(255, 255, 255).into_linear(); + + let error_black = diffused_error - black.distance(*pixel).sqrt(); + let error_white = diffused_error + white.distance(*pixel).sqrt(); + + if error_black.abs() < error_white.abs() { + (0, error_black) + } else { + (255, error_white) + } +} + +pub fn dither_atkinson_linear(image: &mut RgbImage) { + let (width, _height) = image.dimensions(); + let mut error: Vec<f32> = vec![0.; (3 * width) as usize]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let linear: LinSrgb<f32> = Srgb::new(*r, *g, *b).into_linear(); + let err_pos: u32 = (y * width + x) % (3 * width); + + let (pixel, err) = dither_atkinson_linear_chose_color(error[err_pos as usize], &linear); + + error[err_pos as usize] = 0.; + error[((err_pos + 1) % (3 * width)) as usize] += err / 8.; + error[((err_pos + 2) % (3 * width)) as usize] += err / 8.; + error[((err_pos + width + 1) % (3 * width)) as usize] += err / 8.; + error[((err_pos + width - 1) % (3 * width)) as usize] += err / 8.; + error[((err_pos + width) % (3 * width)) as usize] += err / 8.; + error[((err_pos + 2 * width) % (3 * width)) as usize] += err / 8.; + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} + +pub fn dither_atkinson_linear_sqrt(image: &mut RgbImage) { + let (width, _height) = image.dimensions(); + let mut error: Vec<f32> = vec![0.; (3 * width) as usize]; + + for (x, y, pixel_) in &mut image.enumerate_pixels_mut() { + let Rgb([r, g, b]) = pixel_; + let linear: LinSrgb<f32> = Srgb::new(*r, *g, *b).into_linear(); + let err_pos: u32 = (y * width + x) % (3 * width); + + let (pixel, err) = + dither_atkinson_linear_chose_color_sqrt(error[err_pos as usize], &linear); + + error[err_pos as usize] = 0.; + error[((err_pos + 1) % (3 * width)) as usize] += err / 8.; + error[((err_pos + 2) % (3 * width)) as usize] += err / 8.; + error[((err_pos + width + 1) % (3 * width)) as usize] += err / 8.; + error[((err_pos + width - 1) % (3 * width)) as usize] += err / 8.; + error[((err_pos + width) % (3 * width)) as usize] += err / 8.; + error[((err_pos + 2 * width) % (3 * width)) as usize] += err / 8.; + + *pixel_ = image::Rgb([pixel, pixel, pixel]); + } +} + +#[allow(dead_code)] +fn chose_color_palette( + diffused_error: &[f32; 3], + pixel: Rgb<u8>, + palette: &[Rgb<u8>], +) -> (Rgb<u8>, [f32; 3]) { + let mut dist_palette: Vec<[f32; 3]> = Vec::with_capacity(palette.len()); + + for color in palette { + dist_palette.push([ + color.0[0] as f32 - pixel.0[0] as f32 + diffused_error[0], + color.0[1] as f32 - pixel.0[1] as f32 + diffused_error[1], + (color.0[2] as f32) - pixel.0[2] as f32 + diffused_error[2], + ]); + } + + let index_of_min: Option<usize> = dist_palette + .iter() + .enumerate() + .min_by(|(_, x), (_, y)| { + (x[0].abs() + x[1].abs() + x[2].abs()) + .total_cmp(&(y[0].abs() + y[1].abs() + y[2].abs()).abs()) + }) + .map(|(index, _)| index); + + let i = index_of_min.unwrap() as usize; + + (palette[i], dist_palette[i]) +} +pub fn dither_palette(image: &mut RgbImage, palette: &[Rgb<u8>], kernel: kernel::Kernel) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let mut error: Vec<[f32; 3]> = vec![[0.; 3]; error_buffer_size]; + + for (x, y, pixel) in &mut image.enumerate_pixels_mut() { + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + let (pixel_nearest, err) = chose_color_palette(&error[err_pos as usize], *pixel, palette); + + for e in &kernel { + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize][0] += err[0] as f32 * e.weight; + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize][1] += err[1] as f32 * e.weight; + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize][2] += err[2] as f32 * e.weight; + } + error[err_pos as usize] = [0., 0., 0.]; + + *pixel = pixel_nearest; + } +} + +fn l_rgb(p: f32) -> f32 { + if p < 0.04045 { + p * 0.0773993808 + } else { + f32::powf(p * 0.9478672986 + 0.0521327014, 2.4) + } +} +#[allow(dead_code)] +fn srgb(l: f32) -> f32 { + if l < 0.0031308 { + l * 12.92 + } else { + 1.055 * f32::powf(l, 0.41666) - 0.055 + } +} + +#[allow(dead_code)] +fn chose_color_palette_linear( + diffused_error: &[f32; 3], + pixel: Rgb<u8>, + palette: &[Rgb<u8>], +) -> (Rgb<u8>, [f32; 3]) { + let mut dist_palette: Vec<[f32; 3]> = Vec::with_capacity(palette.len()); + + // let distance_to_black = BLACK_LINEAR_SRGB.distance(grey_pixel).sqrt() + diffused_error; + //Linear = + // if (sRGB < 0.04045) sRGB * 0.0773993808 + // else (pow(sRGB * 0.9478672986 + 0.0521327014, 2.4)) + + for color in palette { + dist_palette.push([ + l_rgb(color.0[0] as f32) - l_rgb(pixel.0[0] as f32) + diffused_error[0], + l_rgb(color.0[1] as f32) - l_rgb(pixel.0[1] as f32) + diffused_error[1], + l_rgb(color.0[2] as f32) - l_rgb(pixel.0[2] as f32) + diffused_error[2], + ]); + } + + let index_of_min: Option<usize> = dist_palette + .iter() + .enumerate() + .min_by(|(_, x), (_, y)| { + (x[0].abs() + x[1].abs() + x[2].abs()) + .total_cmp(&(y[0].abs() + y[1].abs() + y[2].abs()).abs()) + }) + .map(|(index, _)| index); + + let i = index_of_min.unwrap() as usize; + + (palette[i], dist_palette[i]) +} +pub fn dither_palette_linear(image: &mut RgbImage, palette: &[Rgb<u8>], kernel: kernel::Kernel) { + let (width, _height) = image.dimensions(); + let width = width as usize; + let error_buffer_size = kernel + .iter() + .map(|k| k.dx + k.dy * width as i32) + .max() + .unwrap() as usize; + assert!(error_buffer_size > 0); + let mut error: Vec<[f32; 3]> = vec![[0.; 3]; error_buffer_size]; + + for (x, y, pixel) in &mut image.enumerate_pixels_mut() { + let err_pos: u32 = (y * width as u32 + x) % (error_buffer_size as u32); + let (pixel_nearest, err) = + chose_color_palette_linear(&error[err_pos as usize], *pixel, palette); + + for e in &kernel { + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize][0] += err[0] as f32 * e.weight; + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize][1] += err[1] as f32 * e.weight; + error[(err_pos as i32 + e.dx + width as i32 * e.dy).rem_euclid(error_buffer_size as i32) + as usize][2] += err[2] as f32 * e.weight; + } + error[err_pos as usize] = [0., 0., 0.]; + + *pixel = pixel_nearest; + } +} diff --git a/src/main.rs b/src/main.rs index 2595f78..e1ee198 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,80 +1,117 @@ // Implement a simple dithering algorithm -use image::{DynamicImage, ImageReader, Rgb, RgbImage}; -use std::env; -use std::io::Cursor; +use image::{ImageReader, Rgb}; +use std::path::Path; -fn dither_atkinson(image: &mut RgbImage) { - let (width, height) = image.dimensions(); - let mut error: Vec<f32> = vec![0.; (3 * width) as usize]; - - for (x, y, mut pixel_) in &mut image.enumerate_pixels_mut() { - let Rgb([r, g, b]) = pixel_; - let pixel = (*r as u32 + *g as u32 + *b as u32) as f32 / 3.; - let err_pos: u32 = (y * width + x) % (3 * width); - - let (pixel, err) = if (pixel + error[err_pos as usize]) > 127. { - (255, (pixel + error[err_pos as usize] - 255.) / 8.) - } else { - (0, (pixel + error[err_pos as usize]) / 8.) - }; - - error[err_pos as usize] = 0.; - error[((err_pos + 1) % (3 * width)) as usize] += err; - error[((err_pos + 2) % (3 * width)) as usize] += err; - error[((err_pos + width + 1) % (3 * width)) as usize] += err; - error[((err_pos + width - 1) % (3 * width)) as usize] += err; - error[((err_pos + width) % (3 * width)) as usize] += err; - error[((err_pos + 2 * width) % (3 * width)) as usize] += err; - - *pixel_ = image::Rgb([pixel, pixel, pixel]); - } -} - -fn dither_sierra(image: &mut RgbImage) { - let (width, height) = image.dimensions(); - let mut error: Vec<f32> = vec![0.; (3 * width) as usize]; - - for (x, y, mut pixel_) in &mut image.enumerate_pixels_mut() { - let Rgb([r, g, b]) = pixel_; - let pixel = (*r as u32 + *g as u32 + *b as u32) as f32 / 3.; - let err_pos: u32 = (y * width + x) % (3 * width); - - let (pixel, err) = if (pixel + error[err_pos as usize]) > 127. { - (255, (pixel + error[err_pos as usize] - 255.) / 32.) - } else { - (0, (pixel + error[err_pos as usize]) / 32.) - }; - - error[err_pos as usize] = 0.; - error[((err_pos + 1) % (3 * width)) as usize] += 5. * err; - error[((err_pos + 2) % (3 * width)) as usize] += 3. * err; - error[((err_pos + width + 2) % (3 * width)) as usize] += 2. * err; - error[((err_pos + width + 1) % (3 * width)) as usize] += 4. * err; - error[((err_pos + width) % (3 * width)) as usize] += 5. * err; - error[((err_pos + width - 1) % (3 * width)) as usize] += 4. * err; - error[((err_pos + width - 2) % (3 * width)) as usize] += 2. * err; - error[((err_pos + 2 * width + 1) % (3 * width)) as usize] += 2. * err; - error[((err_pos + 2 * width) % (3 * width)) as usize] += 3. * err; - error[((err_pos + 2 * width - 1) % (3 * width)) as usize] += 2. * err; - - *pixel_ = image::Rgb([pixel, pixel, pixel]); - } -} +const LIST_OF_SAMPLES: [&str; 7] = [ + "samples/david.png", + "samples/david2.jpg", + "samples/us.png", + "samples/jane.jpg", + "samples/jane2.webp", + "samples/karel_käos.jpg", + "samples/diderot_111.png", +]; +const PALETTE_3B: [Rgb<u8>; 8] = [ + Rgb([0, 0, 255]), + Rgb([0, 255, 255]), + Rgb([255, 0, 255]), + Rgb([255, 255, 255]), + Rgb([0, 0, 0]), + Rgb([0, 255, 0]), + Rgb([255, 0, 0]), + Rgb([255, 255, 0]), +]; fn main() -> Result<(), image::ImageError> { + /* let args: Vec<String> = env::args().collect(); - let image_path = format!("{}.png", &args[1]); - let image_out = format!("{}_dithered_atkinso.jpg", &args[1]); - let image_out_sierra = format!("{}_dithered_sierra.jpg", &args[1]); + */ - let mut img = ImageReader::open(&image_path)?.decode()?.into_rgb8(); - dither_atkinson(&mut img); - img.save(image_out)?; + let algos: Vec<(&str, dither::DitherFn)> = vec![ + // ("dither_kong_kui_1b", dither::dither_kong_kui), + // ("dither_atkinson_linear", dither::dither_atkinson_linear), + // ("kernel_atkinson_linear", |img| { + // dither::dither_linear(img, dither::kernel::ATKINSON_KERNEL.to_vec()) + // }), + ("kernel_atkinson_palette_3b", |img| { + dither::dither_palette(img, &PALETTE_3B, dither::kernel::ATKINSON_KERNEL.to_vec()) + }), + ("kernel_jarvis_judice_ninke_linear_3b", |img| { + dither::dither_palette_linear( + img, + &PALETTE_3B, + dither::kernel::JARVIS_JUDICE_NINKE_KERNEL.to_vec(), + ) + }), + /* + ("kernel_kong_kui_3b", |img| { + dither::dither_shift_8colors( + img, + dither::kernel::KernelShift { + kernel: dither::kernel::KONG_KUI_KERNEL_INTEGER.to_vec(), + bit_shift: dither::kernel::KONG_KUI_KERNEL_BIT_SHIFT, + }, + ) + }), + ("random_1b", dither::dither_random), + ("random_3b", dither::dither_random_color), + ("threshold_1b", dither::dither_threshold_1b), + ("threshold_3b", dither::dither_threshold_3b), + ("burkes_3b", |img| { + dither::dither_shift_8colors( + img, + dither::kernel::KernelShift { + kernel: dither::kernel::BURKES_KERNEL_INTEGER.to_vec(), + bit_shift: dither::kernel::BURKES_KERNEL_BIT_SHIFT, + }, + ) + }),*/ + ]; - let mut img = ImageReader::open(&image_path)?.decode()?.into_rgb8(); - dither_sierra(&mut img); - img.save(image_out_sierra)?; + for sample in LIST_OF_SAMPLES { + for (suffix, algo) in &algos { + println!("{} on {}", suffix, sample); + let path = Path::new(sample); + let mut img = ImageReader::open(&sample)?.decode()?.into_rgb8(); + algo(&mut img); + let path_out = format!( + "{}/{}_dithered_{}.png", + path.parent().unwrap().to_str().unwrap(), + path.file_stem().unwrap().to_str().unwrap(), + suffix + ); + img.save(&path_out)?; + } + } Ok(()) } + +// https://github.com/blenderskool/pigmnts/tree/master +// https://github.com/cyotek/Dithering/blob/master/resources/DITHER.TXT +// https://github.com/cyotek/Dithering/blob/master/resources/DHALF.TXT +// https://cv.ulichney.com/papers/1993-void-cluster.pdf +// https://en.wikipedia.org/wiki/Ordered_dithering +// https://www.sciencedirect.com/science/article/abs/pii/014193829190005X +// https://www.google.fr/search?q=Ostromoukhov+dithering+matrix&sca_esv=cc91aa7b516a412e&ei=xxgIaKe5LKLekPIP876PuQY&ved=0ahUKEwjnuPbg2OyMAxUiL0QIHXPfI2cQ4dUDCBI&uact=5&oq=Ostromoukhov+dithering+matrix&gs_lp=Egxnd3Mtd2l6LXNlcnAiHU9zdHJvbW91a2hvdiBkaXRoZXJpbmcgbWF0cml4MgUQABjvBTIFEAAY7wUyBRAAGO8FMgUQABjvBTIFEAAY7wVIrMEBUN-rAVj-vgFwAngAkAEAmAH6AqABwQ2qAQUyLTYuMbgBA8gBAPgBAZgCCaAC8g3CAgcQABiwAxgewgILEAAYgAQYsAMYogTCAggQABiwAxjvBcICBhAAGBYYHsICCBAAGIAEGKIEwgIFECEYoAHCAgcQIRigARgKmAMAiAYBkAYGkgcHMi4wLjYuMaAHlBGyBwUyLTYuMbgH5g0&sclient=gws-wiz-serp +// +// https://perso.liris.cnrs.fr/ostrom/publications/pdf/SIGGRAPH99_MultiColorDithering_600dpi.pdf +// https://patents.google.com/patent/EP0828379A2/en +// https://www.iro.umontreal.ca/~ostrom/publications/pdf/SIGGRAPH01_varcoeffED.pdf +// https://observablehq.com/@jobleonard/variable-coefficient-dithering +// https://perso.liris.cnrs.fr/victor.ostromoukhov/publications/pdf/SIGGRAPH94_RotatedDither.pdf +// https://perso.liris.cnrs.fr/victor.ostromoukhov/publications/pdf/SIGGRAPH99_MultiColorDithering_600dpi.pdf +// https://perso.liris.cnrs.fr/victor.ostromoukhov/publications/SIGAsisa2024_Best_paper_award.jpg +// https://bisqwit.iki.fi/story/howto/dither/jy/ +// https://github.com/samhocevar/scolorq/blob/master/spatial_color_quant.cpp +// https://github.com/samhocevar/scolorq +// https://patents.google.com/patent/EP0828379A2/en +// https://lib.rs/crates/rscolorq +// https://github.com/okaneco/rscolorq +// https://people.eecs.berkeley.edu/~dcoetzee/downloads/scolorq/ +// https://web.archive.org/web/20210625055243/https://people.eecs.berkeley.edu/~dcoetzee/downloads/scolorq/#contact +// https://newsgroup.xnview.com/viewtopic.php?t=16591 +// https://bisqwit.iki.fi/jutut/colorquant/ +// https://github.com/tromero/BayerMatrix +// https://docs.gimp.org/2.10/en/gimp-filter-bayer-matrix.html diff --git a/src/test_all.rs b/src/test_all.rs new file mode 100644 index 0000000..0d96ede --- /dev/null +++ b/src/test_all.rs @@ -0,0 +1,147 @@ +// Implement a simple dithering algorithm +use image::ImageReader; +use std::path::Path; + +const LIST_OF_SAMPLES: [&str; 7] = [ + "samples/david.png", + "samples/david2.jpg", + "samples/us.png", + "samples/diderot_111.png", + "samples/jane.jpg", + "samples/jane2.webp", + "samples/karel_käos.jpg", +]; + +fn main() -> Result<(), image::ImageError> { + /* + let args: Vec<String> = env::args().collect(); + let image_path = format!("{}.png", &args[1]); + */ + + let algos: Vec<(&str, dither::DitherFn)> = vec![ + ("atkinson", dither::dither_atkinson), + ("atkinson_base", dither::dither_atkinson_base), + ("atkinson_linear", dither::dither_atkinson_linear), + ("atkinson_linear_sqrt", dither::dither_atkinson_linear_sqrt), + ("kernel_atkinson", |img| { + dither::dither(img, dither::kernel::ATKINSON_KERNEL.to_vec()) + }), + ("kernel_atkinson_integer", |img| { + dither::dither_shift( + img, + dither::kernel::KernelShift { + kernel: dither::kernel::ATKINSON_KERNEL_INTEGER.to_vec(), + bit_shift: dither::kernel::ATKINSON_KERNEL_BIT_SHIFT, + }, + ) + }), + ("kernel_floyd_steinberg", |img| { + dither::dither(img, dither::kernel::FLOYD_STEINBERG_KERNEL.to_vec()) + }), + ("kernel_floyd_steinberg_integer", |img| { + dither::dither_shift( + img, + dither::kernel::KernelShift { + kernel: dither::kernel::FLOYD_STEINBERG_KERNEL_INTEGER.to_vec(), + bit_shift: dither::kernel::FLOYD_STEINBERG_KERNEL_BIT_SHIFT, + }, + ) + }), + ("kernel_fedoseev", |img| { + dither::dither(img, dither::kernel::FEDOSEEV_KERNEL.to_vec()) + }), + ("kernel_fedoseev2", |img| { + dither::dither(img, dither::kernel::FEDOSEEV2_KERNEL.to_vec()) + }), + ("kernel_fedoseev3", |img| { + dither::dither(img, dither::kernel::FEDOSEEV3_KERNEL.to_vec()) + }), + ("kernel_fedoseev4", |img| { + dither::dither(img, dither::kernel::FEDOSEEV4_KERNEL.to_vec()) + }), + ("kernel_wong_allebach", |img| { + dither::dither(img, dither::kernel::WONG_ALLEBACH_KERNEL.to_vec()) + }), + ("kernel_jarvis_judice_ninke", |img| { + dither::dither(img, dither::kernel::JARVIS_JUDICE_NINKE_KERNEL.to_vec()) + }), + ("kernel_stucki", |img| { + dither::dither(img, dither::kernel::STUCKI_KERNEL.to_vec()) + }), + ("kernel_burkes", |img| { + dither::dither(img, dither::kernel::BURKES_KERNEL.to_vec()) + }), + ("kernel_burkes_integer", |img| { + dither::dither_shift( + img, + dither::kernel::KernelShift { + kernel: dither::kernel::BURKES_KERNEL_INTEGER.to_vec(), + bit_shift: dither::kernel::BURKES_KERNEL_BIT_SHIFT, + }, + ) + }), + ("kernel_sierra3", |img| { + dither::dither(img, dither::kernel::SIERRA3_KERNEL.to_vec()) + }), + ("kernel_sierra3_integer", |img| { + dither::dither_shift( + img, + dither::kernel::KernelShift { + kernel: dither::kernel::SIERRA3_KERNEL_INTEGER.to_vec(), + bit_shift: dither::kernel::SIERRA3_KERNEL_BIT_SHIFT, + }, + ) + }), + ("kernel_sierra2", |img| { + dither::dither(img, dither::kernel::SIERRA2_KERNEL.to_vec()) + }), + ("kernel_sierra2_integer", |img| { + dither::dither_shift( + img, + dither::kernel::KernelShift { + kernel: dither::kernel::SIERRA2_KERNEL_INTEGER.to_vec(), + bit_shift: dither::kernel::SIERRA2_KERNEL_BIT_SHIFT, + }, + ) + }), + ("kernel_sierra-lite", |img| { + dither::dither(img, dither::kernel::SIERRA_LITE_KERNEL.to_vec()) + }), + ("kernel_sierra-line_integer", |img| { + dither::dither_shift( + img, + dither::kernel::KernelShift { + kernel: dither::kernel::SIERRA_LITE_KERNEL_INTEGER.to_vec(), + bit_shift: dither::kernel::SIERRA_LITE_KERNEL_BIT_SHIFT, + }, + ) + }), + ("kernel_hallucination_prewitt", |img| { + dither::dither(img, dither::kernel::PREWITT_KERNEL.to_vec()) + }), + ("kernel_hallucination_roberts", |img| { + dither::dither(img, dither::kernel::ROBERTS_KERNEL.to_vec()) + }), + ("kernel_hallucination_sobel", |img| { + dither::dither(img, dither::kernel::SOBEL_KERNEL.to_vec()) + }), + ]; + + for sample in LIST_OF_SAMPLES { + for (suffix, algo) in &algos { + println!("{} on {}", suffix, sample); + let path = Path::new(sample); + let mut img = ImageReader::open(&sample)?.decode()?.into_rgb8(); + algo(&mut img); + let path_out = format!( + "{}/{}_dithered_{}.png", + path.parent().unwrap().to_str().unwrap(), + path.file_stem().unwrap().to_str().unwrap(), + suffix + ); + img.save(&path_out)?; + } + } + + Ok(()) +} |