From 5a9b206e5bfe86ccec93e83aed534a0b6e2c6b66 Mon Sep 17 00:00:00 2001 From: PurpleMyst Date: Fri, 15 Jul 2022 01:22:01 +0200 Subject: [PATCH] feat: use better calculations for distance and average color --- .gitattributes | 1 + .gitignore | 5 +- Cargo.lock | 323 +++++++++++++++++++++++++++++++++++++++++-------- Cargo.toml | 13 +- justfile | 7 ++ src/main.rs | 162 +++++++++++++++---------- 6 files changed, 393 insertions(+), 118 deletions(-) create mode 100644 .gitattributes create mode 100644 justfile diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..383338e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Cargo.lock -diff diff --git a/.gitignore b/.gitignore index 685d689..efbc3f9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,8 @@ *.heic *.mp4 *.webm +*.webp /.mypy_cache/ -tiles/ +/tiles/ +/input/ +/output/ diff --git a/Cargo.lock b/Cargo.lock index a785e1a..5ed7710 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,12 +23,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anyhow" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" - [[package]] name = "atty" version = "0.2.14" @@ -46,12 +40,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + [[package]] name = "bytemuck" version = "1.10.0" @@ -100,7 +106,6 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "regex", "terminal_size", "unicode-width", "winapi", @@ -162,12 +167,11 @@ dependencies = [ [[package]] name = "deflate" -version = "0.8.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" dependencies = [ "adler32", - "byteorder", ] [[package]] @@ -182,6 +186,80 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "exr" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cc0e06fb5f67e5d6beadf3a382fec9baca1aa751c6d5368fdeee7e5932c215" +dependencies = [ + "bit_field", + "deflate", + "flume", + "half", + "inflate", + "lebe", + "smallvec", + "threadpool", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ceeb589a3157cac0ab8cc585feb749bd2cea5cb55a6ee802ad72d9fd38303da" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "gif" version = "0.11.4" @@ -192,6 +270,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "heck" version = "0.3.3" @@ -212,13 +296,14 @@ dependencies = [ [[package]] name = "image" -version = "0.23.14" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212" dependencies = [ "bytemuck", "byteorder", "color_quant", + "exr", "gif", "jpeg-decoder", "num-iter", @@ -230,10 +315,16 @@ dependencies = [ ] [[package]] -name = "indicatif" -version = "0.15.0" +name = "indenter" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7baab56125e25686df467fe470785512329883aab42696d661247aca2a2896e4" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indicatif" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" dependencies = [ "console", "lazy_static", @@ -245,26 +336,69 @@ dependencies = [ ] [[package]] -name = "jpeg-decoder" -version = "0.1.22" +name = "inflate" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +dependencies = [ + "adler32", +] + +[[package]] +name = "jpeg-decoder" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" dependencies = [ "rayon", ] +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" + [[package]] name = "libc" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -276,30 +410,20 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.3.7" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", - "autocfg", ] [[package]] -name = "noisy_float" -version = "0.1.15" +name = "nanorand" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3deb89f8062c0dce4c805a987daff07f92e17c560f7c3ed631282555af902258" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "num-traits", + "getrandom", ] [[package]] @@ -325,9 +449,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-integer", @@ -355,9 +479,9 @@ dependencies = [ [[package]] name = "number_prefix" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "once_cell" @@ -366,15 +490,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] -name = "png" -version = "0.16.8" +name = "pin-project" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "png" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" dependencies = [ "bitflags", "crc32fast", "deflate", - "miniz_oxide 0.3.7", + "miniz_oxide", ] [[package]] @@ -470,6 +614,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +dependencies = [ + "lock_api", +] + [[package]] name = "strsim" version = "0.8.0" @@ -534,22 +693,30 @@ dependencies = [ name = "themis" version = "1.2.0" dependencies = [ - "anyhow", + "eyre", "image", "indicatif", - "noisy_float", "rayon", "structopt", ] [[package]] -name = "tiff" -version = "0.6.1" +name = "threadpool" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cfada0986f446a770eca461e8c6566cb879682f7d687c8348aa0c857bd52286" +dependencies = [ + "flate2", "jpeg-decoder", - "miniz_oxide 0.4.4", "weezl", ] @@ -583,6 +750,66 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + [[package]] name = "weezl" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index eb0bf47..3a097f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,10 @@ license = "GPL-3.0-only" edition = "2018" [dependencies] -image = "0.23.6" -anyhow = "1.0.31" -rayon = "1.3.1" -indicatif = { version = "0.15.0", features = ["rayon", "improved_unicode"] } -noisy_float = "0.1.12" -structopt = "0.3.15" +image = "0.24.2" +rayon = "1.5.3" +indicatif = { version = "0.16.2", features = ["rayon", "improved_unicode"] } +structopt = "0.3.26" +eyre = "0.6.8" + +[features] diff --git a/justfile b/justfile new file mode 100644 index 0000000..616bfa1 --- /dev/null +++ b/justfile @@ -0,0 +1,7 @@ +set shell := ["pwsh.exe", "-c"] + +run side *args: + cargo run --release -- -i input -t tiles -k -m {{side}} {{args}} + +run-debug side *args: + cargo run -- -i input -t tiles -k -m {{side}} {{args}} diff --git a/src/main.rs b/src/main.rs index 4cccc03..aee3cb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,11 @@ use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; -use anyhow::Result; -use image::{DynamicImage, GenericImage, GenericImageView, Pixel, Rgba}; -use indicatif::{ParallelProgressIterator, ProgressBar, ProgressIterator, ProgressStyle}; -use noisy_float::prelude::*; +use eyre::Result; +use image::{DynamicImage, GenericImage, GenericImageView, Rgba}; +use indicatif::{ + ParallelProgressIterator, ProgressBar, ProgressFinish, ProgressIterator, ProgressStyle, +}; use rayon::prelude::*; use structopt::StructOpt; @@ -15,28 +16,26 @@ fn average_color(image: &DynamicImage) -> Rgba { let (mut r, mut g, mut b, mut a) = (0., 0., 0., 0.); for (_x, _y, Rgba([pr, pg, pb, pa])) in image.pixels() { - r += pr as f64; - g += pg as f64; - b += pb as f64; - a += pa as f64; + r += pr as f64 * pr as f64; + g += pg as f64 * pg as f64; + b += pb as f64 * pb as f64; + a += pa as f64 * pa as f64; } - - let r = (r / pixel_count) as u8; - let g = (g / pixel_count) as u8; - let b = (b / pixel_count) as u8; - let a = (a / pixel_count) as u8; + let r = (r / pixel_count).sqrt() as u8; + let g = (g / pixel_count).sqrt() as u8; + let b = (b / pixel_count).sqrt() as u8; + let a = (a / pixel_count).sqrt() as u8; Rgba([r, g, b, a]) } -/// Calculate the euclidean distance between two pixels -fn distance(pixel1: Rgba, pixel2: Rgba) -> R64 { - r64(pixel1 - .map2(&pixel2, |l, r| if l < r { r - l } else { l - r }) - .channels() - .iter() - .map(|&n| (n as f64).powi(2)) - .sum::() - .sqrt()) +/// Calculate the distance (squared) between two colors +/// Code adapted from https://stackoverflow.com/a/9085524/13204109 +fn distance(Rgba([r1, g1, b1, _]): Rgba, Rgba([r2, g2, b2, _]): Rgba) -> i64 { + let rmean = (i64::from(r1) + i64::from(r2)) / 2; + let r = i64::from(r1) - i64::from(r2); + let g = i64::from(g1) - i64::from(g2); + let b = i64::from(b1) - i64::from(b2); + (((512 + rmean) * r * r) >> 8) + 4 * g * g + (((767 - rmean) * b * b) >> 8) } /// Choose the image in the given tileset whose average color is closest to the given pixel @@ -54,6 +53,7 @@ fn pick_image_for_pixel( fn load_images>(dir: P, tile_side: u32) -> Result)>> { let dir = fs::read_dir(dir)?.collect::, _>>()?; let len = dir.len(); + Ok(dir .into_par_iter() .progress_with(make_pbar("images loaded", len as _)) @@ -68,37 +68,52 @@ fn load_images>(dir: P, tile_side: u32) -> Result ProgressBar { +fn make_pbar(msg: &'static str, len: u64) -> ProgressBar { let bar = ProgressBar::new(len); bar.set_message(msg); bar.set_style( ProgressStyle::default_bar() .template("[{elapsed_precise}/{eta_precise}] {bar:40.green/red} {pos:>4}/{len:4} {msg}") - .progress_chars("##-"), + .progress_chars("##-") + .on_finish(ProgressFinish::AndLeave), ); bar } +/// Create a styled spinner +fn make_spinner(msg: &'static str, done_msg: &'static str) -> ProgressBar { + let bar = ProgressBar::new(0); + bar.set_message(msg); + bar.set_style( + ProgressStyle::default_bar() + .template("{spinner:.yellow} {msg}") + .progress_chars("##-") + .on_finish(ProgressFinish::WithMessage(done_msg)), + ); + bar.enable_steady_tick(100); + bar +} + #[derive(StructOpt)] struct Opt { /// The image to turn into a mosaic - #[structopt(parse(from_os_str))] - image: PathBuf, + #[structopt(short, long, parse(from_os_str))] + input_dir: PathBuf, /// The directory containing the tiles to utilize - #[structopt(parse(from_os_str))] - tiles_directory: PathBuf, + #[structopt(short, long, parse(from_os_str))] + tiles_dir: PathBuf, /// Where to save the finished mosaic - #[structopt(short, long, parse(from_os_str), default_value = "mosaic.png")] - output: PathBuf, + #[structopt(short, long, parse(from_os_str), default_value = "output")] + output_dir: PathBuf, /// The side length that the target image'll be resized to. #[structopt(short, long, default_value = "128")] mosaic_size: u32, /// The side length of each tile. - #[structopt(short, long, default_value = "26")] + #[structopt(long, default_value = "26")] tile_size: u32, /// Keep the image's aspect ratio @@ -108,46 +123,67 @@ struct Opt { fn main() -> Result<()> { let Opt { - image, - tiles_directory, + input_dir, + tiles_dir, mosaic_size, tile_size, keep_aspect_ratio, - output, + output_dir, } = Opt::from_args(); - let possible_tiles = load_images(tiles_directory, tile_size)?; - let image = image::open(image)?; - let image = if keep_aspect_ratio { - image.thumbnail(mosaic_size, mosaic_size) - } else { - image.thumbnail_exact(mosaic_size, mosaic_size) - }; + fs::create_dir_all(&output_dir)?; - // For every unique pixel in the image, find its most appropiate tile - let unique_pixels = image - .pixels() - .map(|(_x, _y, pixel)| pixel) - .collect::>(); - let pbar = make_pbar("pixels", unique_pixels.len() as _); - let tiles = unique_pixels - .into_par_iter() - .progress_with(pbar) - .filter_map(|pixel| { - let tile = pick_image_for_pixel(pixel, &possible_tiles)?; - Some((pixel, tile)) - }) - .collect::>(); + let possible_tiles = load_images(tiles_dir, tile_size)?; - // Apply the mapping previously calculated and save the mosaic - let mut mosaic = DynamicImage::new_rgba8(image.width() * tile_size, image.height() * tile_size); - for (x, y, pixel) in image.pixels().progress_with(make_pbar( - "actual pixels", - u64::from(image.width() * image.height()), - )) { - mosaic.copy_from(&**tiles.get(&pixel).unwrap(), x * tile_size, y * tile_size)?; + let input_dir = fs::read_dir(input_dir)?.collect::, _>>()?; + + for image in input_dir { + let input_path = image.path(); + eprintln!("Processing {}", input_path.display()); + + let output = output_dir.join(format!( + "{}.mosaic{mosaic_size}.png", + input_path.file_stem().unwrap().to_string_lossy() + )); + if output.exists() { + continue; + } + + let img = image::open(&input_path)?; + let img = if keep_aspect_ratio { + img.thumbnail(mosaic_size, mosaic_size) + } else { + img.thumbnail_exact(mosaic_size, mosaic_size) + }; + + // For every unique pixel in the image, find its most appropiate tile + let unique_pixels = img + .pixels() + .map(|(_x, _y, pixel)| pixel) + .collect::>(); + let len = unique_pixels.len(); + let tiles = unique_pixels + .into_par_iter() + .progress_with(make_pbar("pixels", len as _)) + .filter_map(|pixel| { + let tile = pick_image_for_pixel(pixel, &possible_tiles)?; + Some((pixel, tile)) + }) + .collect::>(); + + // Apply the mapping previously calculated and save the mosaic + let mut mosaic = DynamicImage::new_rgba8(img.width() * tile_size, img.height() * tile_size); + for (x, y, pixel) in img.pixels().progress_with(make_pbar( + "actual pixels", + u64::from(img.width() * img.height()), + )) { + mosaic.copy_from(&**tiles.get(&pixel).unwrap(), x * tile_size, y * tile_size)?; + } + + let spinner = make_spinner("Saving", "Saved!"); + mosaic.save(output)?; + spinner.finish_using_style(); } - mosaic.save(output)?; Ok(()) }