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");
|
|
||||||
}
|
|
@ -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…
Reference in new issue