From d6b5ab5ebc5b561966ded035e188f21ec7ba5b8c Mon Sep 17 00:00:00 2001 From: okaneco <47607823+okaneco@users.noreply.github.com> Date: Wed, 2 Aug 2023 21:37:40 -0400 Subject: [PATCH] Add `fxhash` for caching, relocate `find_colors` to `find.rs` Add `fxhash` to `palette_colors` feature Add more `clippy` `forbid` and `warn` lints to `lib.rs` Use caching of `Lab` conversions in the binary application Add `cached_srgba_to_lab` to `utils.rs` Use `map_err` in `parse_color` Add `allow` annotations for new `clippy` lints in `Sort` and kmeans Replace uses of `std::collections::HashMap` with `FxHashMap` --- Cargo.lock | 40 ++- Cargo.toml | 9 +- src/bin/kmeans_colors/app.rs | 555 ++++----------------------------- src/bin/kmeans_colors/find.rs | 467 +++++++++++++++++++++++++++ src/bin/kmeans_colors/main.rs | 20 +- src/bin/kmeans_colors/utils.rs | 23 +- src/colors/kmeans.rs | 16 +- src/colors/sort.rs | 34 +- src/lib.rs | 17 +- 9 files changed, 636 insertions(+), 545 deletions(-) create mode 100644 src/bin/kmeans_colors/find.rs diff --git a/Cargo.lock b/Cargo.lock index 2f8d6b9..87061ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,14 +90,23 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -120,9 +129,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -143,6 +152,7 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" name = "kmeans_colors" version = "0.6.0" dependencies = [ + "fxhash", "image", "num-traits", "palette", @@ -205,9 +215,9 @@ dependencies = [ [[package]] name = "palette" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1641aee47803391405d0a1250e837d2336fdddd18b27f3ddb8c1d80ce8d7f43" +checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc" dependencies = [ "approx", "fast-srgb8", @@ -216,13 +226,13 @@ dependencies = [ [[package]] name = "palette_derive" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c02bfa6b3ba8af5434fa0531bf5701f750d983d4260acd6867faca51cdc4484" +checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -279,9 +289,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -318,9 +328,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "structopt" @@ -359,9 +369,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5d6eb6a..5876f7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,12 @@ app = [ ] # Enable `palette` color types -palette_color = ["palette", "num-traits"] +palette_color = ["palette", "num-traits", "fxhash"] + +[dependencies.fxhash] +version = "0.2.1" +default-features = false +optional = true [dependencies.image] version = "0.24.6" @@ -34,7 +39,7 @@ features = ["jpeg", "png"] optional = true [dependencies.palette] -version = "0.7.2" +version = "0.7.3" default-features = false features = ["std"] optional = true diff --git a/src/bin/kmeans_colors/app.rs b/src/bin/kmeans_colors/app.rs index de01b6d..bfbce13 100644 --- a/src/bin/kmeans_colors/app.rs +++ b/src/bin/kmeans_colors/app.rs @@ -1,43 +1,48 @@ -use std::error::Error; - -use palette::cast::{from_component_slice, into_component_slice}; -use palette::{white_point::D65, FromColor, IntoColor, Lab, Srgb, Srgba}; - -use crate::args::{Command, Opt}; +use crate::args::Opt; use crate::filename::{create_filename, create_filename_palette}; -use crate::utils::{parse_color, print_colors, save_image, save_image_alpha, save_palette}; +use crate::utils::{cached_srgba_to_lab, print_colors, save_image, save_image_alpha, save_palette}; + +use fxhash::FxHashMap; use kmeans_colors::{get_kmeans, get_kmeans_hamerly, Calculate, Kmeans, MapColor, Sort}; +use palette::cast::{AsComponents, ComponentsAs}; +use palette::{white_point::D65, FromColor, IntoColor, Lab, LinSrgba, Srgb, Srgba}; -pub fn run(opt: Opt) -> Result<(), Box> { +pub fn run(opt: Opt) -> Result<(), Box> { if opt.input.is_empty() { eprintln!("No input files specified.") } let seed = opt.seed.unwrap_or(0); + // Cached results of Srgb -> Lab conversions; not cleared between runs + let mut lab_cache = FxHashMap::default(); + // Vec of pixels converted to Lab; cleared and reused between runs + let mut lab_pixels: Vec> = Vec::new(); + // Vec of pixels converted to Srgb; cleared and reused between runs + let mut rgb_pixels: Vec> = Vec::new(); + for file in &opt.input { if opt.verbose { println!("{}", &file.to_string_lossy()); } let img = image::open(file)?.into_rgba8(); - let (imgx, imgy) = (img.dimensions().0, img.dimensions().1); - let img_vec = img.as_raw(); + let (imgx, imgy) = img.dimensions(); + let img_vec: &[Srgba] = img.as_raw().components_as(); let converge = opt.factor.unwrap_or(if !opt.rgb { 5.0 } else { 0.0025 }); // Defaults to Lab, first case. if !opt.rgb { + lab_pixels.clear(); + // Convert Srgb image buffer to Lab for kmeans - let lab: Vec> = if !opt.transparent { - from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_linear::<_, f32>().into_color()) - .collect() + if !opt.transparent { + cached_srgba_to_lab(img_vec.iter(), &mut lab_cache, &mut lab_pixels); } else { - from_component_slice::>(img_vec) - .iter() - .filter(|x| x.alpha == 255) - .map(|x| x.into_linear::<_, f32>().into_color()) - .collect() + cached_srgba_to_lab( + img_vec.iter().filter(|x: &&Srgba| x.alpha == 255), + &mut lab_cache, + &mut lab_pixels, + ); }; // Iterate over amount of runs keeping best results @@ -49,7 +54,7 @@ pub fn run(opt: Opt) -> Result<(), Box> { opt.max_iter, converge, opt.verbose, - &lab, + &lab_pixels, seed + i as u64, ); if run_result.score < result.score { @@ -63,7 +68,7 @@ pub fn run(opt: Opt) -> Result<(), Box> { opt.max_iter, converge, opt.verbose, - &lab, + &lab_pixels, seed + i as u64, ); if run_result.score < result.score { @@ -114,10 +119,10 @@ pub fn run(opt: Opt) -> Result<(), Box> { .iter() .map(|&x| Srgb::from_linear(x.into_color())) .collect::>>(); - let lab: Vec> = Srgb::map_indices_to_centroids(centroids, &result.indices); + let rgb: Vec> = Srgb::map_indices_to_centroids(centroids, &result.indices); save_image( - into_component_slice(&lab), + rgb.as_components(), imgx, imgy, &create_filename(&opt.input, &opt.output, &opt.extension, Some(opt.k), file)?, @@ -128,22 +133,20 @@ pub fn run(opt: Opt) -> Result<(), Box> { // on the centroids we calculated and only paint in the pixels // that have a full alpha let mut indices = Vec::with_capacity(img_vec.len()); - let lab: Vec> = from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_linear::<_, f32>().into_color()) - .collect(); - Lab::::get_closest_centroid(&lab, &result.centroids, &mut indices); + + lab_pixels.clear(); + cached_srgba_to_lab(img_vec.iter(), &mut lab_cache, &mut lab_pixels); + Lab::::get_closest_centroid(&lab_pixels, &result.centroids, &mut indices); let centroids = &result .centroids .iter() - .map(|x| Srgba::from_color(*x).into_format()) + .map(|&x| Srgba::::from_linear(LinSrgba::from_color(x)).into_format()) .collect::>>(); - let data = from_component_slice::>(img_vec); - let lab: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) + let rgba: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) .iter() - .zip(data) + .zip(img_vec) .map(|(x, orig)| { if orig.alpha == 255 { *x @@ -153,26 +156,30 @@ pub fn run(opt: Opt) -> Result<(), Box> { }) .collect(); save_image_alpha( - into_component_slice(&lab), + rgba.as_components(), imgx, imgy, &create_filename(&opt.input, &opt.output, &opt.extension, Some(opt.k), file)?, )?; } } else { + rgb_pixels.clear(); + // Read image buffer into Srgb format - let rgb: Vec = if !opt.transparent { - from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_format::<_, f32>().into_color()) - .collect() + if !opt.transparent { + rgb_pixels.extend( + img_vec + .iter() + .map(|x| Srgb::::from_color(x.into_format::<_, f32>())), + ); } else { - from_component_slice::>(img_vec) - .iter() - .filter(|x| x.alpha == 255) - .map(|x| x.into_format::<_, f32>().into_color()) - .collect() - }; + rgb_pixels.extend( + img_vec + .iter() + .filter(|x| x.alpha == 255) + .map(|x| Srgb::::from_color(x.into_format::<_, f32>())), + ); + } // Iterate over amount of runs keeping best results let mut result = Kmeans::new(); @@ -183,7 +190,7 @@ pub fn run(opt: Opt) -> Result<(), Box> { opt.max_iter, converge, opt.verbose, - &rgb, + &rgb_pixels, seed + i as u64, ); if run_result.score < result.score { @@ -197,7 +204,7 @@ pub fn run(opt: Opt) -> Result<(), Box> { opt.max_iter, converge, opt.verbose, - &rgb, + &rgb_pixels, seed + i as u64, ); if run_result.score < result.score { @@ -250,7 +257,7 @@ pub fn run(opt: Opt) -> Result<(), Box> { let rgb: Vec> = Srgb::map_indices_to_centroids(centroids, &result.indices); save_image( - into_component_slice(&rgb), + rgb.as_components(), imgx, imgy, &create_filename(&opt.input, &opt.output, &opt.extension, Some(opt.k), file)?, @@ -261,11 +268,14 @@ pub fn run(opt: Opt) -> Result<(), Box> { // on the centroids we calculated and only paint in the pixels // that have a full alpha let mut indices = Vec::with_capacity(img_vec.len()); - let rgb: Vec = from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_format::<_, f32>().into_color()) - .collect(); - Srgb::get_closest_centroid(&rgb, &result.centroids, &mut indices); + + rgb_pixels.clear(); + rgb_pixels.extend( + img_vec + .iter() + .map(|x| Srgb::::from_color(x.into_format::<_, f32>())), + ); + Srgb::get_closest_centroid(&rgb_pixels, &result.centroids, &mut indices); let centroids = &result .centroids @@ -273,10 +283,9 @@ pub fn run(opt: Opt) -> Result<(), Box> { .map(|x| x.into_format().into()) .collect::>>(); - let data = from_component_slice::>(img_vec); let rgb: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) .iter() - .zip(data) + .zip(img_vec) .map(|(x, orig)| { if orig.alpha == 255 { *x @@ -286,7 +295,7 @@ pub fn run(opt: Opt) -> Result<(), Box> { }) .collect(); save_image_alpha( - into_component_slice(&rgb), + rgb.as_components(), imgx, imgy, &create_filename(&opt.input, &opt.output, &opt.extension, Some(opt.k), file)?, @@ -297,437 +306,3 @@ pub fn run(opt: Opt) -> Result<(), Box> { Ok(()) } - -/// Find the image pixels which closest match the supplied colors and save that -/// image as output. -pub fn find_colors( - Command::Find { - input, - colors, - replace, - max_iter, - factor, - runs, - percentage, - rgb, - verbose, - output, - seed, - transparent, - }: Command, -) -> Result<(), Box> { - // Print filename if multiple files and percentage is set - let display_filename = (input.len() > 1) && (percentage); - let converge = factor.unwrap_or(if !rgb { 5.0 } else { 0.0025 }); - - let seed = seed.unwrap_or(0); - - // Default to Lab colors - if !rgb { - // Initialize user centroids - let mut centroids: Vec> = Vec::with_capacity(colors.len()); - for c in colors { - centroids.push( - (parse_color(c.trim_start_matches('#'))?) - .into_linear::() - .into_color(), - ); - } - - for file in &input { - if display_filename { - println!("{}", &file.to_string_lossy()); - } - - let img = image::open(file)?.into_rgba8(); - let (imgx, imgy) = (img.dimensions().0, img.dimensions().1); - let img_vec = img.as_raw(); - - let lab: Vec> = if !transparent { - from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_linear::<_, f32>().into_color()) - .collect() - } else { - from_component_slice::>(img_vec) - .iter() - .filter(|x| x.alpha == 255) - .map(|x| x.into_linear::<_, f32>().into_color()) - .collect() - }; - - if !replace { - let mut indices = Vec::with_capacity(img_vec.len()); - - // We only need to do one pass of getting the closest colors to the - // custom centroids - Lab::::get_closest_centroid(&lab, ¢roids, &mut indices); - - if percentage { - let res = Lab::::sort_indexed_colors(¢roids, &indices); - print_colors(percentage, &res)?; - } - - if !transparent { - let rgb_centroids = ¢roids - .iter() - .map(|&x| Srgb::from_linear(x.into_color())) - .collect::>>(); - let lab: Vec> = - Srgb::map_indices_to_centroids(rgb_centroids, &indices); - - save_image( - into_component_slice(&lab), - imgx, - imgy, - &create_filename(&input, &output, "png", None, file)?, - false, - )?; - } else { - let rgb_centroids = ¢roids - .iter() - .map(|&x| Srgb::from_linear(x.into_color())) - .collect::>(); - - let mut indices = Vec::with_capacity(img_vec.len()); - let rgb: Vec = from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_format::<_, f32>().into_color()) - .collect(); - Srgb::get_closest_centroid(&rgb, rgb_centroids, &mut indices); - - let centroids = &rgb_centroids - .iter() - .map(|x| Srgba::from(*x).into_format()) - .collect::>>(); - - let data = from_component_slice::>(img_vec); - let lab: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) - .iter() - .zip(data) - .map(|(x, orig)| { - if orig.alpha == 255 { - *x - } else { - Srgba::new(0u8, 0, 0, 0) - } - }) - .collect(); - - save_image_alpha( - into_component_slice(&lab), - imgx, - imgy, - &create_filename(&input, &output, "png", None, file)?, - )?; - } - } else { - // Replace the k-means colors case - let mut result = Kmeans::new(); - let k = centroids.len(); - if k > 1 { - for i in 0..runs { - let run_result = get_kmeans_hamerly( - k, - max_iter, - converge, - verbose, - &lab, - seed + i as u64, - ); - if run_result.score < result.score { - result = run_result; - } - } - } else { - for i in 0..runs { - let run_result = - get_kmeans(k, max_iter, converge, verbose, &lab, seed + i as u64); - if run_result.score < result.score { - result = run_result; - } - } - } - - // This is the easiest way to make this work for transparent without a larger restructuring - let cloned_res = result.centroids.clone(); - - // We want to sort the user centroids based on the kmeans colors - // sorted by luminosity using the u8 returned in `sorted`. This - // corresponds to the index of the colors from darkest to lightest. - // We replace the colors in `sorted` with our centroids for printing - // purposes. - let mut res = - Lab::::sort_indexed_colors(&result.centroids, &result.indices); - res.iter_mut() - .zip(¢roids) - .for_each(|(s, c)| s.centroid = *c); - - if percentage { - print_colors(percentage, &res)?; - } - - // Sorting the centroids now - res.sort_unstable_by(|a, b| (a.index).cmp(&b.index)); - let sorted: Vec> = res.iter().map(|x| x.centroid).collect(); - - if !transparent { - let rgb_centroids = &sorted - .iter() - .map(|x| Srgb::from_color(*x).into_format()) - .collect::>>(); - let lab: Vec> = - Srgb::map_indices_to_centroids(rgb_centroids, &result.indices); - save_image( - into_component_slice(&lab), - imgx, - imgy, - &create_filename(&input, &output, "png", None, file)?, - false, - )?; - } else { - let rgb_centroids = &sorted - .iter() - .map(|x| Srgb::from_color(*x).into_format()) - .collect::>(); - - let mut indices = Vec::with_capacity(img_vec.len()); - let rgb: Vec = from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_format::<_, f32>().into_color()) - .collect(); - let temp_centroids = cloned_res - .iter() - .map(|x| Srgb::from_color(*x)) - .collect::>(); - Srgb::get_closest_centroid(&rgb, &temp_centroids, &mut indices); - - let centroids = &rgb_centroids - .iter() - .map(|x| Srgba::from(*x).into_format()) - .collect::>>(); - - let data = from_component_slice::>(img_vec); - let lab: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) - .iter() - .zip(data) - .map(|(x, orig)| { - if orig.alpha == 255 { - *x - } else { - Srgba::new(0u8, 0, 0, 0) - } - }) - .collect(); - - save_image_alpha( - into_component_slice(&lab), - imgx, - imgy, - &create_filename(&input, &output, "png", None, file)?, - )?; - } - } - } - - // Rgb case - } else { - // Initialize user centroids - let mut centroids: Vec = Vec::with_capacity(colors.len()); - for c in colors { - centroids.push((parse_color(c.trim_start_matches('#'))?).into_format()); - } - - for file in &input { - if display_filename { - println!("{}", &file.to_string_lossy()); - } - let img = image::open(file)?.into_rgba8(); - let (imgx, imgy) = (img.dimensions().0, img.dimensions().1); - let img_vec = img.as_raw(); - - let rgb: Vec = if !transparent { - from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_format::<_, f32>().into_color()) - .collect() - } else { - from_component_slice::>(img_vec) - .iter() - .filter(|x| x.alpha == 255) - .map(|x| x.into_format::<_, f32>().into_color()) - .collect() - }; - - if !replace { - let mut indices = Vec::with_capacity(img_vec.len()); - - // We only need to do one pass of getting the closest colors to the - // custom centroids - Srgb::get_closest_centroid(&rgb, ¢roids, &mut indices); - - if percentage { - let res = Srgb::sort_indexed_colors(¢roids, &indices); - print_colors(percentage, &res)?; - } - - if !transparent { - let rgb_centroids = ¢roids - .iter() - .map(|x| x.into_format()) - .collect::>>(); - let rgb: Vec> = - Srgb::map_indices_to_centroids(rgb_centroids, &indices); - - save_image( - into_component_slice(&rgb), - imgx, - imgy, - &create_filename(&input, &output, "png", None, file)?, - false, - )?; - } else { - let rgb_centroids = ¢roids - .iter() - .map(|x| x.into_format()) - .collect::>(); - - let mut indices = Vec::with_capacity(img_vec.len()); - let rgb: Vec = from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_format::<_, f32>().into_color()) - .collect(); - Srgb::get_closest_centroid(&rgb, rgb_centroids, &mut indices); - - let centroids = &rgb_centroids - .iter() - .map(|x| Srgba::from(*x).into_format()) - .collect::>>(); - - let data = from_component_slice::>(img_vec); - let rgb: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) - .iter() - .zip(data) - .map(|(x, orig)| { - if orig.alpha == 255 { - *x - } else { - Srgba::new(0u8, 0, 0, 0) - } - }) - .collect(); - - save_image_alpha( - into_component_slice(&rgb), - imgx, - imgy, - &create_filename(&input, &output, "png", None, file)?, - )?; - } - } else { - // Replace the k-means colors case - let mut result = Kmeans::new(); - let k = centroids.len(); - if k > 1 { - for i in 0..runs { - let run_result = get_kmeans_hamerly( - k, - max_iter, - converge, - verbose, - &rgb, - seed + i as u64, - ); - if run_result.score < result.score { - result = run_result; - } - } - } else { - for i in 0..runs { - let run_result = - get_kmeans(k, max_iter, converge, verbose, &rgb, seed + i as u64); - if run_result.score < result.score { - result = run_result; - } - } - } - - let cloned_res = result.centroids.clone(); - - // We want to sort the user centroids based on the kmeans colors - // sorted by luminosity using the u8 returned in `sorted`. This - // corresponds to the index of the colors from darkest to lightest. - // We replace the colors in `sorted` with our centroids for printing - // purposes. - let mut res = Srgb::sort_indexed_colors(&result.centroids, &result.indices); - res.iter_mut() - .zip(¢roids) - .for_each(|(s, c)| s.centroid = *c); - - if percentage { - print_colors(percentage, &res)?; - } - - // Sorting the centroids now - res.sort_unstable_by(|a, b| (a.index).cmp(&b.index)); - let sorted: Vec = res.iter().map(|x| x.centroid).collect(); - - if !transparent { - let rgb_centroids = &sorted - .iter() - .map(|x| x.into_format()) - .collect::>>(); - let rgb: Vec> = - Srgb::map_indices_to_centroids(rgb_centroids, &result.indices); - - save_image( - into_component_slice(&rgb), - imgx, - imgy, - &create_filename(&input, &output, "png", None, file)?, - false, - )?; - } else { - let rgb_centroids = &sorted - .iter() - .map(|x| x.into_format()) - .collect::>(); - - let mut indices = Vec::with_capacity(img_vec.len()); - let rgb: Vec = from_component_slice::>(img_vec) - .iter() - .map(|x| x.into_format::<_, f32>().into_color()) - .collect(); - Srgb::get_closest_centroid(&rgb, &cloned_res, &mut indices); - - let centroids = &rgb_centroids - .iter() - .map(|x| Srgba::from(*x).into_format()) - .collect::>>(); - - let data = from_component_slice::>(img_vec); - let lab: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) - .iter() - .zip(data) - .map(|(x, orig)| { - if orig.alpha == 255 { - *x - } else { - Srgba::new(0u8, 0, 0, 0) - } - }) - .collect(); - - save_image_alpha( - into_component_slice(&lab), - imgx, - imgy, - &create_filename(&input, &output, "png", None, file)?, - )?; - } - } - } - } - - Ok(()) -} diff --git a/src/bin/kmeans_colors/find.rs b/src/bin/kmeans_colors/find.rs new file mode 100644 index 0000000..a76d032 --- /dev/null +++ b/src/bin/kmeans_colors/find.rs @@ -0,0 +1,467 @@ +use fxhash::FxHashMap; +use palette::cast::{AsComponents, ComponentsAs}; +use palette::{white_point::D65, FromColor, IntoColor, Lab, Srgb, Srgba}; + +use crate::args::Command; +use crate::err::CliError; +use crate::filename::create_filename; +use crate::utils::{cached_srgba_to_lab, parse_color, print_colors, save_image, save_image_alpha}; +use kmeans_colors::{get_kmeans, get_kmeans_hamerly, Calculate, Kmeans, MapColor, Sort}; + +/// Find the image pixels which closest match the supplied colors and save that +/// image as output. +pub fn find_colors( + Command::Find { + input, + colors, + replace, + max_iter, + factor, + runs, + percentage, + rgb, + verbose, + output, + seed, + transparent, + }: Command, +) -> Result<(), Box> { + // Print filename if multiple files and percentage is set + let display_filename = (input.len() > 1) && (percentage); + let converge = factor.unwrap_or(if !rgb { 5.0 } else { 0.0025 }); + + let seed = seed.unwrap_or(0); + + // Cached results of Srgb -> Lab conversions; not cleared between runs + let mut lab_cache = FxHashMap::default(); + // Vec of pixels converted to Lab; cleared and reused between runs + let mut lab_pixels: Vec> = Vec::new(); + // Vec of pixels converted to Srgb; cleared and reused between runs + let mut rgb_pixels: Vec> = Vec::new(); + + // Default to Lab colors + if !rgb { + // Initialize user centroids + let centroids: Vec> = colors + .iter() + .map(|c| { + parse_color(c.trim_start_matches('#')).map(|c| c.into_linear::().into_color()) + }) + .collect::>()?; + + for file in &input { + if display_filename { + println!("{}", &file.to_string_lossy()); + } + + let img = image::open(file)?.into_rgba8(); + let (imgx, imgy) = img.dimensions(); + let img_vec: &[Srgba] = img.as_raw().components_as(); + + lab_pixels.clear(); + + if !transparent { + cached_srgba_to_lab(img_vec.iter(), &mut lab_cache, &mut lab_pixels); + } else { + cached_srgba_to_lab( + img_vec.iter().filter(|x: &&Srgba| x.alpha == 255), + &mut lab_cache, + &mut lab_pixels, + ); + } + + if !replace { + let mut indices = Vec::with_capacity(img_vec.len()); + + // We only need to do one pass of getting the closest colors to the + // custom centroids + Lab::::get_closest_centroid(&lab_pixels, ¢roids, &mut indices); + + if percentage { + let res = Lab::::sort_indexed_colors(¢roids, &indices); + print_colors(percentage, &res)?; + } + + if !transparent { + let rgb_centroids = ¢roids + .iter() + .map(|&x| Srgb::from_linear(x.into_color())) + .collect::>>(); + let lab: Vec> = + Srgb::map_indices_to_centroids(rgb_centroids, &indices); + + save_image( + lab.as_components(), + imgx, + imgy, + &create_filename(&input, &output, "png", None, file)?, + false, + )?; + } else { + let rgb_centroids = ¢roids + .iter() + .map(|&x| Srgb::from_linear(x.into_color())) + .collect::>(); + + let mut indices = Vec::with_capacity(img_vec.len()); + rgb_pixels.clear(); + rgb_pixels.extend( + img_vec + .iter() + .map(|x| Srgb::from_color(x.into_format::<_, f32>())), + ); + Srgb::get_closest_centroid(&rgb_pixels, rgb_centroids, &mut indices); + + let centroids = &rgb_centroids + .iter() + .map(|x| Srgba::from(*x).into_format()) + .collect::>>(); + + let rgba: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) + .iter() + .zip(img_vec) + .map(|(x, orig)| { + if orig.alpha == 255 { + *x + } else { + Srgba::new(0u8, 0, 0, 0) + } + }) + .collect(); + + save_image_alpha( + rgba.as_components(), + imgx, + imgy, + &create_filename(&input, &output, "png", None, file)?, + )?; + } + } else { + // Replace the k-means colors case + let mut result = Kmeans::new(); + let k = centroids.len(); + if k > 1 { + for i in 0..runs { + let run_result = get_kmeans_hamerly( + k, + max_iter, + converge, + verbose, + &lab_pixels, + seed + i as u64, + ); + if run_result.score < result.score { + result = run_result; + } + } + } else { + for i in 0..runs { + let run_result = get_kmeans( + k, + max_iter, + converge, + verbose, + &lab_pixels, + seed + i as u64, + ); + if run_result.score < result.score { + result = run_result; + } + } + } + + // This is the easiest way to make this work for transparent without a larger restructuring + let cloned_res = result.centroids.clone(); + + // We want to sort the user centroids based on the kmeans colors + // sorted by luminosity using the u8 returned in `sorted`. This + // corresponds to the index of the colors from darkest to lightest. + // We replace the colors in `sorted` with our centroids for printing + // purposes. + let mut res = + Lab::::sort_indexed_colors(&result.centroids, &result.indices); + res.iter_mut() + .zip(¢roids) + .for_each(|(s, c)| s.centroid = *c); + + if percentage { + print_colors(percentage, &res)?; + } + + // Sorting the centroids now + res.sort_unstable_by(|a, b| (a.index).cmp(&b.index)); + let sorted: Vec> = res.iter().map(|x| x.centroid).collect(); + + if !transparent { + let rgb_centroids = &sorted + .iter() + .map(|&x| Srgb::from_linear(x.into_color())) + .collect::>>(); + let rgb: Vec> = + Srgb::map_indices_to_centroids(rgb_centroids, &result.indices); + save_image( + rgb.as_components(), + imgx, + imgy, + &create_filename(&input, &output, "png", None, file)?, + false, + )?; + } else { + let rgb_centroids = &sorted + .iter() + .map(|&x| Srgb::from_linear(x.into_color())) + .collect::>(); + + let mut indices = Vec::with_capacity(img_vec.len()); + rgb_pixels.clear(); + rgb_pixels.extend( + img_vec + .iter() + .map(|x| Srgb::from_color(x.into_format::<_, f32>())), + ); + let temp_centroids = cloned_res + .iter() + .map(|&x| Srgb::from_linear(x.into_color())) + .collect::>(); + Srgb::get_closest_centroid(&rgb_pixels, &temp_centroids, &mut indices); + + let centroids = &rgb_centroids + .iter() + .map(|x| Srgba::from(*x).into_format()) + .collect::>>(); + + let rgba: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) + .iter() + .zip(img_vec) + .map(|(x, orig)| { + if orig.alpha == 255 { + *x + } else { + Srgba::new(0u8, 0, 0, 0) + } + }) + .collect(); + + save_image_alpha( + rgba.as_components(), + imgx, + imgy, + &create_filename(&input, &output, "png", None, file)?, + )?; + } + } + } + + // Rgb case + } else { + // Initialize user centroids + let mut centroids: Vec = Vec::with_capacity(colors.len()); + for c in colors { + centroids.push((parse_color(c.trim_start_matches('#'))?).into_format()); + } + + for file in &input { + if display_filename { + println!("{}", &file.to_string_lossy()); + } + let img = image::open(file)?.into_rgba8(); + let (imgx, imgy) = img.dimensions(); + let img_vec: &[Srgba] = img.as_raw().components_as(); + + rgb_pixels.clear(); + + if !transparent { + rgb_pixels.extend( + img_vec + .iter() + .map(|x| Srgb::from_color(x.into_format::<_, f32>())), + ); + } else { + rgb_pixels.extend( + img_vec + .iter() + .filter(|x| x.alpha == 255) + .map(|x| Srgb::from_color(x.into_format::<_, f32>())), + ); + } + + if !replace { + let mut indices = Vec::with_capacity(img_vec.len()); + + // We only need to do one pass of getting the closest colors to the + // custom centroids + Srgb::get_closest_centroid(&rgb_pixels, ¢roids, &mut indices); + + if percentage { + let res = Srgb::sort_indexed_colors(¢roids, &indices); + print_colors(percentage, &res)?; + } + + if !transparent { + let rgb_centroids = ¢roids + .iter() + .map(|x| x.into_format()) + .collect::>>(); + let rgb: Vec> = + Srgb::map_indices_to_centroids(rgb_centroids, &indices); + + save_image( + rgb.as_components(), + imgx, + imgy, + &create_filename(&input, &output, "png", None, file)?, + false, + )?; + } else { + let rgb_centroids = ¢roids + .iter() + .map(|x| x.into_format()) + .collect::>(); + + let mut indices = Vec::with_capacity(img_vec.len()); + rgb_pixels.clear(); + rgb_pixels.extend( + img_vec + .iter() + .map(|&x| Srgb::from_color(x.into_format::<_, f32>())), + ); + Srgb::get_closest_centroid(&rgb_pixels, rgb_centroids, &mut indices); + + let centroids = &rgb_centroids + .iter() + .map(|x| Srgba::from(*x).into_format()) + .collect::>>(); + + let rgb: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) + .iter() + .zip(img_vec) + .map(|(x, orig)| { + if orig.alpha == 255 { + *x + } else { + Srgba::new(0u8, 0, 0, 0) + } + }) + .collect(); + + save_image_alpha( + rgb.as_components(), + imgx, + imgy, + &create_filename(&input, &output, "png", None, file)?, + )?; + } + } else { + // Replace the k-means colors case + let mut result = Kmeans::new(); + let k = centroids.len(); + if k > 1 { + for i in 0..runs { + let run_result = get_kmeans_hamerly( + k, + max_iter, + converge, + verbose, + &rgb_pixels, + seed + i as u64, + ); + if run_result.score < result.score { + result = run_result; + } + } + } else { + for i in 0..runs { + let run_result = get_kmeans( + k, + max_iter, + converge, + verbose, + &rgb_pixels, + seed + i as u64, + ); + if run_result.score < result.score { + result = run_result; + } + } + } + + let cloned_res = result.centroids.clone(); + + // We want to sort the user centroids based on the kmeans colors + // sorted by luminosity using the u8 returned in `sorted`. This + // corresponds to the index of the colors from darkest to lightest. + // We replace the colors in `sorted` with our centroids for printing + // purposes. + let mut res = Srgb::sort_indexed_colors(&result.centroids, &result.indices); + res.iter_mut() + .zip(¢roids) + .for_each(|(s, c)| s.centroid = *c); + + if percentage { + print_colors(percentage, &res)?; + } + + // Sorting the centroids now + res.sort_unstable_by(|a, b| (a.index).cmp(&b.index)); + let sorted: Vec = res.iter().map(|x| x.centroid).collect(); + + if !transparent { + let rgb_centroids = &sorted + .iter() + .map(|x| x.into_format()) + .collect::>>(); + let rgb: Vec> = + Srgb::map_indices_to_centroids(rgb_centroids, &result.indices); + + save_image( + rgb.as_components(), + imgx, + imgy, + &create_filename(&input, &output, "png", None, file)?, + false, + )?; + } else { + let rgb_centroids = &sorted + .iter() + .map(|x| x.into_format()) + .collect::>(); + + let mut indices = Vec::with_capacity(img_vec.len()); + rgb_pixels.clear(); + rgb_pixels.extend( + img_vec + .iter() + .map(|x| Srgb::from_color(x.into_format::<_, f32>())), + ); + Srgb::get_closest_centroid(&rgb_pixels, &cloned_res, &mut indices); + + let centroids = &rgb_centroids + .iter() + .map(|x| Srgba::from(*x).into_format()) + .collect::>>(); + + let rgba: Vec> = Srgba::map_indices_to_centroids(centroids, &indices) + .iter() + .zip(img_vec) + .map(|(x, orig)| { + if orig.alpha == 255 { + *x + } else { + Srgba::new(0u8, 0, 0, 0) + } + }) + .collect(); + + save_image_alpha( + rgba.as_components(), + imgx, + imgy, + &create_filename(&input, &output, "png", None, file)?, + )?; + } + } + } + } + + Ok(()) +} diff --git a/src/bin/kmeans_colors/main.rs b/src/bin/kmeans_colors/main.rs index 7f8cd5f..e118ad1 100644 --- a/src/bin/kmeans_colors/main.rs +++ b/src/bin/kmeans_colors/main.rs @@ -1,31 +1,23 @@ #![warn(rust_2018_idioms, unsafe_code)] - mod app; mod args; mod err; mod filename; +mod find; mod utils; -use std::error::Error; -use std::process; - -use structopt::StructOpt; - -use app::{find_colors, run}; -use args::{Command, Opt}; - fn main() { if let Err(e) = try_main() { eprintln!("kmeans_colors: {e}"); - process::exit(1); + std::process::exit(1); } } -fn try_main() -> Result<(), Box> { - let opt = Opt::from_args(); +fn try_main() -> Result<(), Box> { + let opt: args::Opt = structopt::StructOpt::from_args(); match opt.cmd { - Some(command @ Command::Find { .. }) => find_colors(command)?, - _ => run(opt)?, + Some(command @ args::Command::Find { .. }) => find::find_colors(command)?, + _ => app::run(opt)?, } Ok(()) diff --git a/src/bin/kmeans_colors/utils.rs b/src/bin/kmeans_colors/utils.rs index 831a377..fa9f2e0 100644 --- a/src/bin/kmeans_colors/utils.rs +++ b/src/bin/kmeans_colors/utils.rs @@ -6,16 +6,16 @@ use std::path::Path; use std::str::FromStr; use image::ImageEncoder; -use palette::{IntoColor, Srgb}; +use palette::{white_point::D65, IntoColor, Lab, Srgb, Srgba}; use crate::err::CliError; use kmeans_colors::{Calculate, CentroidData}; /// Parse hex string to Rgb color. pub fn parse_color(c: &str) -> Result, CliError> { - Srgb::from_str(c).or_else(|_| { + Srgb::from_str(c).map_err(|_| { eprintln!("Invalid color: {c}"); - Err(CliError::InvalidHex) + CliError::InvalidHex }) } @@ -198,3 +198,20 @@ pub fn save_palette>( save_image(imgbuf.as_raw(), w, height, title, true) } + +/// Optimized conversion of colors from Srgb to Lab using a hashmap for caching +/// of expensive color conversions. +/// +/// Additionally, converting from Srgb to Linear Srgb is special-cased in +/// `palette` to use a lookup table which is faster than the regular conversion +/// using `color.into_format().into_color()`. +pub fn cached_srgba_to_lab<'a>( + rgb: impl Iterator>, + map: &mut fxhash::FxHashMap<[u8; 3], Lab>, + lab_pixels: &mut Vec>, +) { + lab_pixels.extend(rgb.map(|color| { + *map.entry([color.red, color.green, color.blue]) + .or_insert_with(|| color.into_linear::<_, f32>().into_color()) + })) +} diff --git a/src/colors/kmeans.rs b/src/colors/kmeans.rs index 94bc03a..0b8a48d 100644 --- a/src/colors/kmeans.rs +++ b/src/colors/kmeans.rs @@ -13,6 +13,7 @@ where T: Float + FromPrimitive + Zero, Lab: core::ops::AddAssign> + Default, { + #[allow(clippy::cast_possible_truncation)] fn get_closest_centroid(lab: &[Lab], centroids: &[Lab], indices: &mut Vec) { for color in lab.iter() { let mut index = 0; @@ -29,6 +30,7 @@ where } } + #[allow(clippy::cast_precision_loss)] fn recalculate_centroids( mut rng: &mut impl Rng, buf: &[Lab], @@ -39,7 +41,7 @@ where let mut temp = Lab::::default(); let mut counter: u64 = 0; for (&jdx, &color) in indices.iter().zip(buf) { - if jdx == idx as u8 { + if jdx as usize == idx { temp += color; counter += 1; } @@ -88,6 +90,7 @@ where T: Float + FromPrimitive + Zero, Rgb: core::ops::AddAssign> + Default, { + #[allow(clippy::cast_possible_truncation)] fn get_closest_centroid(rgb: &[Rgb], centroids: &[Rgb], indices: &mut Vec) { for color in rgb.iter() { let mut index = 0; @@ -104,6 +107,7 @@ where } } + #[allow(clippy::cast_precision_loss)] fn recalculate_centroids( mut rng: &mut impl Rng, buf: &[Rgb], @@ -114,7 +118,7 @@ where let mut temp = Rgb::::new(T::zero(), T::zero(), T::zero()); let mut counter: u64 = 0; for (&jdx, &color) in indices.iter().zip(buf) { - if jdx == idx as u8 { + if jdx as usize == idx { temp += color; counter += 1; } @@ -187,6 +191,7 @@ where } } + #[allow(clippy::cast_possible_truncation)] fn get_closest_centroid_hamerly( buffer: &[Self], centers: &HamerlyCentroids, @@ -241,6 +246,7 @@ where } } + #[allow(clippy::cast_precision_loss)] fn recalculate_centroids_hamerly( mut rng: &mut impl Rng, buf: &[Self], @@ -256,7 +262,7 @@ where let mut temp = Lab::::default(); let mut counter: u64 = 0; for (point, &color) in points.iter().zip(buf) { - if point.index == idx as u8 { + if point.index as usize == idx { temp += color; counter += 1; } @@ -318,6 +324,7 @@ where } } + #[allow(clippy::cast_possible_truncation)] fn get_closest_centroid_hamerly( buffer: &[Self], centers: &HamerlyCentroids, @@ -372,6 +379,7 @@ where } } + #[allow(clippy::cast_precision_loss)] fn recalculate_centroids_hamerly( mut rng: &mut impl Rng, buf: &[Self], @@ -387,7 +395,7 @@ where let mut temp = Rgb::::default(); let mut counter: u64 = 0; for (point, &color) in points.iter().zip(buf) { - if point.index == idx as u8 { + if point.index as usize == idx { temp += color; counter += 1; } diff --git a/src/colors/sort.rs b/src/colors/sort.rs index 5068da4..80f5e22 100644 --- a/src/colors/sort.rs +++ b/src/colors/sort.rs @@ -17,12 +17,15 @@ where .map(|res| res.centroid) } + #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn sort_indexed_colors(centroids: &[Self], indices: &[u8]) -> Vec> { // Count occurences of each color - "histogram" - let mut map: std::collections::HashMap = std::collections::HashMap::new(); - for (i, _) in centroids.iter().enumerate() { - map.insert(i as u8, 0); - } + let mut map: fxhash::FxHashMap = centroids + .iter() + .enumerate() + .map(|(i, _)| (i as u8, 0)) + .collect(); + for i in indices { let count = map.entry(*i).or_insert(0); *count += 1; @@ -32,10 +35,8 @@ where assert!(len > 0); let mut colors: Vec<(u8, f32)> = Vec::with_capacity(centroids.len()); for (i, _) in centroids.iter().enumerate() { - let count = map.get(&(i as u8)); - match count { - Some(x) => colors.push((i as u8, (*x as f32) / (len as f32))), - None => continue, + if let Some(&count) = map.get(&(i as u8)) { + colors.push((i as u8, (count as f32) / (len as f32))) } } @@ -81,12 +82,15 @@ where .map(|res| res.centroid) } + #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn sort_indexed_colors(centroids: &[Self], indices: &[u8]) -> Vec> { // Count occurences of each color - "histogram" - let mut map: std::collections::HashMap = std::collections::HashMap::new(); - for (i, _) in centroids.iter().enumerate() { - map.insert(i as u8, 0); - } + let mut map: fxhash::FxHashMap = centroids + .iter() + .enumerate() + .map(|(i, _)| (i as u8, 0)) + .collect(); + for i in indices { let count = map.entry(*i).or_insert(0); *count += 1; @@ -96,10 +100,8 @@ where assert!(len > 0); let mut colors: Vec<(u8, f32)> = Vec::with_capacity(centroids.len()); for (i, _) in centroids.iter().enumerate() { - let count = map.get(&(i as u8)); - match count { - Some(x) => colors.push((i as u8, (*x as f32) / (len as f32))), - None => continue, + if let Some(&count) = map.get(&(i as u8)) { + colors.push((i as u8, (count as f32) / (len as f32))) } } diff --git a/src/lib.rs b/src/lib.rs index 9bf12c1..9e3c753 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,7 +186,22 @@ //! res.sort_unstable_by(|a, b| (b.percentage).total_cmp(&a.percentage)); //! let dominant_color = res.first().unwrap().centroid; //! ``` -#![warn(missing_docs, rust_2018_idioms, unsafe_code)] +#![forbid( + absolute_paths_not_starting_with_crate, + missing_docs, + non_ascii_idents, + noop_method_call, + rust_2018_idioms, + unsafe_code, + unused_results +)] +#![warn( + clippy::cast_lossless, + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_precision_loss, + clippy::cast_sign_loss +)] #[cfg(feature = "palette_color")] mod colors;