Attempt to implement nearest colors in different color spaces.

This commit is contained in:
Bill Thiede 2021-04-29 20:36:31 -07:00
parent e7ac178f7f
commit 34f1924c74
4 changed files with 300 additions and 27 deletions

23
Cargo.lock generated
View File

@ -157,13 +157,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
[[package]]
name = "gif"
version = "0.10.3"
name = "float-ord"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af"
checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
[[package]]
name = "gif"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02efba560f227847cb41463a7395c514d127d4f74fff12ef0137fff1b84b96c4"
dependencies = [
"color_quant",
"lzw",
"weezl",
]
[[package]]
@ -186,7 +192,7 @@ dependencies = [
[[package]]
name = "image"
version = "0.23.6"
version = "0.23.10"
dependencies = [
"bytemuck",
"byteorder",
@ -317,6 +323,7 @@ name = "perler"
version = "0.1.0"
dependencies = [
"anyhow",
"float-ord",
"image",
"lazy_static",
"log",
@ -547,6 +554,12 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "weezl"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2bb9fc8309084dd7cd651336673844c1d47f8ef6d2091ec160b27f5c4aa277"
[[package]]
name = "winapi"
version = "0.3.8"

View File

@ -16,6 +16,7 @@ thiserror = "1.0.20"
anyhow = "1.0.31"
#x11colors = {git="https://git.z.xinu.tv/wathiede/x11colors"}
x11colors = {path="../../xinu.tv/x11colors"}
float-ord = "0.2.0"
[dev-dependencies]
lazy_static = "1.4.0"

View File

@ -4,16 +4,29 @@ use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use float_ord::FloatOrd;
use image::imageops::colorops::{index_colors, ColorMap};
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, Rgba};
use structopt::StructOpt;
use x11colors::COLORS;
use perler::NearestColor;
use perler::{rgba_to_hsv, NearestColorCIELAB, NearestColorHSV, NearestColorRGB};
#[derive(Debug, StructOpt)]
enum Command {
Nearest {
NearestRGB {
/// Input image file.
input: PathBuf,
/// Output of processed image.
output: PathBuf,
},
NearestHSV {
/// Input image file.
input: PathBuf,
/// Output of processed image.
output: PathBuf,
},
NearestCIELAB {
/// Input image file.
input: PathBuf,
/// Output of processed image.
@ -23,6 +36,10 @@ enum Command {
/// Output of processed image.
output: PathBuf,
},
GeneratePalette {
/// Output of processed image.
output: PathBuf,
},
}
/// Convert image to SVG in the style of perler instructions.
@ -76,6 +93,27 @@ fn print_stats(img: &DynamicImage) {
}
}
fn generate_palette(output: PathBuf) -> Result<()> {
let w = 256;
let stripe_h = 8;
let h = COLORS.len() as u32 * stripe_h;
let mut img = DynamicImage::new_rgba8(w, h);
let mut colors: Vec<_> = COLORS.iter().collect();
colors.sort_by_key(|[r, g, b]| {
let [h, s, v] = rgba_to_hsv(&[*r, *g, *b, 255].into());
[FloatOrd(s), FloatOrd(h), FloatOrd(v)]
});
for (y, c) in colors.iter().enumerate() {
for x in 0..w {
for s in 0..stripe_h {
img.put_pixel(x, stripe_h * y as u32 + s, [c[0], c[1], c[2], 255].into());
}
}
}
img.save(output)?;
Ok(())
}
fn generate(output: PathBuf) -> Result<()> {
let w = 256;
let h = 300;
@ -94,7 +132,13 @@ fn generate(output: PathBuf) -> Result<()> {
Ok(())
}
fn nearest(input: PathBuf, output: PathBuf) -> Result<()> {
enum NearestType {
RGB,
HSV,
CIELAB,
}
fn nearest(input: PathBuf, output: PathBuf, n_type: NearestType) -> Result<()> {
let img = image::open(input)?;
let (w, h) = &img.dimensions();
println!("Before:");
@ -106,9 +150,16 @@ fn nearest(input: PathBuf, output: PathBuf) -> Result<()> {
.cloned()
.map(|[r, g, b]| [r, g, b, 255].into())
.collect();
palette.sort_by_key(|Rgba([r, g, b, a])| r << 24 | g << 16 | b << 8 | a);
let cmap = NearestColor::new(palette);
let pal = index_colors(&mut img2, &cmap);
palette.sort_by_key(|Rgba([r, g, b, a])| {
(*r as u32) << 24 | (*g as u32) << 16 | (*b as u32) << 8 | *a as u32
});
let cmap: Box<dyn ColorMap<Color = Rgba<u8>>> = match n_type {
NearestType::RGB => Box::new(NearestColorRGB::new(palette)),
NearestType::HSV => Box::new(NearestColorHSV::new(palette)),
NearestType::CIELAB => Box::new(NearestColorCIELAB::new(palette)),
};
let pal = index_colors(&mut img2, cmap.as_ref());
let out_buf = ImageBuffer::from_fn(*w, *h, |x, y| -> Rgba<u8> {
let p = pal.get_pixel(x, y);
cmap.lookup(p.0[0] as usize)
@ -128,7 +179,10 @@ fn main() -> Result<()> {
let opt = Opt::from_args();
match opt.cmd {
Command::Nearest { input, output } => nearest(input, output),
Command::NearestRGB { input, output } => nearest(input, output, NearestType::RGB),
Command::NearestHSV { input, output } => nearest(input, output, NearestType::HSV),
Command::NearestCIELAB { input, output } => nearest(input, output, NearestType::CIELAB),
Command::Generate { output } => generate(output),
Command::GeneratePalette { output } => generate_palette(output),
}
}

View File

@ -1,8 +1,7 @@
use std::collections::HashMap;
use float_ord::FloatOrd;
use image::imageops::colorops::ColorMap;
use image::Rgba;
use image::{DynamicImage, GenericImage, GenericImageView};
use image::{Rgb, Rgba};
use log::debug;
use svg::node::element::Group;
use svg::node::element::{Circle, Rectangle};
@ -21,22 +20,187 @@ pub enum PerlerError {
},
}
#[derive(Default, Debug)]
pub struct NearestColor {
palette: Vec<Rgba<u8>>,
color_cache: HashMap<Rgba<u8>, usize>,
/// Convert tristimulus values in the XYZ color space (as defined by CIE) matching the human eye's
/// response to RGB values in the sRGB color space.
/// Values are nominally in the range [0..1].
pub fn xyz_to_rgb(xyz: [f32; 3]) -> [f32; 3] {
[
3.240479 * xyz[0] - 1.537150 * xyz[1] - 0.498535 * xyz[2],
-0.969256 * xyz[0] + 1.875991 * xyz[1] + 0.041556 * xyz[2],
0.055648 * xyz[0] - 0.204043 * xyz[1] + 1.057311 * xyz[2],
]
}
impl NearestColor {
pub fn new(palette: Vec<Rgba<u8>>) -> NearestColor {
NearestColor {
palette,
color_cache: HashMap::new(),
}
/// From https://en.wikipedia.org/wiki/CIELAB_color_space#CIELAB%E2%80%93CIEXYZ_conversions
pub fn xyz_to_cielab(xyz: [f32; 3]) -> [f32; 3] {
todo!();
}
/// From https://en.wikipedia.org/wiki/CIELAB_color_space#CIELAB%E2%80%93CIEXYZ_conversions
pub fn cielab_to_xyz(cielab: [f32; 3]) -> [f32; 3] {
todo!();
}
/// Convert tristimulus values in the sRGB color space values to the XYZ color space (as defined by
/// CIE) matching the human eye's response.
/// Values are nominally in the range [0..1].
pub fn rgb_to_xyz(rgb: [f32; 3]) -> [f32; 3] {
[
0.412453 * rgb[0] + 0.357580 * rgb[1] + 0.180423 * rgb[2],
0.212671 * rgb[0] + 0.715160 * rgb[1] + 0.072169 * rgb[2],
0.019334 * rgb[0] + 0.119193 * rgb[1] + 0.950227 * rgb[2],
]
}
// From https://www.rapidtables.com/convert/color/rgb-to-hsv.html
pub fn rgba_to_hsv(rgb: &Rgba<u8>) -> [f32; 3] {
let [r, g, b, _] = rgb.0;
let [f_r, f_g, f_b] = [r as f32 / 255., g as f32 / 255., b as f32 / 255.];
let c_max = *[r, g, b].iter().max().unwrap();
let f_c_max = c_max as f32 / 255.;
let c_min = *[r, g, b].iter().min().unwrap();
let f_c_min = c_min as f32 / 255.;
let delta = c_max - c_min;
let f_delta = f_c_max - f_c_min;
//dbg!((r, g, b, c_max, c_min, delta));
//dbg!((f_r, f_g, f_b, f_c_max, f_c_min, f_delta));
let mut h = if delta == 0 {
0.
} else if c_max == r {
60. * (((f_g - f_b) / f_delta) % 6.)
} else if c_max == g {
60. * ((f_b - f_r) / f_delta + 2.)
} else if c_max == b {
60. * ((f_r - f_g) / f_delta + 4.)
} else {
unreachable!("c_max value not equal to r, g, or b");
};
if h < 0. {
h += 360.;
};
let s = match c_max {
0 => 0.,
_ => f_delta / f_c_max,
};
let v = f_c_max;
//dbg!(h, s, v);
[h / 360., s, v]
}
// HSV values in [0..1]
// returns [r, g, b] values from 0 to 255
//From https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Rgb<u8> {
let h_i = (h * 6.) as i32;
let f = h * 6. - h_i as f32;
let p = v * (1. - s);
let q = v * (1. - f * s);
let t = v * (1. - (1. - f) * s);
let [r, g, b] = match h_i {
0 => [v, t, p],
1 => [q, v, p],
2 => [p, v, t],
3 => [p, q, v],
4 => [t, p, v],
5 => [v, p, q],
_ => panic!(format!("Unknown H value {}", h_i)),
};
[(r * 255.) as u8, (g * 255.) as u8, (b * 255.) as u8].into()
}
#[derive(Default, Debug)]
pub struct NearestColorCIELAB {
palette: Vec<Rgba<u8>>,
cielab: Vec<[f32; 3]>,
}
impl NearestColorCIELAB {
pub fn new(palette: Vec<Rgba<u8>>) -> NearestColorCIELAB {
let cielab = palette
.iter()
.map(|rgba| {
let [r, g, b, _] = rgba.0;
xyz_to_cielab(rgb_to_xyz([
r as f32 / 255.,
g as f32 / 255.,
b as f32 / 255.,
]))
})
.collect();
NearestColorCIELAB { palette, cielab }
}
}
impl ColorMap for NearestColor {
impl ColorMap for NearestColorCIELAB {
type Color = Rgba<u8>;
fn index_of(&self, color: &Self::Color) -> usize {
todo!();
}
fn map_color(&self, color: &mut Self::Color) {
*color = self.palette[self.index_of(color)];
}
fn lookup(&self, idx: usize) -> Option<Self::Color> {
self.palette.get(idx).cloned()
}
}
#[derive(Default, Debug)]
pub struct NearestColorHSV {
palette: Vec<Rgba<u8>>,
hsv: Vec<[f32; 3]>,
}
impl NearestColorHSV {
pub fn new(palette: Vec<Rgba<u8>>) -> NearestColorHSV {
let hsv = palette.iter().map(|rgba| rgba_to_hsv(rgba)).collect();
NearestColorHSV { palette, hsv }
}
}
impl ColorMap for NearestColorHSV {
type Color = Rgba<u8>;
fn index_of(&self, color: &Self::Color) -> usize {
let c = rgba_to_hsv(color);
let mut min_idx = usize::MAX;
let mut min_dist = f32::MAX;
for (idx, hsv) in self.hsv.iter().enumerate() {
let [h1, s1, v1] = hsv;
let [h2, s2, v2] = c;
//dbg!((h1, s1, v1), (h2, s2, v2));
let dist = (h1 - h2).abs().min(1. - (h1 - h2).abs());
//dbg!((dist, min_dist, idx, min_idx));
if dist < min_dist {
min_dist = dist;
min_idx = idx;
}
}
min_idx
}
fn map_color(&self, color: &mut Self::Color) {
*color = self.palette[self.index_of(color)];
}
fn lookup(&self, idx: usize) -> Option<Self::Color> {
self.palette.get(idx).cloned()
}
}
#[derive(Default, Debug)]
pub struct NearestColorRGB {
palette: Vec<Rgba<u8>>,
}
impl NearestColorRGB {
pub fn new(palette: Vec<Rgba<u8>>) -> NearestColorRGB {
NearestColorRGB { palette }
}
}
impl ColorMap for NearestColorRGB {
type Color = Rgba<u8>;
fn index_of(&self, color: &Self::Color) -> usize {
@ -334,11 +498,52 @@ mod tests {
.into_iter()
.map(|rgb| rgb.into())
.collect();
let cmap = NearestColor::new(palette);
let cmap = NearestColorRGB::new(palette);
let got = cmap.index_of(&[255, 10, 10, 255].into());
assert_eq!(got, 2);
let got = cmap.lookup(2);
assert_eq!(got, Some([255, 0, 0, 255].into()));
}
const COLOR_DATA: [(&str, [u8; 3], [f32; 3]); 16] = [
// Name, [R,G,B], [H,S,V]
("Black", [0, 0, 0], [0., 0., 0.]),
("White", [255, 255, 255], [0., 0., 1.]),
("Red", [255, 0, 0], [0., 1., 1.]),
("Lime", [0, 255, 0], [120., 1., 1.]),
("Blue", [0, 0, 255], [240., 1., 1.]),
("Yellow", [255, 255, 0], [60., 1., 1.]),
("Cyan", [0, 255, 255], [180., 1., 1.]),
("Magenta", [255, 0, 255], [300., 1., 1.]),
("Silver", [191, 191, 191], [0., 0., 0.7490196]),
("Gray", [128, 128, 128], [0., 0., 0.5019608]),
("Maroon", [128, 0, 0], [0., 1., 0.5019608]),
("Olive", [128, 128, 0], [60., 1., 0.5019608]),
("Green", [0, 128, 0], [120., 1., 0.5019608]),
("Purple", [128, 0, 128], [300., 1., 0.5019608]),
("Teal", [0, 128, 128], [180., 1., 0.5019608]),
("Navy", [0, 0, 128], [240., 1., 0.5019608]),
];
#[test]
fn color_roundtrip() {
for (name, rgb, hsv) in COLOR_DATA.iter() {
let rgba: Rgba<u8> = [rgb[0], rgb[1], rgb[2], 255].into();
let got = rgba_to_hsv(&rgba);
let [h, s, v] = *hsv;
assert_eq!(
got,
[h / 360., s, v],
"rgba_to_hsv failed for {} ({:?})",
name,
rgb
);
let rgb: Rgb<u8> = Rgb(*rgb);
let [h, s, v] = *hsv;
let got = hsv_to_rgb(h / 360., s, v);
assert_eq!(got, rgb, "hsv_to_rgb failed for {} ({:?})", name, hsv);
}
}
}