Attempt to implement nearest colors in different color spaces.
This commit is contained in:
parent
e7ac178f7f
commit
34f1924c74
23
Cargo.lock
generated
23
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
231
src/lib.rs
231
src/lib.rs
@ -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 {
|
||||
},
|
||||
}
|
||||
|
||||
/// 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],
|
||||
]
|
||||
}
|
||||
|
||||
/// 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 NearestColor {
|
||||
pub struct NearestColorCIELAB {
|
||||
palette: Vec<Rgba<u8>>,
|
||||
color_cache: HashMap<Rgba<u8>, usize>,
|
||||
cielab: Vec<[f32; 3]>,
|
||||
}
|
||||
|
||||
impl NearestColor {
|
||||
pub fn new(palette: Vec<Rgba<u8>>) -> NearestColor {
|
||||
NearestColor {
|
||||
palette,
|
||||
color_cache: HashMap::new(),
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user