good
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | src/lib.rs | 28 | ||||
| -rw-r--r-- | src/main.rs | 22 | ||||
| -rw-r--r-- | src/ordered.rs | 142 | ||||
| -rw-r--r-- | tests/test.rs | 43 | ||||
| -rw-r--r-- | tests/tres/.testing | 1 |
7 files changed, 155 insertions, 84 deletions
@@ -1 +1,2 @@ /target +tests/tres/*
\ No newline at end of file @@ -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", @@ -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 |