commit 0d8a4e9b0e0831a567aa65d50394bd6552d93e2d Author: CrispyPin Date: Wed Dec 4 01:46:45 2024 +0100 init, implement color quantization diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..97d4943 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,158 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dither" +version = "0.1.0" +dependencies = [ + "image", +] + +[[package]] +name = "fdeflate" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "image-webp", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "png" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9075dbe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dither" +version = "0.1.0" +edition = "2021" + +[dependencies] +# image = "0.25.5" +# image = { version = "0.25.5", default-features = false, features = ["default-formats"] } +image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "webp"] } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2e545f4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,138 @@ +use std::{env, fs::File, io::Read}; + +use image::{imageops::FilterType, DynamicImage, GenericImage, GenericImageView, Pixel, Rgb}; + +fn main() { + let mut args: Vec = env::args().skip(1).collect(); + if args.is_empty() { + println!("no input file specified"); + return; + } + dbg!(&args); + + let inpath = args.pop().unwrap(); + let mut data = Vec::new(); + File::open(inpath).unwrap().read_to_end(&mut data).unwrap(); + let image = image::load_from_memory(&data).unwrap(); + println!("loaded"); + + let (w, h) = image.dimensions(); + let image = image.resize(w / 8, h / 8, FilterType::CatmullRom); + println!("resized"); + let image = dither_limit(image, 2); + image.save("out.png").unwrap(); + println!("saved"); +} + +const BAYER_4x4: [[u8; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]]; + +fn bayer_f(x: usize, y: usize) -> f32 { + (BAYER_4x4[y][x] as f32 / 16.) // - (15./16.)/2. +} + +fn quantize(image: &DynamicImage, count: usize) -> Vec> { + assert!(count > 0); + + let mut buckets: Vec>> = vec![image + .pixels() + .map(|(_x, _y, a)| a.to_rgb()) // + .collect()]; + + // divide buckets count-1 times + for _i in 0..(count - 1) { + // spread amount and channel for each bucket + let mut spreads: Vec<(u8, usize)> = Vec::new(); + // calculate where each bucket would do its division if it's chosen + // todo: only calculate this when the bucket is created/modified + for bucket in &buckets { + let mut min = [255; 3]; + let mut max = [0; 3]; + for pixel in bucket { + for channel in 0..3 { + min[channel] = min[channel].min(pixel.0[channel]); + max[channel] = max[channel].max(pixel.0[channel]); + } + } + let range_r = max[0] - min[0]; + let range_g = max[1] - min[1]; + let range_b = max[2] - min[2]; + let mut widest_channel = 0; + let mut widest_amount = range_r; + if range_g > widest_amount { + widest_amount = range_g; + widest_channel = 1; + } + if range_b > widest_amount { + widest_amount = range_b; + widest_channel = 2; + } + spreads.push((widest_amount, widest_channel)) + } + + let mut most_spread_bucket = 0; + let mut highest_spread = 0; + for (i, &(spread, _channel)) in spreads.iter().enumerate() { + if spread > highest_spread { + most_spread_bucket = i; + highest_spread = spread; + } + } + + // divide the most spread bucket + let bucket = &mut buckets[most_spread_bucket]; + if bucket.len() < 2 { + continue; + } + + let channel = spreads[most_spread_bucket].1; + bucket.sort_unstable_by_key(|pixel| pixel.0[channel]); + let halfway = bucket.len() / 2; + let new_bucket = bucket[halfway..].to_owned(); + bucket.truncate(halfway); + buckets.push(new_bucket); + } + let mut colors = Vec::new(); + for bucket in buckets { + let mut avg = [0u128; 3]; + for p in &bucket { + for channel in 0..3 { + avg[channel] += p.0[channel] as u128; + } + } + let num = bucket.len() as u128; + colors.push(Rgb([ + (avg[0] / num) as u8, + (avg[1] / num) as u8, + (avg[2] / num) as u8, + ])) + } + colors +} + +fn color_dist(a: Rgb, b: Rgb) -> f32 { + let r = a.0[0] as f32 - b.0[0] as f32; + let g = a.0[1] as f32 - b.0[1] as f32; + let b = a.0[2] as f32 - b.0[2] as f32; + (r * r + g * g + b * b).sqrt() +} + +fn dither_limit(input: DynamicImage, count: usize) -> DynamicImage { + let colors = quantize(&input, count); + let mut out = input.clone(); + + for (x, y, color) in input.pixels() { + let color = color.to_rgb(); + let mut closest_index = 0; + let mut closest_dist = color_dist(color, colors[0]); + for (i, &c) in colors.iter().enumerate() { + let d = color_dist(color, c); + if d < closest_dist { + closest_dist = d; + closest_index = i; + } + } + out.put_pixel(x, y, colors[closest_index].to_rgba()); + } + + out +}