From e6ba386e1b55c12132ad9d30dfaa47638877132a Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Thu, 9 May 2024 00:16:10 +0200 Subject: [PATCH] Clean code up a bit and document options a bit --- src/bin/bin-search.rs | 57 ---------------- src/bin/sort.rs | 94 -------------------------- src/lib.rs | 105 ++++++++++++++++++++++++++++- src/main.rs | 153 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 257 insertions(+), 152 deletions(-) delete mode 100644 src/bin/bin-search.rs delete mode 100644 src/bin/sort.rs create mode 100644 src/main.rs diff --git a/src/bin/bin-search.rs b/src/bin/bin-search.rs deleted file mode 100644 index 6352f75..0000000 --- a/src/bin/bin-search.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::path::PathBuf; - -use clap::Parser; -use color_art::Color; -use image::{io::Reader as ImageReader, ImageBuffer, Rgba}; -use rand::{thread_rng, Rng}; -use sort_image::*; - -#[derive(Parser, Debug)] -#[command(version, about, long_about = None)] -struct Args { - #[arg(short, long)] - input: PathBuf, - - #[arg(short, long)] - output: PathBuf, - - #[arg(long)] - noise: Option, -} - -fn main() { - let args = Args::parse(); - - let input = ImageReader::open(args.input) - .expect("Couldn't open input file") - .decode() - .expect("Input file is not a valid image") - .into_rgba8(); - - let noise = args.noise.unwrap_or_default(); - - let mut rng = thread_rng(); - let mut res = ImageBuffer::, Vec>::new(input.width(), input.height()); - - for (input_row, output_row) in input.rows().zip(res.rows_mut()) { - let pixels = input_row - .enumerate() - .map(|(index, color)| (index, color, lightness(*color))) - .collect::>(); - // let original_lightness = pixels_ordered.iter().map(|(_, _, light)| *light).collect::>(); - // pixels_ordered.sort_by_key(|(_index, _color, light)| *light); - - for (index, output_pixel) in output_row.enumerate() { - let original_lightness = pixels[index] - .2 - .saturating_add_signed(((rng.gen::() - 0.5) * noise * u32::MAX as f64) as i32); - let index = pixels - .binary_search_by_key(&original_lightness, |(_, _, light)| *light) - .unwrap_or_else(|x| x); - let index = index.min(pixels.len() - 1); - *output_pixel = *pixels[index].1; - } - } - - res.save(args.output).expect("Couldn't save output file"); -} diff --git a/src/bin/sort.rs b/src/bin/sort.rs deleted file mode 100644 index c928ea9..0000000 --- a/src/bin/sort.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::path::PathBuf; - -use clap::Parser; -use image::{io::Reader as ImageReader, ImageBuffer, Rgba}; -use rand::{thread_rng, Rng}; -use sort_image::*; - -#[derive(Parser, Debug)] -#[command(version, about, long_about = None)] -struct Args { - #[arg(short, long)] - input: PathBuf, - - #[arg(short, long)] - output: PathBuf, - - #[arg(long)] - threshold: Option, - - #[arg(long)] - hue: bool, - - #[arg(long)] - absolute: bool, - - #[arg(long)] - reverse: bool, - - #[arg(long)] - noise: Option, - - #[arg(long)] - scatter: Option, -} - -fn main() { - let args = Args::parse(); - - let input = ImageReader::open(args.input) - .expect("Couldn't open input file") - .decode() - .expect("Input file is not a valid image") - .into_rgba8(); - - let threshold = args.threshold.unwrap_or(0.05); - let noise = args.noise.unwrap_or_default().min(1.0).max(0.0); - let scatter = args.scatter.unwrap_or_default().min(1.0).max(0.0); - - let mut res = ImageBuffer::, Vec>::new(input.width(), input.height()); - - let distance = if args.hue { hue_distance } else { rgb_distance }; - - let mut rng = thread_rng(); - let mut should_cutoff = move |left: Rgba, right: Rgba| -> bool { - distance(left, right) >= threshold * (1.0 - rng.gen::() * noise) - || rng.gen_bool(scatter) - }; - - for column in 0..input.width() { - let mut sorted = (0..input.height()) - .map(|index| input.get_pixel(column, index)) - .copied() - .collect::>(); - - if !args.absolute { - for chunk in sorted.chunk_by_mut(|left, right| !should_cutoff(*left, *right)) { - chunk.sort_by_key(|pixel| lightness(*pixel)); - if args.reverse { - chunk.reverse(); - } - } - } else { - let mut chunk_start = 0; - for i in 0..sorted.len() { - if should_cutoff(sorted[chunk_start], sorted[i]) { - let chunk = &mut sorted[chunk_start..=i]; - - chunk.sort_by_key(|pixel| lightness(*pixel)); - if args.reverse { - chunk.reverse(); - } - - chunk_start = i + 1; - } - } - } - - for (index, pixel) in sorted.drain(..).enumerate() { - *res.get_pixel_mut(column, index as u32) = pixel; - } - } - - res.save(args.output).expect("Couldn't save output file"); -} diff --git a/src/lib.rs b/src/lib.rs index f4db13d..811363c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ use color_art::Color; -use image::Rgba; +use image::{ImageBuffer, Rgba}; + +pub type Image = ImageBuffer, Vec>; pub fn lightness(color: Rgba) -> u32 { let [r, g, b, _a] = color.0; @@ -33,3 +35,104 @@ pub fn rgb_distance(left: Rgba, right: Rgba) -> f64 { (dr + dg + db).sqrt() as f64 / 3f64.sqrt() } + +fn modify_orthogonal( + input_image: &Image, + output_image: &mut Image, + mut condition: Cond, + mut modify: Modify, + map_coords: impl Fn(u32, u32) -> (u32, u32), + inv_map_coords: impl Fn(u32, u32) -> (u32, u32), +) where + Modify: for<'a> FnMut(&'a mut [Rgba]), + Cond: for<'a> FnMut(&'a [Rgba]) -> bool, +{ + assert!(input_image.width() == output_image.width()); + assert!(input_image.height() == output_image.height()); + + let (major_length, minor_length) = map_coords(input_image.width(), input_image.height()); + + let mut buffer = Vec::with_capacity(input_image.height() as usize); + for major_index in 0..major_length { + let mut start = 0; + for minor_index in 0..minor_length { + let (x, y) = inv_map_coords(major_index, minor_index); + if !buffer.is_empty() && (condition)(&buffer) { + (modify)(&mut buffer); + + for (pixel, out_minor) in buffer.drain(..).zip(start..=minor_index) { + let (x, y) = inv_map_coords(major_index, out_minor); + *output_image.get_pixel_mut(x, y) = pixel; + } + + start = minor_index; + } + + buffer.push(*input_image.get_pixel(x, y)); + } + + if buffer.len() > 0 { + (modify)(&mut buffer); + + for (pixel, out_minor) in buffer.drain(..).zip(start..=minor_length) { + let (x, y) = inv_map_coords(major_index, out_minor); + *output_image.get_pixel_mut(x, y) = pixel; + } + } + } +} + +pub fn modify_rows( + input_image: &Image, + output_image: &mut Image, + condition: Cond, + modify: Modify, +) where + Modify: for<'a> FnMut(&'a mut [Rgba]), + Cond: for<'a> FnMut(&'a [Rgba]) -> bool, +{ + modify_orthogonal( + input_image, + output_image, + condition, + modify, + |x, y| (y, x), + |x, y| (y, x), + ) +} + +pub fn modify_columns( + input_image: &Image, + output_image: &mut Image, + condition: Cond, + modify: Modify, +) where + Modify: for<'a> FnMut(&'a mut [Rgba]), + Cond: for<'a> FnMut(&'a [Rgba]) -> bool, +{ + modify_orthogonal( + input_image, + output_image, + condition, + modify, + |x, y| (x, y), + |x, y| (x, y), + ) +} + +pub fn modify_direction( + input_image: &Image, + output_image: &mut Image, + condition: Cond, + modify: Modify, + column: bool, +) where + Modify: for<'a> FnMut(&'a mut [Rgba]), + Cond: for<'a> FnMut(&'a [Rgba]) -> bool, +{ + if column { + modify_columns(input_image, output_image, condition, modify) + } else { + modify_rows(input_image, output_image, condition, modify) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8cf27c7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,153 @@ +use std::path::PathBuf; + +use clap::{Parser, ValueEnum}; +use image::{io::Reader as ImageReader, ImageBuffer, Rgba}; +use rand::{thread_rng, Rng}; +use sort_image::*; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// The input image file + #[arg(short, long)] + input: PathBuf, + + /// The output image file + #[arg(short, long)] + output: PathBuf, + + /// How much of a difference there needs to be between two pixels for them to be + /// placed in two different chunks. + #[arg(short, long)] + threshold: Option, + + /// When set, computes the difference between two pixels based on hue, + /// rather than RGB euclidian distance. + #[arg(long)] + hue: bool, + + /// When set, computes the difference between the starting and the ending pixel + /// of a chunk, rather than the last two pixels. + #[arg(long)] + absolute: bool, + + /// Reverses the order of the sort/binary search. + #[arg(short, long)] + reverse: bool, + + /// Adds some noise to the input image when computing the lightness. + /// (not fully implemented) + #[arg(long)] + noise: Option, + + /// Adds some randomness to the chunking algorithm; makes it so that the effective + /// threshold takes a random value between `threshold * (1 - threshold_noise)` and `threshold`. + #[arg(long)] + threshold_noise: Option, + + /// Adds random chunk boundaries throughout the image. + #[arg(long)] + scatter: Option, + + /// Choose which algorithm to use on chunks. + #[arg(long)] + mode: Option, + + /// Choose the direction of chunks. + #[arg(long)] + direction: Option, +} + +#[derive(ValueEnum, Debug, Clone, Copy)] +#[clap(rename_all = "kebab_case")] +enum Mode { + BinSearch, + Sort, +} + +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq)] +#[clap(rename_all = "kebab_case")] +enum Direction { + Column, + Row, +} + +/// Does an erronerous binary search on `pixels`, essentially re-paletting them in a funky way. +fn binary_search_shuffle(pixels: &mut [Rgba], rng: &mut R, noise: f64) { + let lightnesses = pixels + .iter() + .copied() + .map(|color| (color, lightness(color))) + .collect::>(); + + for output_pixel in pixels.iter_mut() { + let original_lightness = lightness(*output_pixel) + .saturating_add_signed(((rng.gen::() - 0.5) * noise * u32::MAX as f64) as i32); + let index = lightnesses + .binary_search_by_key(&original_lightness, |(_, light)| *light) + .unwrap_or_else(|x| x); + + let index = index.min(lightnesses.len() - 1); + *output_pixel = lightnesses[index].0; + } +} + +fn main() { + let args = Args::parse(); + + let input = ImageReader::open(args.input) + .expect("Couldn't open input file") + .decode() + .expect("Input file is not a valid image") + .into_rgba8(); + + let threshold = args.threshold.unwrap_or(0.05); + let noise = args.noise.unwrap_or_default().min(1.0).max(0.0); + let threshold_noise = args.threshold_noise.unwrap_or_default().min(1.0).max(0.0); + let scatter = args.scatter.unwrap_or_default().min(1.0).max(0.0); + + let mut res = ImageBuffer::, Vec>::new(input.width(), input.height()); + + let distance = if args.hue { hue_distance } else { rgb_distance }; + + let mut cutoff_rng = thread_rng(); + let mut rng = thread_rng(); + let mut should_cutoff = move |left: Rgba, right: Rgba| -> bool { + distance(left, right) >= threshold * (1.0 - cutoff_rng.gen::() * threshold_noise) + || cutoff_rng.gen_bool(scatter) + }; + + sort_image::modify_direction( + &input, + &mut res, + |pixels| { + if args.absolute { + should_cutoff(*pixels.first().unwrap(), *pixels.last().unwrap()) + } else { + should_cutoff( + pixels[pixels.len().saturating_sub(2)], + pixels[pixels.len() - 1], + ) + } + }, + |chunk| { + match args.mode.unwrap_or(Mode::Sort) { + Mode::Sort => { + chunk.sort_by_key(|pixel| lightness(*pixel)); + } + Mode::BinSearch => { + if args.reverse { + chunk.reverse(); + } + binary_search_shuffle(chunk, &mut rng, noise) + } + } + if args.reverse { + chunk.reverse(); + } + }, + args.direction != Some(Direction::Row), + ); + + res.save(args.output).expect("Couldn't save output file"); +}