Clean code up a bit and document options a bit

main
Shad Amethyst 6 months ago
parent cf9eb41c17
commit e6ba386e1b

@ -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<f64>,
}
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::<Rgba<u8>, Vec<u8>>::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::<Vec<_>>();
// let original_lightness = pixels_ordered.iter().map(|(_, _, light)| *light).collect::<Vec<_>>();
// 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::<f64>() - 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");
}

@ -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<f64>,
#[arg(long)]
hue: bool,
#[arg(long)]
absolute: bool,
#[arg(long)]
reverse: bool,
#[arg(long)]
noise: Option<f64>,
#[arg(long)]
scatter: Option<f64>,
}
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::<Rgba<u8>, Vec<u8>>::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<u8>, right: Rgba<u8>| -> bool {
distance(left, right) >= threshold * (1.0 - rng.gen::<f64>() * 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::<Vec<_>>();
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");
}

@ -1,5 +1,7 @@
use color_art::Color; use color_art::Color;
use image::Rgba; use image::{ImageBuffer, Rgba};
pub type Image = ImageBuffer<Rgba<u8>, Vec<u8>>;
pub fn lightness(color: Rgba<u8>) -> u32 { pub fn lightness(color: Rgba<u8>) -> u32 {
let [r, g, b, _a] = color.0; let [r, g, b, _a] = color.0;
@ -33,3 +35,104 @@ pub fn rgb_distance(left: Rgba<u8>, right: Rgba<u8>) -> f64 {
(dr + dg + db).sqrt() as f64 / 3f64.sqrt() (dr + dg + db).sqrt() as f64 / 3f64.sqrt()
} }
fn modify_orthogonal<Modify, Cond>(
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<u8>]),
Cond: for<'a> FnMut(&'a [Rgba<u8>]) -> 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<Modify, Cond>(
input_image: &Image,
output_image: &mut Image,
condition: Cond,
modify: Modify,
) where
Modify: for<'a> FnMut(&'a mut [Rgba<u8>]),
Cond: for<'a> FnMut(&'a [Rgba<u8>]) -> bool,
{
modify_orthogonal(
input_image,
output_image,
condition,
modify,
|x, y| (y, x),
|x, y| (y, x),
)
}
pub fn modify_columns<Modify, Cond>(
input_image: &Image,
output_image: &mut Image,
condition: Cond,
modify: Modify,
) where
Modify: for<'a> FnMut(&'a mut [Rgba<u8>]),
Cond: for<'a> FnMut(&'a [Rgba<u8>]) -> bool,
{
modify_orthogonal(
input_image,
output_image,
condition,
modify,
|x, y| (x, y),
|x, y| (x, y),
)
}
pub fn modify_direction<Modify, Cond>(
input_image: &Image,
output_image: &mut Image,
condition: Cond,
modify: Modify,
column: bool,
) where
Modify: for<'a> FnMut(&'a mut [Rgba<u8>]),
Cond: for<'a> FnMut(&'a [Rgba<u8>]) -> bool,
{
if column {
modify_columns(input_image, output_image, condition, modify)
} else {
modify_rows(input_image, output_image, condition, modify)
}
}

@ -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<f64>,
/// 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<f64>,
/// 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<f64>,
/// Adds random chunk boundaries throughout the image.
#[arg(long)]
scatter: Option<f64>,
/// Choose which algorithm to use on chunks.
#[arg(long)]
mode: Option<Mode>,
/// Choose the direction of chunks.
#[arg(long)]
direction: Option<Direction>,
}
#[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<R: Rng>(pixels: &mut [Rgba<u8>], rng: &mut R, noise: f64) {
let lightnesses = pixels
.iter()
.copied()
.map(|color| (color, lightness(color)))
.collect::<Vec<_>>();
for output_pixel in pixels.iter_mut() {
let original_lightness = lightness(*output_pixel)
.saturating_add_signed(((rng.gen::<f64>() - 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::<Rgba<u8>, Vec<u8>>::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<u8>, right: Rgba<u8>| -> bool {
distance(left, right) >= threshold * (1.0 - cutoff_rng.gen::<f64>() * 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");
}
Loading…
Cancel
Save