diff options
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | squash/Cargo.toml | 4 | ||||
-rw-r--r-- | squash/src/cli.rs | 51 | ||||
-rw-r--r-- | squash/src/main.rs | 25 | ||||
-rw-r--r-- | src/difference.rs | 54 | ||||
-rw-r--r-- | src/lib.rs | 246 | ||||
-rw-r--r-- | src/selection.rs | 118 |
8 files changed, 339 insertions, 169 deletions
diff --git a/Cargo.toml b/Cargo.toml index 7be3913..e0cdfc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "colorsquash" -version = "0.1.0" -authors = ["gennyble <gen@nyble.dev>"] +version = "0.2.0" +authors = ["gennyble <gen@nyble.dev>", "novedevo <devon@nove.dev>"] edition = "2021" license = "ISC" description = "A crate for quantizing colours with preference to the most frequently occuring" diff --git a/README.md b/README.md index ef9ce84..3601c3a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ and takes the top N colours that are sufficiently different. [^1]: [wikipedia: color quantization](https://en.wikipedia.org/wiki/Color_quantization) +**features** + +**`kmeans`** - use kmeans for palette selection instead of sort & select. +**`gifed`** - adds the `Squasher::palette_gifed()` method allowing you to +directly get a gifed's Palette struct. + ### squash A CLI tool to quantize colours :D diff --git a/squash/Cargo.toml b/squash/Cargo.toml index 760dcaf..7d33b5b 100644 --- a/squash/Cargo.toml +++ b/squash/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "squash" -version = "0.2.0" +version = "0.3.0" authors = ["gennyble <gen@nyble.dev>"] edition = "2021" license = "ISC" @@ -11,7 +11,7 @@ repository = "https://github.com/gennyble/colorsquash/tree/main/squash" [dependencies] # the meat 'o the thing! the meaning behind it all -colorsquash = { path = "..", version = "0.1.0", features = ["gifed"] } +colorsquash = { path = "..", version = "0.2.0", features = ["gifed", "kmeans"] } # just useful tools for writing binaries anyhow = "1.0.75" diff --git a/squash/src/cli.rs b/squash/src/cli.rs index dec36fa..ba1658a 100644 --- a/squash/src/cli.rs +++ b/squash/src/cli.rs @@ -1,6 +1,7 @@ use std::cmp::Ordering; use camino::Utf8PathBuf; +use colorsquash::difference::{self, DiffFn}; const NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -9,7 +10,8 @@ const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); pub struct Cli { pub color_count: u8, pub tolerance: Option<f32>, - pub difference: DifferenceFn, + pub selector: Selector, + pub difference: &'static DiffFn, pub input: Utf8PathBuf, pub in_type: InType, pub output: Utf8PathBuf, @@ -23,6 +25,7 @@ struct BuildingCli { pub color_count: Option<u8>, pub tolerance: Option<f32>, pub difference: DifferenceFn, + pub selector: Selector, } impl BuildingCli { @@ -58,10 +61,16 @@ impl BuildingCli { } }; + let difference = match self.difference { + DifferenceFn::Rgb => &difference::rgb as &DiffFn, + DifferenceFn::Redmean => &difference::redmean as &DiffFn, + }; + Cli { color_count: self.color_count.unwrap_or(Self::DEFAULT_COLORS), tolerance: self.tolerance, - difference: self.difference, + selector: self.selector, + difference, input, in_type, output, @@ -87,6 +96,13 @@ pub enum DifferenceFn { Redmean, } +#[derive(Debug, Default)] +pub enum Selector { + #[default] + SortSelect, + Kmeans, +} + pub fn build() -> Cli { let mut free = vec![]; let mut building = BuildingCli::default(); @@ -142,7 +158,16 @@ pub fn build() -> Cli { std::process::exit(1); } }, + Some(("selector", sel)) | Some(("sel", sel)) => match sel { + "sort/select" => building.selector = Selector::SortSelect, + "kmeans" => building.selector = Selector::Kmeans, + _ => { + eprintln!("'{sel}' is not recognized as a selector. See help=selectors"); + std::process::exit(1); + } + }, Some(("help", "algorithms")) => print_help_algorithms(), + Some(("help", "selectors")) => print_help_selectors(), Some(("help", _)) => print_help(), Some(("version", _)) => print_version(), Some((key, _)) => { @@ -174,12 +199,16 @@ fn print_help() -> ! { println!(" the number of colours the final image should contain"); println!(" a whole number more than 0 and less than, or equal, 256"); println!(" [Default 256]\n"); - println!(" difference=<algorithm> | did=<algorithm>"); + println!(" difference=<algorithm> | dif=<algorithm>"); println!(" the color comparison function to use. one of: rgb, redmean"); - println!(" for more details use help=algorithms. [Default rgb]"); + println!(" for more details use help=algorithms. [Default rgb]\n"); + println!(" selection=<selector> | sel=<selector>"); + println!(" the algorithm for picking the palette. one of: means, sort/select"); + println!(" for more details use help=selectors. [Default sort/select]"); println!(" tolerance=<float> | tol=<float>"); println!(" how different colours should be to be added to the palette"); - println!(" a number > 0 and <= 100\n"); + println!(" only sort/select usese this value."); + println!(" a number > 0 and <= 100 [Default 3]\n"); println!(" help= | -h | --help"); println!(" print this message and exit\n"); println!(" version= | -V | --version"); @@ -199,6 +228,18 @@ fn print_help_algorithms() -> ! { std::process::exit(0) } +fn print_help_selectors() -> ! { + println!("SELECTORS:"); + println!("sort/select:"); + println!(" the original colorsquash algorithm. sorts colors from most to least"); + println!(" frequent and then picks the most frequent colors so long as they are"); + println!(" sufficiently different (configurable with tolerance=)\n"); + println!("kmeans:"); + println!(" uses the kmeans clustering algorithm to select colours."); + println!(" Ignores tolerance="); + std::process::exit(0) +} + fn print_version() -> ! { println!("squash version {VERSION}"); println!("written by {AUTHORS}"); diff --git a/squash/src/main.rs b/squash/src/main.rs index 5437ea1..a7cc66b 100644 --- a/squash/src/main.rs +++ b/squash/src/main.rs @@ -1,5 +1,7 @@ -use cli::DifferenceFn; -use colorsquash::{Squasher, SquasherBuilder}; +use colorsquash::{ + selection::{Kmeans, SortSelect}, + SquasherBuilder, +}; use crate::cli::{InType, OutType}; @@ -16,15 +18,20 @@ fn main() -> Result<(), anyhow::Error> { InType::Jpeg => image::get_jpg(cli.input)?, }; - let mut builder = SquasherBuilder::default().max_colors(cli.color_count); + let mut builder = SquasherBuilder::new() + .max_colors(cli.color_count) + .mapper_difference(cli.difference); - if let Some(tol) = cli.tolerance { - builder = builder.tolerance(tol); - } + match cli.selector { + cli::Selector::SortSelect => { + let mut sorsel = SortSelect::default().difference(cli.difference); + if let Some(tol) = cli.tolerance { + sorsel = sorsel.tolerance(tol) + } - builder = match cli.difference { - DifferenceFn::Rgb => builder.difference(&colorsquash::difference::rgb_difference), - DifferenceFn::Redmean => builder.difference(&colorsquash::difference::redmean_difference), + builder = builder.selector(sorsel); + } + cli::Selector::Kmeans => builder = builder.selector(Kmeans), }; let mut squasher = builder.build(&image.data); diff --git a/src/difference.rs b/src/difference.rs index 42c4c4f..b52b34f 100644 --- a/src/difference.rs +++ b/src/difference.rs @@ -1,30 +1,56 @@ -//! A set of difference functions you can use with [SquasherBuilder::difference] +//! A set of difference functions you can use with [SquasherBuilder::difference()] +//! +//! # Writing your own difference function +//! The type you want is `dyn Fn(&RGB8, &RGB8) -> f32` +//! (defined as [`DiffFn`]) +//! +//! The first argument is the color already in the palette and the second is +//! the color we're checking. These are [RGB8] which is a rexport from the `rgb` +//! crate. +//! +//! The value returned is between 0 and 768, but that's not a hard-rule. If you +//! return a value out of that range you'll have to adjust the tolerance with +//! [Squasher::set_tolerance()] or [SquasherBuilder::tolerance]. +//! +//! The difference functions have the possibility of being called hundreds of +//! thousands of times; you might want to `#[inline(always)]` + +// This is used in the module level documentation just above. Without it we'd +// have to fully qualify the interlink which is also how it'd be displayed. +#[allow(unused_imports)] +use crate::{Squasher, SquasherBuilder}; // rexport this so people don't need to add the rgb crate to their project. this // also helps avoid crate version mismatch +/// rexport from the [`rgb`](https://docs.rs/rgb/0.8.37/rgb/) crate. pub use rgb::RGB8; +/// Type definition for difference functions. +pub type DiffFn = dyn Fn(&RGB8, &RGB8) -> f32; + /// A naïve comparison just summing the channel differences /// I.E. `|a.red - b.red| + |a.green - b.green| + |a.blue - b.blue|` #[allow(clippy::many_single_char_names)] #[inline(always)] -pub fn rgb_difference(a: &RGB8, b: &RGB8) -> f32 { - let absdiff = |a: u8, b: u8| (a as f32 - b as f32).abs(); - absdiff(a.r, b.r) + absdiff(a.g, b.g) + absdiff(a.b, b.b) +pub fn rgb(a: &RGB8, b: &RGB8) -> f32 { + let absdiff = |a: u8, b: u8| (a as f32 - b as f32).abs(); + absdiff(a.r, b.r) + absdiff(a.g, b.g) + absdiff(a.b, b.b) } // https://en.wikipedia.org/wiki/Color_difference#sRGB +/// a slightly more intelligent algorithm that weighs the channels in an attempt +/// to better align with human color perception. #[inline(always)] -pub fn redmean_difference(a: &RGB8, b: &RGB8) -> f32 { - let delta_r = a.r as f32 - b.r as f32; - let delta_g = a.g as f32 - b.g as f32; - let delta_b = a.b as f32 - b.b as f32; - // reasonably sure calling it prime is wrong, but - let r_prime = 0.5 * (a.r as f32 + b.r as f32); +pub fn redmean(a: &RGB8, b: &RGB8) -> f32 { + let delta_r = a.r as f32 - b.r as f32; + let delta_g = a.g as f32 - b.g as f32; + let delta_b = a.b as f32 - b.b as f32; + // reasonably sure calling it prime is wrong, but + let r_prime = 0.5 * (a.r as f32 + b.r as f32); - let red_part = (2.0 + (r_prime / 256.0)) * (delta_r * delta_r); - let green_part = 4.0 * (delta_g * delta_g); - let blue_part = (2.0 + (255.0 - r_prime) / 256.0) * (delta_b * delta_b); + let red_part = (2.0 + (r_prime / 256.0)) * (delta_r * delta_r); + let green_part = 4.0 * (delta_g * delta_g); + let blue_part = (2.0 + (255.0 - r_prime) / 256.0) * (delta_b * delta_b); - (red_part + green_part + blue_part).sqrt() + (red_part + green_part + blue_part).sqrt() } diff --git a/src/lib.rs b/src/lib.rs index ed3d4e2..9ab6b5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,83 +1,91 @@ -use nih_kmeans::KMeans; -use rgb::RGB8; -use std::collections::HashMap; +use std::collections::HashSet; + +use rgb::{ComponentBytes, FromSlice, RGB8}; pub mod difference; mod nih_kmeans; +pub mod selection; -type DiffFn = dyn Fn(&RGB8, &RGB8) -> f32; +use difference::DiffFn; +use selection::Selector; -pub struct SquasherBuilder<T> { +pub struct SquasherBuilder<T: Count> { max_colours: T, difference_fn: Box<DiffFn>, - tolerance: f32, + selector: Option<Box<dyn Selector + 'static>>, } impl<T: Count> SquasherBuilder<T> { + // I don't want a default here because, to me anyway, Default implies a + // working struct and this would panic build() + #[allow(clippy::new_without_default)] pub fn new() -> Self { - Self::default() + Self { + max_colours: T::zero(), + difference_fn: Box::new(difference::rgb), + selector: None, + } } /// The max number of colors selected for the palette, minus one. /// /// `max_colors(255)` will attempt to make a 256 color palette - pub fn max_colors(mut self, max_minus_one: T) -> SquasherBuilder<T> { + pub fn max_colors(mut self, max_minus_one: T) -> Self { self.max_colours = max_minus_one; self } - /// The function to use to compare colours. + /// The function to use to compare colours while mapping the image. /// - /// see the [difference] module for functions included with the crate. - pub fn difference(mut self, difference: &'static DiffFn) -> SquasherBuilder<T> { + /// see the [difference] module for functions included with the crate and + /// information on implementing your own. + pub fn mapper_difference(mut self, difference: &'static DiffFn) -> Self { self.difference_fn = Box::new(difference); self } - /// Percent colours have to differ by to be included into the palette. - /// between and including 0.0 to 100.0 - pub fn tolerance(mut self, percent: f32) -> SquasherBuilder<T> { - self.tolerance = percent; + pub fn selector(mut self, selector: impl Selector + 'static) -> Self { + self.selector = Some(Box::new(selector)); self } - pub fn build(self, image: &[u8]) -> Squasher<T> { + pub fn build<'a, Img>(self, image: Img) -> Squasher<T> + where + Img: Into<ImageData<'a>>, + { let mut squasher = - Squasher::from_parts(self.max_colours, self.difference_fn, self.tolerance); + Squasher::from_parts(self.max_colours, self.difference_fn, self.selector.unwrap()); squasher.recolor(image); squasher } } -impl<T: Count> Default for SquasherBuilder<T> { - fn default() -> Self { - Self { - max_colours: T::from_usize(255), - difference_fn: Box::new(difference::rgb_difference), - tolerance: 1.0, - } - } -} - pub struct Squasher<T> { // one less than the max colours as you can't have a zero colour image. max_colours_min1: T, palette: Vec<RGB8>, map: Vec<T>, + selector: Box<dyn Selector + 'static>, difference_fn: Box<DiffFn>, - tolerance_percent: f32, } impl<T: Count> Squasher<T> { /// Creates a new squasher and allocates a new color map. A color map /// contains every 24-bit color and ends up with an amount of memory /// equal to `16MB * std::mem::size_of(T)`. - pub fn new(max_colors_minus_one: T, buffer: &[u8]) -> Self { + pub fn new<'a, Img>( + max_colors_minus_one: T, + selector: impl Selector + 'static, + buffer: Img, + ) -> Self + where + Img: Into<ImageData<'a>>, + { let mut this = Self::from_parts( max_colors_minus_one, - Box::new(difference::rgb_difference), - 1.0, + Box::new(difference::rgb), + Box::new(selector), ); this.recolor(buffer); @@ -88,79 +96,75 @@ impl<T: Count> Squasher<T> { SquasherBuilder::new() } - pub fn set_tolerance(&mut self, percent: f32) { - self.tolerance_percent = percent; - } - /// Create a new palette from the colours in the given image. - pub fn recolor(&mut self, image: &[u8]) { - let sorted = Self::unique_and_sort(image); - let selected = self.select_colors(sorted); - self.palette = selected; - } - - /// Create a new palette from the colours in the given image, using the iterative kmeans algorithm with simplified seeding - pub fn recolor_kmeans(&mut self, image: &[u8], max_iter: usize) { - let kmean = KMeans::new( - image - .chunks_exact(3) - .map(|bytes| RGB8::new(bytes[0], bytes[1], bytes[2])) - .collect(), - ); - self.palette = kmean.get_k_colors(self.max_colours_min1.as_usize() + 1, max_iter); + pub fn recolor<'a, Img>(&mut self, image: Img) + where + Img: Into<ImageData<'a>>, + { + self.palette = self + .selector + .select(self.max_colours_min1.as_usize() + 1, image.into()); } /// Create a Squasher from parts. Noteably, this leave your palette empty - fn from_parts(max_colours_min1: T, difference_fn: Box<DiffFn>, tolerance: f32) -> Self { + fn from_parts( + max_colours_min1: T, + difference_fn: Box<DiffFn>, + selector: Box<dyn Selector>, + ) -> Self { Self { max_colours_min1, palette: vec![], map: vec![T::zero(); 256 * 256 * 256], difference_fn, - tolerance_percent: tolerance, + selector, } } /// Take an RGB image buffer and an output buffer. The function will fill /// the output buffer with indexes into the Palette. The output buffer should /// be a third of the size of the image buffer. - pub fn map(&mut self, image: &[u8], buffer: &mut [T]) { - if buffer.len() * 3 < image.len() { - panic!("outout buffer too small to fit indexed image"); + pub fn map<'a, Img>(&mut self, image: Img, buffer: &mut [T]) + where + Img: Into<ImageData<'a>>, + { + let ImageData(rgb) = image.into(); + + if buffer.len() * 3 < rgb.len() { + panic!("output buffer too small to fit indexed image"); } // We have to map the colours of this image now because it might contain // colours not present in the first image. - let sorted = Self::unique_and_sort(image); - self.map_selected(&sorted); - - for (idx, color) in image.chunks(3).enumerate() { - let index = self.map[color_index(&RGB8::new(color[0], color[1], color[2]))]; + let unique = Self::unique_colors(rgb); + self.map_selected(&unique); - buffer[idx] = index; + for (idx, color) in rgb.iter().enumerate() { + buffer[idx] = self.map[color_index(color)]; } } /// Like [Squasher::map] but it doesn't recount the input image. This will /// cause colors the Squasher hasn't seen before to come out as index 0 which - /// may be incorrect. + /// may be incorrect! //TODO: gen- Better name? - pub fn map_unsafe(&self, image: &[u8], buffer: &mut [T]) { - if buffer.len() * 3 < image.len() { - panic!("outout buffer too small to fit indexed image"); + pub fn map_no_recolor<'a, Img>(&self, image: Img, buffer: &mut [T]) + where + Img: Into<ImageData<'a>>, + { + let ImageData(rgb) = image.into(); + + if buffer.len() * 3 < rgb.len() { + panic!("output buffer too small to fit indexed image"); } - for (idx, color) in image.chunks(3).enumerate() { - let index = self.map[color_index(&RGB8::new(color[0], color[1], color[2]))]; - - buffer[idx] = index; + for (idx, color) in rgb.iter().enumerate() { + buffer[idx] = self.map[color_index(color)]; } } #[cfg(feature = "gifed")] pub fn palette_gifed(&self) -> gifed::block::Palette { - use rgb::ComponentBytes; - self.palette.as_slice().as_bytes().try_into().unwrap() } @@ -171,73 +175,12 @@ impl<T: Count> Squasher<T> { /// Retrieve the palette as bytes pub fn palette_bytes(&self) -> Vec<u8> { - self.palette - .clone() - .into_iter() - .flat_map(|rgb| [rgb.r, rgb.g, rgb.b].into_iter()) - .collect() - } - - /// Takes an image buffer of RGB data and fill the color map - fn unique_and_sort(buffer: &[u8]) -> Vec<RGB8> { - let mut colors: HashMap<RGB8, usize> = HashMap::default(); - - //count pixels - for pixel in buffer.chunks(3) { - let rgb = RGB8::new(pixel[0], pixel[1], pixel[2]); - - match colors.get_mut(&rgb) { - None => { - colors.insert(rgb, 1); - } - Some(n) => *n += 1, - } - } - - Self::sort(colors) - } - - fn sort(map: HashMap<RGB8, usize>) -> Vec<RGB8> { - let mut sorted: Vec<(RGB8, usize)> = map.into_iter().collect(); - sorted.sort_by(|(colour1, freq1), (colour2, freq2)| { - freq2 - .cmp(freq1) - .then(colour2.r.cmp(&colour1.r)) - .then(colour2.g.cmp(&colour1.g)) - .then(colour2.b.cmp(&colour1.b)) - }); - - sorted.into_iter().map(|(color, _count)| color).collect() - } - - /// Pick the colors in the palette from a Vec of colors sorted by number - /// of times they occur, high to low. - fn select_colors(&self, sorted: Vec<RGB8>) -> Vec<RGB8> { - // I made these numbers up - #[allow(non_snake_case)] - //let RGB_TOLERANCE: f32 = 0.01 * 765.0; - //let RGB_TOLERANCE: f32 = 36.0; - let tolerance = (self.tolerance_percent / 100.0) * 765.0; - let max_colours = self.max_colours_min1.as_usize() + 1; - let mut selected_colors: Vec<RGB8> = Vec::with_capacity(max_colours); - - for sorted_color in sorted { - if max_colours <= selected_colors.len() { - break; - } else if selected_colors - .iter() - .all(|color| (self.difference_fn)(&sorted_color, color) > tolerance) - { - selected_colors.push(sorted_color); - } - } - - selected_colors + self.palette.as_bytes().to_owned() } /// Pick the closest colour in the palette for each unique color in the image - fn map_selected(&mut self, sorted: &[RGB8]) { - for colour in sorted { + fn map_selected(&mut self, unique: &[RGB8]) { + for colour in unique { let mut min_diff = f32::MAX; let mut min_index = usize::MAX; @@ -253,6 +196,14 @@ impl<T: Count> Squasher<T> { self.map[color_index(colour)] = T::from_usize(min_index); } } + + fn unique_colors(image: &[RGB8]) -> Vec<RGB8> { + let mut unique: HashSet<RGB8> = HashSet::new(); + for px in image { + unique.insert(*px); + } + unique.into_iter().collect() + } } impl Squasher<u8> { @@ -264,8 +215,8 @@ impl Squasher<u8> { pub fn map_over(&mut self, image: &mut [u8]) -> usize { // We have to map the colours of this image now because it might contain // colours not present in the first image. - let sorted = Self::unique_and_sort(image); - self.map_selected(&sorted); + let unique = Self::unique_colors(image.as_rgb()); + self.map_selected(&unique); for idx in 0..(image.len() / 3) { let rgb_idx = idx * 3; @@ -316,6 +267,27 @@ count_impl!(u32); count_impl!(u64); count_impl!(usize); +pub struct ImageData<'a>(&'a [RGB8]); + +impl<'a> From<&'a Vec<u8>> for ImageData<'a> { + fn from(plain: &'a Vec<u8>) -> Self { + ImageData(plain.as_rgb()) + } +} + +impl<'a> From<&'a [u8]> for ImageData<'a> { + fn from(plain: &'a [u8]) -> Self { + ImageData(plain.as_rgb()) + } +} + +impl<'a> From<&'a [RGB8]> for ImageData<'a> { + fn from(rgb: &'a [RGB8]) -> Self { + ImageData(rgb) + } +} + +/// Compute the color index into the big-map-of-all-colours. #[inline(always)] fn color_index(c: &RGB8) -> usize { c.r as usize * (256 * 256) + c.g as usize * 256 + c.b as usize diff --git a/src/selection.rs b/src/selection.rs new file mode 100644 index 0000000..dacd735 --- /dev/null +++ b/src/selection.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; + +#[cfg(feature = "kmeans")] +use crate::nih_kmeans::KMeans; +use rgb::{ComponentBytes, RGB8}; + +use crate::{ + difference::{self, DiffFn}, + ImageData, +}; + +pub trait Selector { + // wanted Into<ImageData> here but rustc got mad about vtable building + // because we store this as Box<dyn Selector> in Squasher and it's builder + fn select(&mut self, max_colors: usize, image: ImageData) -> Vec<RGB8>; +} + +pub struct SortSelect { + tolerance: f32, + difference_fn: Box<DiffFn>, +} + +impl Selector for SortSelect { + /// Pick the colors in the palette from a Vec of colors sorted by number + /// of times they occur, high to low. + fn select(&mut self, max_colours: usize, image: ImageData) -> Vec<RGB8> { + let sorted = Self::unique_and_sort(image); + let tolerance = (self.tolerance / 100.0) * 765.0; + let mut selected_colors: Vec<RGB8> = Vec::with_capacity(max_colours); + + for sorted_color in sorted { + if max_colours <= selected_colors.len() { + break; + } else if selected_colors.iter().all(|selected_color| { + (self.difference_fn)(selected_color, &sorted_color) > tolerance + }) { + selected_colors.push(sorted_color); + } + } + + selected_colors + } +} + +impl SortSelect { + /// How different colours have to be to enter the palette. Should be between + /// 0.0 and 100.0, but is unchecked. + pub fn tolerance(mut self, percent: f32) -> Self { + self.tolerance = percent; + self + } + + /// The function to use to compare colours while selecting the palette. + /// + /// see the [difference] module for functions included with the crate and + /// information on implementing your own. + pub fn difference(mut self, diff_fn: &'static DiffFn) -> Self { + self.difference_fn = Box::new(diff_fn); + self + } + + /// Takes an image buffer of RGB data and fill the color map + fn unique_and_sort<'a, Img>(buffer: Img) -> Vec<RGB8> + where + Img: Into<ImageData<'a>>, + { + let ImageData(rgb) = buffer.into(); + let mut colors: HashMap<RGB8, usize> = HashMap::default(); + + //count pixels + for px in rgb { + match colors.get_mut(px) { + None => { + colors.insert(*px, 1); + } + Some(n) => *n += 1, + } + } + + Self::sort(colors) + } + + fn sort(map: HashMap<RGB8, usize>) -> Vec<RGB8> { + let mut sorted: Vec<(RGB8, usize)> = map.into_iter().collect(); + sorted.sort_by(|(colour1, freq1), (colour2, freq2)| { + freq2 + .cmp(freq1) + .then(colour2.r.cmp(&colour1.r)) + .then(colour2.g.cmp(&colour1.g)) + .then(colour2.b.cmp(&colour1.b)) + }); + + sorted.into_iter().map(|(color, _count)| color).collect() + } +} + +impl Default for SortSelect { + fn default() -> Self { + Self { + tolerance: 3.0, + difference_fn: Box::new(difference::rgb), + } + } +} + +#[cfg(feature = "kmeans")] +#[derive(Debug, Default)] +pub struct Kmeans; + +#[cfg(feature = "kmeans")] +impl Selector for Kmeans { + fn select(&mut self, max_colors: usize, image: ImageData) -> Vec<RGB8> { + let ImageData(rgb) = image; + + let kmean = KMeans::new(rgb.to_vec()); + kmean.get_k_colors(max_colors, max_iter) + } +} |