about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml4
-rw-r--r--README.md6
-rw-r--r--squash/Cargo.toml4
-rw-r--r--squash/src/cli.rs51
-rw-r--r--squash/src/main.rs25
-rw-r--r--src/difference.rs54
-rw-r--r--src/lib.rs246
-rw-r--r--src/selection.rs118
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)
+	}
+}