summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorache <ache@ache.one>2025-04-23 17:12:17 +0200
committerache <ache@ache.one>2025-04-23 17:12:17 +0200
commitd9630d3342ebdbf4eb952a6090e68021de2acd3d (patch)
tree9b8518d0fd467fe9cdbca6f73d731f7b45843c55
parentImplement sierra (diff)
Worst atomic commit ever
-rw-r--r--Cargo.toml23
-rw-r--r--benches/all_samples.rs255
-rw-r--r--benches/random.rs44
-rw-r--r--benches/shift_kernels.rs173
-rw-r--r--samples/david.png (renamed from david.png)bin27218 -> 27218 bytes
-rw-r--r--samples/david2.jpgbin0 -> 86914 bytes
-rw-r--r--samples/diderot_111.pngbin0 -> 4734542 bytes
-rw-r--r--samples/jane.jpgbin0 -> 386231 bytes
-rw-r--r--samples/jane2.webpbin0 -> 316512 bytes
-rw-r--r--samples/karel_käos.jpgbin0 -> 114462 bytes
-rw-r--r--samples/us.pngbin0 -> 361839 bytes
-rw-r--r--src/kernel.rs925
-rw-r--r--src/lib.rs750
-rw-r--r--src/main.rs175
-rw-r--r--src/test_all.rs147
15 files changed, 2423 insertions, 69 deletions
diff --git a/Cargo.toml b/Cargo.toml
index cb0a9de..fe03175 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
index 6cfa884..6cfa884 100644
--- a/david.png
+++ b/samples/david.png
Binary files differ
diff --git a/samples/david2.jpg b/samples/david2.jpg
new file mode 100644
index 0000000..4ae1200
--- /dev/null
+++ b/samples/david2.jpg
Binary files differ
diff --git a/samples/diderot_111.png b/samples/diderot_111.png
new file mode 100644
index 0000000..9197157
--- /dev/null
+++ b/samples/diderot_111.png
Binary files differ
diff --git a/samples/jane.jpg b/samples/jane.jpg
new file mode 100644
index 0000000..3d1ef81
--- /dev/null
+++ b/samples/jane.jpg
Binary files differ
diff --git a/samples/jane2.webp b/samples/jane2.webp
new file mode 100644
index 0000000..09fccb2
--- /dev/null
+++ b/samples/jane2.webp
Binary files differ
diff --git a/samples/karel_käos.jpg b/samples/karel_käos.jpg
new file mode 100644
index 0000000..71cbe92
--- /dev/null
+++ b/samples/karel_käos.jpg
Binary files differ
diff --git a/samples/us.png b/samples/us.png
new file mode 100644
index 0000000..96a090a
--- /dev/null
+++ b/samples/us.png
Binary files differ
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(())
+}