init, implement color quantization
This commit is contained in:
commit
0d8a4e9b0e
4 changed files with 306 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
158
Cargo.lock
generated
Normal file
158
Cargo.lock
generated
Normal file
|
@ -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",
|
||||
]
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -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"] }
|
138
src/main.rs
Normal file
138
src/main.rs
Normal file
|
@ -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<String> = 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<Rgb<u8>> {
|
||||
assert!(count > 0);
|
||||
|
||||
let mut buckets: Vec<Vec<Rgb<u8>>> = 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<u8>, b: Rgb<u8>) -> 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
|
||||
}
|
Loading…
Reference in a new issue