bendn 2025-01-19
parent f369c09 · commit 8aa6365
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock2
-rw-r--r--src/lib.rs28
-rw-r--r--src/main.rs22
-rw-r--r--src/ordered.rs142
-rw-r--r--tests/test.rs43
-rw-r--r--tests/tres/.testing1
7 files changed, 155 insertions, 84 deletions
diff --git a/.gitignore b/.gitignore
index ea8c4bf..cdb3f55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
+tests/tres/* \ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index d25289a..f4c6610 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -82,7 +82,7 @@ checksum = "6a02dba6a60cd31533cf16561ced53239686d18f1464bff49579dd320fcea081"
[[package]]
name = "fimg"
version = "0.4.43"
-source = "git+https://github.com/bend-n/fimg#193a7b4ec395a7e4e102da3cbce11bbebb7bdb85"
+source = "git+https://github.com/bend-n/fimg#f8b7d64f22414e510a6ea622a5d8aa80dd58997c"
dependencies = [
"atools",
"clipline",
diff --git a/src/lib.rs b/src/lib.rs
index fa301fc..0267653 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,6 @@
#![allow(incomplete_features, internal_features)]
#![feature(
+ isqrt,
const_fn_floating_point_arithmetic,
inline_const_pat,
iter_chain,
@@ -25,23 +26,26 @@ pub mod ordered;
mod dumb;
mod kd;
use atools::prelude::*;
-use fimg::Image;
+use fimg::{indexed::IndexedImage, Image};
use kd::KD;
// type KD = kiddo::immutable::float::kdtree::ImmutableKdTree<f32, u64, 4, 32>;
fn map(colors: &[[f32; 4]]) -> KD {
KD::new(colors)
}
-fn dither(
+fn dither<'a>(
image: Image<&[f32], 4>,
- f: impl FnMut(((usize, usize), &[f32; 4])) -> [f32; 4],
-) -> Image<Box<[f32]>, 4> {
- Image::build(image.width(), image.height()).buf(
- image
- .chunked()
- .zip(image.ordered())
- .map(|(p, xy)| (xy.array().map(|x| x as usize).tuple(), p))
- .flat_map(f)
- .collect(),
- )
+ f: impl FnMut(((usize, usize), &[f32; 4])) -> u32,
+ pal: &'a [[f32; 4]],
+) -> IndexedImage<Box<[u32]>, &'a [[f32; 4]]> {
+ IndexedImage::build(image.width(), image.height())
+ .pal(pal)
+ .buf(
+ image
+ .chunked()
+ .zip(image.ordered())
+ .map(|(p, xy)| (xy.array().map(|x| x as usize).tuple(), p))
+ .map(f)
+ .collect(),
+ )
}
diff --git a/src/main.rs b/src/main.rs
index 361f33f..866e38e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -41,21 +41,17 @@ fn reemap() {
*/
// println!("{pal:?}");
- fimg::Image::<Box<[u8]>, 4>::from(
- remapper::ordered::remap_bayer_8x8(
- fimg::Image::<Box<[f32]>, 4>::from(
- // fimg::Image::<&[u8], 4>::make::<256, 256>().as_ref(),
- fimg::Image::<Vec<u8>, 4>::open("../fimg/tdata/cat.png")
- // .show()
- // .scale::<fimg::scale::Nearest>(800, 480)
- // .show()
- .as_ref(),
- )
+ remapper::ordered::bayer32x32(
+ // fimg::Image::<&[u8], 4>::make::<256, 256>().as_ref(),
+ fimg::Image::<Vec<u8>, 4>::open("../fimg/tdata/cat.png")
+ .as_ref()
+ .to_f32()
.as_ref(),
- &pal,
- )
- .as_ref(),
+ &pal,
)
+ .to()
+ .to_u8()
+ .show()
.save("yeee.png");
}
diff --git a/src/ordered.rs b/src/ordered.rs
index 84b7220..bef2945 100644
--- a/src/ordered.rs
+++ b/src/ordered.rs
@@ -1,5 +1,7 @@
//! # Ordered dithering.
//! The way this works is by adding a constant texture to the image, and then quantizing that.
+use fimg::indexed::IndexedImage;
+
use super::*;
const fn threshold<const N: usize>(x: [u32; N]) -> [f32; N] {
@@ -7,75 +9,95 @@ const fn threshold<const N: usize>(x: [u32; N]) -> [f32; N] {
- 0.5 * ((N - 1) as f32 * (1. / N as f32)))
}
-static BAYER_2X2: [f32; 4] = {
- threshold([
- 0, 2, //
- 3, 1,
- ])
-};
-static BAYER_4X4: [f32; 4 * 4] = {
- threshold([
- 0, 8, 2, 10, //
- 12, 4, 14, 6, //
- 3, 11, 1, 9, //
- 15, 7, 13, 5,
- ])
-};
-
-pub const BAYER_8X8: [f32; 8 * 8] = threshold(mattr::transposed::<_, 8, 8>(car::from_fn!(|p| {
- let q = p ^ (p >> 3);
- // https://bisqwit.iki.fi/story/howto/dither/jy/
- #[rustfmt::skip]
- (((p & 4) >> 2) | ((q & 4) >> 1)
- | ((p & 2) << 1) | ((q & 2) << 2)
- | ((p & 1) << 4) | ((q & 1) << 5)) as u32
-})));
-
-fn dither_with<const N: usize>(
- image: Image<&[f32], 4>,
- mut f: impl FnMut(((usize, usize), &[f32; 4])) -> [f32; 4],
-) -> Image<Box<[f32]>, 4> {
- dither(image, |((x, y), p)| f(((x % N, y % N), p)))
+const fn next<const N: usize>(input: [u32; N]) -> [u32; N.isqrt() * 2 * N.isqrt() * 2]
+where
+ [(); N.isqrt() * N.isqrt()]:,
+{
+ next_::<{ N.isqrt() }>(unsafe { std::intrinsics::transmute_unchecked(input) })
}
-pub fn remap_bayer_2x2(image: Image<&[f32], 4>, palette: &[[f32; 4]]) -> Image<Box<[f32]>, 4> {
- let kd = map(palette);
- let r = kd.space(palette);
- dither_with::<2>(image, |((x, y), &p)| {
- let color = p.add(r * BAYER_2X2[x + y * 2]);
- palette[kd.find_nearest(color) as usize]
- })
+// https://github.com/surma/surma.dev/blob/master/static/lab/ditherpunk/bayer-worker.js#L6C1-L23C1
+const fn next_<const N: usize>(input: [u32; N * N]) -> [u32; N * 2 * N * 2] {
+ let mut output = [0; { N * 2 * N * 2 }];
+ let base = [0, 2, 3, 1];
+ let mut y = 0;
+ while y != N * 2 {
+ let mut x = 0;
+ while x != N * 2 {
+ output[y * N * 2 + x] = 4 * input[(y % N) * N + (x % N)]
+ + base[((y >= N) as usize) * 2 + ((x >= N) as usize)];
+ x += 1;
+ }
+ y += 1;
+ }
+ output
}
+const BAYER0: [u32; 4] = [
+ 0, 2, //
+ 3, 1,
+];
+const BAYER1: [u32; 4 * 4] = next(BAYER0);
+const BAYER2: [u32; 8 * 8] = next(BAYER1);
+const BAYER3: [u32; 16 * 16] = next(BAYER2);
+const BAYER4: [u32; 32 * 32] = next(BAYER3);
+const BAYER5: [u32; 64 * 64] = next(BAYER4);
-pub fn remap_bayer_4x4(image: Image<&[f32], 4>, palette: &[[f32; 4]]) -> Image<Box<[f32]>, 4> {
- let kd = map(palette);
- let r = kd.space(palette);
- dither_with::<4>(image, |((x, y), &p)| {
- let color = p.add(r * BAYER_4X4[x + y * 4]);
- palette[kd.find_nearest(color) as usize]
- })
+const BAYER_2X2: [f32; 4] = threshold(BAYER0);
+const BAYER_4X4: [f32; 4 * 4] = threshold(BAYER1);
+const BAYER_8X8: [f32; 8 * 8] = threshold(BAYER2);
+const BAYER_16X16: [f32; 16 * 16] = threshold(BAYER3);
+const BAYER_32X32: [f32; 32 * 32] = threshold(BAYER4);
+const BAYER_64X64: [f32; 64 * 64] = threshold(BAYER5);
+
+fn dither_with<'a, const N: usize>(
+ image: Image<&[f32], 4>,
+ mut f: impl FnMut(((usize, usize), &[f32; 4])) -> u32,
+ palette: &'a [[f32; 4]],
+) -> IndexedImage<Box<[u32]>, &'a [[f32; 4]]> {
+ dither(image, |((x, y), p)| f(((x % N, y % N), p)), palette)
}
-pub fn remap_bayer_8x8(image: Image<&[f32], 4>, palette: &[[f32; 4]]) -> Image<Box<[f32]>, 4> {
- let kd = map(palette);
- let r = kd.space(palette);
- dither_with::<8>(image, |((x, y), &p)| {
- let color = p.add(r * BAYER_8X8[x + y * 8]);
- palette[kd.find_nearest(color) as usize]
- })
+macro_rules! bayer {
+ ($i:ident, $c:ident, $j:literal) => {
+ /// Ordered dithering via a bayer matrix.
+ ///
+ /// Dont expect too much difference from each of them.
+ pub fn $i<'a>(
+ image: Image<&[f32], 4>,
+ palette: &'a [[f32; 4]],
+ ) -> IndexedImage<Box<[u32]>, &'a [[f32; 4]]> {
+ let kd = map(palette);
+ let r = kd.space(palette);
+ dither_with::<$j>(
+ image.into(),
+ |((x, y), &p)| {
+ let color = p.add(r * $c[x + y * $j]);
+ kd.find_nearest(color)
+ },
+ palette,
+ )
+ }
+ };
}
-pub fn remap(image: Image<&[f32], 4>, palette: &[[f32; 4]]) -> Image<Box<[f32]>, 4> {
+bayer!(bayer2x2, BAYER_2X2, 2);
+bayer!(bayer4x4, BAYER_4X4, 4);
+bayer!(bayer8x8, BAYER_8X8, 8);
+bayer!(bayer16x16, BAYER_16X16, 16);
+bayer!(bayer32x32, BAYER_32X32, 32);
+bayer!(bayer64x64, BAYER_64X64, 64);
+
+pub fn remap<'a, 'b>(
+ image: Image<&'b [f32], 4>,
+ palette: &'a [[f32; 4]],
+) -> IndexedImage<Box<[u32]>, &'a [[f32; 4]]> {
let kd = map(palette);
// todo!();
- Image::build(image.width(), image.height()).buf(
- image
- .chunked()
- .flat_map(|x| palette[kd.find_nearest(*x) as usize])
- // .map(|&x| palette.closest(x).1)
- .collect(),
- )
+ IndexedImage::build(image.width(), image.height())
+ .pal(palette)
+ .buf(image.chunked().map(|x| kd.find_nearest(*x)).collect())
}
+
const BLUE: Image<[f32; 1024 * 1024 * 3], 3> = unsafe {
Image::new(
std::num::NonZero::new(1024).unwrap(),
@@ -84,6 +106,7 @@ const BLUE: Image<[f32; 1024 * 1024 * 3], 3> = unsafe {
)
};
// todo: figure this out? seems off.
+/*
pub fn remap_blue(image: Image<&[f32], 4>, palette: &[[f32; 4]]) -> Image<Box<[f32]>, 4> {
let kd = map(palette);
// Image::<Box<[u8]>, 3>::from(BLUE.as_ref()).show();
@@ -130,6 +153,8 @@ pub fn remap_blue(image: Image<&[f32], 4>, palette: &[[f32; 4]]) -> Image<Box<[f
})
}
+
+
pub fn remap_triangular(image: Image<&[f32], 4>, palette: &[[f32; 4]]) -> Image<Box<[f32]>, 4> {
let kd = map(palette);
dither(image, |((x, y), p)| {
@@ -153,3 +178,4 @@ pub fn remap_triangular(image: Image<&[f32], 4>, palette: &[[f32; 4]]) -> Image<
palette[kd.find_nearest(c) as usize]
})
}
+*/
diff --git a/tests/test.rs b/tests/test.rs
new file mode 100644
index 0000000..2d9b67b
--- /dev/null
+++ b/tests/test.rs
@@ -0,0 +1,43 @@
+use fimg::{indexed::IndexedImage, Image};
+
+fn test(
+ k: &'static str,
+ f: for<'a> fn(Image<&[f32], 4>, &'a [[f32; 4]]) -> IndexedImage<Box<[u32]>, &'a [[f32; 4]]>,
+) {
+ let pal = fimg::Image::<Box<[f32]>, 4>::from(fimg::Image::open("tdata/endesga.png").as_ref());
+ let pal = pal.flatten();
+ // let d = f(fimg::Image::open("tdata/small_cat.png").to_f32().as_ref(), &pal).to().to_u8().show();
+ let d = f(
+ fimg::Image::open("tdata/small_cat.png").to_f32().as_ref(),
+ &pal,
+ )
+ .into_raw_parts()
+ .0
+ .take_buffer()
+ .iter()
+ .map(|&x| x as u8)
+ .collect::<Vec<_>>();
+ match std::fs::read(format!("tests/tres/{k}")) {
+ Ok(x) => assert!(x == d, "{k}! failed."),
+ Err(_) => std::fs::write(format!("tests/tres/{k}"), d).unwrap(),
+ }
+}
+macro_rules! test {
+ ($x:ident, $call:path) => {
+ #[test]
+ fn $x() {
+ test(stringify!($x), $call);
+ }
+ };
+}
+
+test!(o2x2, remapper::ordered::bayer2x2);
+test!(o4x4, remapper::ordered::bayer4x4);
+test!(o8x8, remapper::ordered::bayer8x8);
+test!(o16x16, remapper::ordered::bayer16x16);
+test!(o32x32, remapper::ordered::bayer32x32);
+test!(o64x64, remapper::ordered::bayer64x64);
+
+// test!(s1, remapper::diffusion::sierra::sierra::<241>);
+// test!(s2, remapper::diffusion::sierra::sierra_two::<241>);
+// test!(s3, remapper::diffusion::sierra::sierra_lite::<241>);
diff --git a/tests/tres/.testing b/tests/tres/.testing
new file mode 100644
index 0000000..728533c
--- /dev/null
+++ b/tests/tres/.testing
@@ -0,0 +1 @@
+(be sure to run cargo test before changes) \ No newline at end of file