Compare commits
8 Commits
58e9e91348
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4edaa2ba03 | |||
| 00f059108a | |||
| 9286255662 | |||
| e3895898ae | |||
| 14093f7a61 | |||
| 9f722b1040 | |||
| 68313eaba9 | |||
| cb92a56b20 |
1293
Cargo.lock
generated
Normal file
1293
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "imagepicker"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.6.*", features = ["derive"] }
|
||||
dotenv = "0.15.0"
|
||||
image = {version = "0.25.10", features = ["png"]}
|
||||
log = "0.4.29"
|
||||
pretty_env_logger = "0.5.0"
|
||||
rand = "0.10.1"
|
||||
74
src/cli.rs
Normal file
74
src/cli.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::fmt::Debug;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use clap::Parser;
|
||||
use image::{DynamicImage, ImageReader, Rgb};
|
||||
|
||||
use crate::Colour;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "imagePicker")]
|
||||
#[command(version = "1.0")]
|
||||
#[command(about = "Matches colours to their closest equivalents in a given image", long_about = None)]
|
||||
/// Matches the colours in one image to either a list of provided colours or a different image.
|
||||
///
|
||||
/// For each provided colour (either explicitly or in the second image), finds the closest colour
|
||||
/// from the first image, using redmean distance.
|
||||
pub struct Cli {
|
||||
/// The source image from which colours should be picked.
|
||||
#[arg(value_parser = valid_image_file)]
|
||||
pub image: DynamicImage,
|
||||
|
||||
/// Optional second image; if used, all colours from this image will be matched to their closest
|
||||
/// equivalent in the first image.
|
||||
#[arg(short = 'i', long = "image", value_parser = valid_image_file)]
|
||||
pub second_image: Option<DynamicImage>,
|
||||
|
||||
/// Optional list of hex values; if used, will match each value to their closest equivalent in
|
||||
/// the first image
|
||||
///
|
||||
/// Should provide valid hex codes only: '24274a', and '#cad3f5' are both valid.
|
||||
#[arg(short = 'c', long = "colours", value_parser = valid_hex_code)]
|
||||
pub colours: Option<Vec<Colour>>,
|
||||
}
|
||||
|
||||
impl Debug for Cli {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Cli")
|
||||
.field("image", &self.image)
|
||||
.field("image_two", &self.second_image)
|
||||
.field("colours", &self.colours)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that a given string represents a hex code for a RGB colour.
|
||||
///
|
||||
/// Optionally accepts a leading '#'.
|
||||
fn valid_hex_code(s: &str) -> Result<Colour, String> {
|
||||
// accept leading '#' for hex values
|
||||
let start = match s.chars().nth(0) == Some('#') {
|
||||
true => 1,
|
||||
false => 0,
|
||||
};
|
||||
|
||||
let u8s: Vec<u8> = (start..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
|
||||
.collect::<Result<Vec<u8>, ParseIntError>>()
|
||||
.map_err(|e| format!("{}", e))?;
|
||||
let rgb: [u8; 3] = u8s
|
||||
.try_into()
|
||||
.map_err(|_| <&str as Into<String>>::into("Hex string must decode to exactly 3 bytes"))?;
|
||||
Ok(Rgb(rgb))
|
||||
}
|
||||
|
||||
/// Validates that agiven string points to a valid image file.
|
||||
fn valid_image_file(s: &str) -> Result<DynamicImage, String> {
|
||||
ImageReader::open(s)
|
||||
.map_err(|e| format!("{}", e))?
|
||||
.with_guessed_format()
|
||||
.map_err(|e| format!("{}", e))?
|
||||
.decode()
|
||||
.map_err(|e| format!("{}", e))
|
||||
}
|
||||
141
src/colours.rs
Normal file
141
src/colours.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use image::{DynamicImage, GenericImageView, Rgb};
|
||||
use log::info;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::encode_hex;
|
||||
|
||||
/// This type is used to efficiently store all colour values from an image.
|
||||
/// By using nested hashmaps we can save on space for colours with identical r and g values.
|
||||
/// Additionally stores a Vec of points to remember which colour maps to which pixel(s).
|
||||
pub type ColourMap = HashMap<u8, HashMap<u8, HashMap<u8, Vec<Point>>>>;
|
||||
|
||||
/// We use one colour type.
|
||||
pub type Colour = Rgb<u8>;
|
||||
|
||||
/// Struct to represent a point in an image.
|
||||
#[derive(Clone)]
|
||||
pub struct Point {
|
||||
x: u32,
|
||||
y: u32,
|
||||
}
|
||||
|
||||
impl Debug for Point {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!("{}, {}", &self.x, &self.y))
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct to save found matches for colours.
|
||||
/// includes the matched colour, and a list of pixels with that colour.
|
||||
pub struct Match {
|
||||
/// matched colour
|
||||
pub colour: Colour,
|
||||
/// list of pixels with the matching colour
|
||||
pub positions: Vec<Point>,
|
||||
}
|
||||
|
||||
/// Extract all colours from a provided image into a nested ColourMap
|
||||
pub fn extract_colours(img: DynamicImage) -> ColourMap {
|
||||
let mut out: ColourMap = HashMap::new();
|
||||
for (x, y, p) in img.pixels() {
|
||||
out.entry(p[0])
|
||||
.or_insert_with(HashMap::new)
|
||||
.entry(p[1])
|
||||
.or_insert_with(HashMap::new)
|
||||
.entry(p[2])
|
||||
.or_insert_with(Vec::new)
|
||||
.push(Point { x, y });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Extract all colours in a provided image into a flat hashmap.
|
||||
fn extract_colours_set(img: DynamicImage) -> HashSet<Rgb<u8>> {
|
||||
let mut out = HashSet::new();
|
||||
img.pixels().for_each(|(_x, _y, p)| {
|
||||
out.insert(p);
|
||||
});
|
||||
out.into_iter().map(|c| Rgb([c[0], c[1], c[2]])).collect()
|
||||
}
|
||||
|
||||
/// Finds the "closest" match to a colour in the provided map. Uses redmean distance.
|
||||
///
|
||||
/// arguments:
|
||||
/// colour: Colour which will be matched against all.
|
||||
/// all: Map of Colours to match against.
|
||||
fn closest_colour(colour: &Colour, all: &ColourMap) -> Option<Match> {
|
||||
// use redmean to calculate the distance between colours
|
||||
let mut closest: Option<Match> = None;
|
||||
let mut dist: f32 = f32::MAX;
|
||||
|
||||
let (colour_r, colour_g, colour_b) = (colour[0] as f32, colour[1] as f32, colour[2] as f32);
|
||||
for (r, gbs) in all.iter() {
|
||||
// iterate over all reds
|
||||
let r_bar = 0.5 * (*r as f32 + colour_r);
|
||||
let r_squared = (*r as f32 - colour_r) * (*r as f32 - colour_r);
|
||||
for (g, bs) in gbs.iter() {
|
||||
// iterate over all greens
|
||||
let g_squared = (*g as f32 - colour_g) * (*g as f32 - colour_g);
|
||||
for (b, vec) in bs.iter() {
|
||||
// iterate over all blues
|
||||
let b_squared = (*b as f32 - colour_b) * (*b as f32 - colour_b);
|
||||
let delta = (2.0 + r_bar / 256.0) * r_squared
|
||||
+ 4.0 * g_squared
|
||||
+ (2.0 + (255.0 - r_bar) / 256.0) * b_squared;
|
||||
if delta < dist {
|
||||
// update closest if better match found
|
||||
dist = delta;
|
||||
closest = Some(Match {
|
||||
colour: Rgb([*r, *g, *b]),
|
||||
positions: vec.to_vec(), // FIXME (low priority): would be more efficient if
|
||||
// i can make these a borrow instead, but that
|
||||
// leads to lifetime issues
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
/// Maps a list of colours to the closest equivalents in the provided map.
|
||||
///
|
||||
/// arguments:
|
||||
/// all_cols: Map of colours match against.
|
||||
/// cols: Vec of Colours which will be matched against all_cols.
|
||||
///
|
||||
/// returns: Hashmap of colour, match pairs.
|
||||
pub fn map_image_list(all_cols: &ColourMap, cols: Vec<Colour>) -> HashMap<Colour, Match> {
|
||||
let mut output_cols: HashMap<Colour, Match> = HashMap::new();
|
||||
|
||||
for c in cols {
|
||||
info!("Matching colour {}", encode_hex(&c));
|
||||
if let Some(col) = closest_colour(&c, &all_cols) {
|
||||
output_cols.insert(c, col);
|
||||
}
|
||||
}
|
||||
output_cols
|
||||
}
|
||||
|
||||
/// Maps the colours of one image to the closest equivalents in the provided map.
|
||||
///
|
||||
/// arguments:
|
||||
/// img_1_cols: Map of colours to match against.
|
||||
/// img_2: DynamicImage for which each colour will be matched against img_1_cols.
|
||||
///
|
||||
/// returns: Hashmap of colour, match pairs.
|
||||
pub fn map_image_image(img_1_cols: &ColourMap, img_2: DynamicImage) -> HashMap<Colour, Match> {
|
||||
let img_2_cols = extract_colours_set(img_2);
|
||||
|
||||
let mut output_cols: HashMap<Colour, Match> = HashMap::new();
|
||||
img_2_cols.iter().for_each(|c| {
|
||||
info!("Matching colour {}", encode_hex(&c));
|
||||
if let Some(col) = closest_colour(c, &img_1_cols) {
|
||||
output_cols.insert(*c, col);
|
||||
}
|
||||
});
|
||||
|
||||
output_cols
|
||||
}
|
||||
46
src/main.rs
Normal file
46
src/main.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
mod cli;
|
||||
mod colours;
|
||||
|
||||
use clap::Parser;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dotenv::dotenv;
|
||||
|
||||
use crate::cli::Cli;
|
||||
use crate::colours::{Colour, Match, extract_colours, map_image_image, map_image_list};
|
||||
|
||||
/// Encodes a RGB colour as a hex code.
|
||||
fn encode_hex(c: &Colour) -> String {
|
||||
format!("#{:02x}{:02x}{:02x}", c[0], c[1], c[2])
|
||||
}
|
||||
|
||||
/// Formats found matches for CLI otuput.
|
||||
fn pretty_print(h: HashMap<Colour, Match>) {
|
||||
h.iter().for_each(|(k, v)| {
|
||||
println!(
|
||||
"{}: {} ({:?})",
|
||||
encode_hex(k),
|
||||
encode_hex(&v.colour),
|
||||
v.positions
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Driver code.
|
||||
fn main() {
|
||||
dotenv().ok();
|
||||
pretty_env_logger::init();
|
||||
let cli = Cli::parse();
|
||||
|
||||
let set = extract_colours(cli.image);
|
||||
|
||||
if let Some(cs) = cli.colours {
|
||||
let colours = map_image_list(&set, cs);
|
||||
pretty_print(colours);
|
||||
}
|
||||
|
||||
if let Some(i) = cli.second_image {
|
||||
let colours = map_image_image(&set, i);
|
||||
pretty_print(colours);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user