commit
cf9eb41c17
@ -0,0 +1 @@
|
|||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "sort-image"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.4", features = ["derive"] }
|
||||||
|
color-art = "0.3.8"
|
||||||
|
image = "0.25.1"
|
||||||
|
rand = "0.8.5"
|
@ -0,0 +1,57 @@
|
|||||||
|
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");
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
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,35 @@
|
|||||||
|
use color_art::Color;
|
||||||
|
use image::Rgba;
|
||||||
|
|
||||||
|
pub fn lightness(color: Rgba<u8>) -> u32 {
|
||||||
|
let [r, g, b, _a] = color.0;
|
||||||
|
|
||||||
|
let r = r as f32 / 255.0;
|
||||||
|
let g = g as f32 / 255.0;
|
||||||
|
let b = b as f32 / 255.0;
|
||||||
|
|
||||||
|
let res = (r * r * 0.299 + g * g * 0.587 + b * b * 0.114).sqrt();
|
||||||
|
|
||||||
|
(res * u32::MAX as f32) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hue_distance(left: Rgba<u8>, right: Rgba<u8>) -> f64 {
|
||||||
|
let left = Color::from_rgb(left.0[0], left.0[1], left.0[2]).unwrap();
|
||||||
|
let right = Color::from_rgb(right.0[0], right.0[1], right.0[2]).unwrap();
|
||||||
|
|
||||||
|
let delta = (left.hue() - right.hue()).abs();
|
||||||
|
let delta = delta.min(360.0 - delta) / 360.0;
|
||||||
|
|
||||||
|
delta
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rgb_distance(left: Rgba<u8>, right: Rgba<u8>) -> f64 {
|
||||||
|
let [r1, g1, b1, _] = left.0;
|
||||||
|
let [r2, g2, b2, _] = right.0;
|
||||||
|
|
||||||
|
let dr = (r1 as f32 / 255.0 - r2 as f32 / 255.0).abs().powi(2);
|
||||||
|
let dg = (g1 as f32 / 255.0 - g2 as f32 / 255.0).abs().powi(2);
|
||||||
|
let db = (b1 as f32 / 255.0 - b2 as f32 / 255.0).abs().powi(2);
|
||||||
|
|
||||||
|
(dr + dg + db).sqrt() as f64 / 3f64.sqrt()
|
||||||
|
}
|
Loading…
Reference in new issue