Closed lunacookies closed 6 months ago
It's definitely interesting! Thanks for bringing it up. Porting the Python implementation seems fine to me, but it will of course have a few more bells and whistles to be in line with the rest of the library. Feel free to give it a try if you want to. A lot of these things are mechanical copies of implementations or specifications, so it wouldn't be the first. Don't hesitate to ask if you decide to try it and run into trouble.
The link has moved to https://github.com/nschloe/colorio/blob/main/colorio/cs/_cam16.py.
The "bells and whistles" can be at least initially reduced by always using the CAM16-UCS viewing conditions. Implementing CAM16 would also contribute to the CAM02 issue, since a lot of code is shared between them.
Thanks for the heads up! And yeah, it would be nice if code can be shared there.
I implemented a color crate, without knowing about "palette" and implemented different color spaces rgb, linear rgb, XYZ... and also color appearance model: Lab, Cam16 and Android HTC for a project of my business. I did it in one week so it has the quality of a crate written in one week. Palette is really superior.
I would prefer to switch to palette but I need Cam16 and HTC. Are you still interested in the implementation of Cam16 in Palette?
I have seen your code, there are cryptic macros "palette_internal". If you explain me I can try to implement Cam16 otherwise, here is an implementation:
#![allow(non_snake_case)]
use super::{ChromaticAdaptation, ViewingCondition as SpecViewingCondition};
use crate::cam::{ColourAppearanceModel, ColourAppearanceTransform};
use crate::color_spaces::AbsoluteWhite;
use crate::color_spaces::{
ColorModel, Illuminant, LinearColorSpacePrimaries, WhitePoint, RELATIVE_TRISTIMULUS_SCALE,
};
use crate::illuminant::E;
use crate::rgb::TristimulusRgb;
use crate::utils::{angle_degree, to_rad};
use crate::xyz::{ChromaticityXy, RelativeXYZ};
use nalgebra::{matrix, vector, Matrix3, Vector3};
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
struct Cam16ColorSpace;
#[derive(Copy, Clone, Debug)]
pub struct Cam16 {
pub hue: f32,
pub lightness: f32,
pub brightness: f32,
pub chroma: f32,
pub colorfulness: f32,
pub saturation: f32,
}
impl_eq_hash! {Cam16, floats(
hue,
lightness,
brightness,
chroma,
colorfulness,
saturation
)}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ViewingCondition {
D_rgb: Vector3<f32>,
Fl: f32,
Nbb: f32,
Aw: f32,
c: f32,
z: f32,
Nc: f32,
n: f32,
}
#[derive(Copy, Clone, Debug)]
pub struct Cam16Hcl {
pub hue: f32,
pub colorfulness: f32,
pub lightness: f32,
}
impl_eq_hash! {Cam16Hcl, floats(
hue,
colorfulness,
lightness
)}
#[derive(Copy, Clone, Debug)]
pub struct Cam16Jab {
pub lightness: f32,
pub a: f32,
pub b: f32,
}
impl_eq_hash! {Cam16Jab, floats(
lightness,
a,
b
)}
impl From<Cam16> for Cam16Hcl {
fn from(v: Cam16) -> Self {
let J = v.lightness;
let M = v.colorfulness;
let h = v.hue;
Self {
lightness: 1.7 * J / (1. + 0.007 * J),
colorfulness: (0.0228 * M).ln_1p() / 0.0228,
hue: h,
}
}
}
impl From<Cam16Hcl> for Cam16Jab {
fn from(v: Cam16Hcl) -> Self {
let M = v.colorfulness;
let h = v.hue;
let a = M * to_rad(h).cos();
let b = M * to_rad(h).sin();
Self {
lightness: v.lightness,
a,
b,
}
}
}
impl From<Cam16Jab> for Cam16Hcl {
fn from(v: Cam16Jab) -> Self {
let M = v.a.hypot(v.b);
let h = angle_degree(v.b, v.a);
Self {
lightness: v.lightness,
colorfulness: M,
hue: h,
}
}
}
impl From<Cam16> for Cam16Jab {
fn from(v: Cam16) -> Cam16Jab {
let inter: Cam16Hcl = v.into();
inter.into()
}
}
impl ColourAppearanceModel for Cam16Jab {
type ViewingCondition = ViewingCondition;
fn to_tristimulus_in<T: AbsoluteWhite>(
&self,
condition: &Self::ViewingCondition,
) -> RelativeXYZ<T> {
Cam16Hcl::to_tristimulus_in(&(*self).into(), condition)
}
fn from_tristimulus_in<T: AbsoluteWhite>(
c: RelativeXYZ<T>,
condition: &Self::ViewingCondition,
) -> Self {
Cam16::from_tristimulus_in(c, condition).into()
}
fn distance(&self, other: &Self) -> f32 {
let dj = self.lightness - other.lightness;
let da = self.a - other.a;
let db = self.b - other.b;
let de = (dj * dj + da * da + db * db).sqrt();
1.41 * de.powf(0.63)
}
fn grey_of_lightness(lightness_scale: f32, _condition: &Self::ViewingCondition) -> Self {
Self {
lightness: lightness_scale,
a: 0.,
b: 0.,
}
}
fn lightness_scale(&self) -> f32 {
self.lightness
}
fn color_scale(&self) -> f32 {
self.a.hypot(self.b)
}
fn hue(&self) -> f32 {
angle_degree(self.b, self.a)
}
fn set_lightness_scale(&mut self, lightness_scale: f32) -> &mut Self {
self.lightness = lightness_scale;
self
}
fn set_color_scale(&mut self, color_scale: f32) -> &mut Self {
let cur = self.color_scale();
if color_scale != cur {
assert!(
cur != 0.,
"cannot give back color to grey Cam16Jab color appearance"
);
let coef = color_scale / cur;
self.a *= coef;
self.b *= coef;
}
self
}
fn set_hue(&mut self, hue: f32) -> &mut Self {
let c = self.color_scale();
self.a = c * to_rad(hue).cos();
self.b = c * to_rad(hue).sin();
self
}
fn from_hue_color_lightness_scales(
hue: f32,
color_scale: f32,
lightness_scale: f32,
_: &ViewingCondition,
) -> Self {
let a = color_scale * to_rad(hue).cos();
let b = color_scale * to_rad(hue).sin();
Self {
lightness: lightness_scale,
a,
b,
}
}
}
impl ColourAppearanceModel for Cam16Hcl {
type ViewingCondition = ViewingCondition;
fn to_tristimulus_in<T: AbsoluteWhite>(
&self,
condition: &Self::ViewingCondition,
) -> RelativeXYZ<T> {
let J = 142.86 * self.lightness / (242.86 - self.lightness);
let M = (self.colorfulness * 0.0228).exp_m1() / 0.0228;
let C = M / condition.Fl.powf(0.25);
let h = self.hue;
appearance_to_tristimulus(J, C, h, condition)
}
fn from_tristimulus_in<T: AbsoluteWhite>(
c: RelativeXYZ<T>,
condition: &Self::ViewingCondition,
) -> Self {
Cam16::from_tristimulus_in(c, condition).into()
}
fn distance(&self, other: &Self) -> f32 {
let s: Cam16Jab = (*self).into();
let o: Cam16Jab = (*other).into();
s.distance(&o)
}
fn grey_of_lightness(lightness_scale: f32, _condition: &Self::ViewingCondition) -> Self {
Self {
lightness: lightness_scale,
hue: 0.,
colorfulness: 0.,
}
}
fn lightness_scale(&self) -> f32 {
self.lightness
}
fn color_scale(&self) -> f32 {
self.colorfulness
}
fn hue(&self) -> f32 {
self.hue
}
fn set_lightness_scale(&mut self, lightness_scale: f32) -> &mut Self {
self.lightness = lightness_scale;
self
}
fn set_color_scale(&mut self, color_scale: f32) -> &mut Self {
self.colorfulness = color_scale;
self
}
fn set_hue(&mut self, hue: f32) -> &mut Self {
self.hue = hue;
self
}
fn from_hue_color_lightness_scales(
hue: f32,
color_scale: f32,
lightness_scale: f32,
_: &ViewingCondition,
) -> Self {
Self {
hue,
lightness: lightness_scale,
colorfulness: color_scale,
}
}
}
impl ColourAppearanceModel for Cam16 {
type ViewingCondition = ViewingCondition;
fn to_tristimulus_in<T: AbsoluteWhite>(
&self,
condition: &Self::ViewingCondition,
) -> RelativeXYZ<T> {
let J = self.lightness;
let C = self.chroma;
let h = self.hue;
appearance_to_tristimulus(J, C, h, condition)
}
fn from_tristimulus_in<T: AbsoluteWhite>(
c: RelativeXYZ<T>,
condition: &Self::ViewingCondition,
) -> Self {
let rgb: TristimulusRgb<Cam16ColorSpace> =
RelativeXYZ::<Cam16ColorSpace>::from_vector(c.into_vector()).into();
let C_rgb = condition.D_rgb.component_mul(&rgb.into_vector());
let a_rgb = C_rgb.apply_into(|v| {
if *v >= 0. {
let c = (condition.Fl * *v).powf(0.42);
*v = 400. * c / (c + 27.13);
//*v = 400. * c / (c + 27.13) + 0.1;
} else {
let c = (-condition.Fl * *v).powf(0.42);
*v = -400. * c / (c + 27.13);
// *v = -400. * c / (c + 27.13) + 0.1;
}
});
let (Ra, Ga, Ba) = (a_rgb.x, a_rgb.y, a_rgb.z);
let a = Ra - 12. * Ga / 11. + Ba / 11.;
let b = (Ra + Ga - 2. * Ba) / 9.;
let h = angle_degree(b, a);
let A = (2. * Ra + Ga + Ba / 20.) * condition.Nbb;
//let A = (2. * Ra + Ga + Ba / 20. - 0.305) * condition.Nbb;
let J_1 = (A / condition.Aw).powf(condition.c * condition.z);
let J = J_1 * 100.;
let Q = (4. / condition.c) * J_1.sqrt() * (condition.Aw + 4.) * condition.Fl.powf(0.25);
let h_prime = if h < 20.14 { h + 360. } else { h };
let et = 0.25 * ((to_rad(h_prime) + 2.).cos() + 3.8);
let t = (50_000. / 13.) * condition.Nc * condition.Nbb * et * a.hypot(b)
/ (Ra + Ga + (21. / 20.) * Ba + 0.305);
// / (Ra + Ga + (21. / 20.) * Ba);
let C = t.powf(0.9) * J_1.sqrt() * (1.64 - 0.29f32.powf(condition.n)).powf(0.73);
let M = C * condition.Fl.powf(0.25);
let s = 100. * (M / Q).sqrt();
Self {
hue: h,
lightness: J,
brightness: Q,
chroma: C,
colorfulness: M,
saturation: s,
}
}
fn from_hue_color_lightness_scales(
hue: f32,
color_scale: f32,
lightness_scale: f32,
condition: &Self::ViewingCondition,
) -> Self {
let h = hue;
let J = lightness_scale;
let s = color_scale;
let J_1 = J / 100.;
let Q = (4. / condition.c) * J_1.sqrt() * (condition.Aw + 4.) * condition.Fl.powf(0.25);
let M = (s / 100.).powi(2) * Q;
let C = M / condition.Fl.powf(0.25);
Self {
hue: h,
lightness: J,
brightness: Q,
chroma: C,
colorfulness: M,
saturation: s,
}
}
fn distance(&self, other: &Self) -> f32 {
let s: Cam16Hcl = (*self).into();
let o: Cam16Hcl = (*other).into();
s.distance(&o)
}
fn grey_of_lightness(lightness: f32, condition: &Self::ViewingCondition) -> Self {
let Q =
(4. / condition.c) * (lightness / 100.) * (condition.Aw + 4.) * condition.Fl.powf(0.25);
Self {
hue: 0.,
lightness,
brightness: Q,
chroma: 0.,
colorfulness: 0.,
saturation: 0.,
}
}
fn lightness_scale(&self) -> f32 {
self.lightness
}
fn color_scale(&self) -> f32 {
self.saturation
}
fn hue(&self) -> f32 {
self.hue
}
fn set_lightness_scale(&mut self, lightness: f32) -> &mut Self {
let Q0 = self.brightness;
let J0 = self.lightness;
let C0 = self.chroma;
let jc0 = (J0 / 100.).sqrt();
let jc1 = (lightness / 100.).sqrt();
let f = jc1 / jc0;
let Q1 = Q0 * f;
let C1 = C0 * f;
let M1 = self.colorfulness * (C1 / C0);
self.lightness = lightness;
self.brightness = Q1;
self.chroma = C1;
self.colorfulness = M1;
self
}
fn set_color_scale(&mut self, saturation: f32) -> &mut Self {
let colorfulness = (saturation / 100.).powi(2) * self.brightness;
let mc_coef = self.chroma / self.colorfulness;
let chroma = colorfulness * mc_coef;
self.saturation = saturation;
self.colorfulness = colorfulness;
self.chroma = chroma;
self
}
fn set_hue(&mut self, hue: f32) -> &mut Self {
self.hue = hue;
self
}
}
impl Cam16 {
pub fn set_lightness_at_constant_colorfulness(&mut self, lightness: f32) -> &mut Self {
let Q0 = self.brightness;
let J0 = self.lightness;
let jc0 = (J0 / 100.).sqrt();
let jc1 = (lightness / 100.).sqrt();
let f = jc1 / jc0;
let Q1 = Q0 * f;
self.lightness = lightness;
self.brightness = Q1;
self.saturation = 100. * (self.colorfulness / Q1).sqrt();
self
}
pub fn set_saturation_at_constant_colorfulness(&mut self, saturation: f32) -> &mut Self {
if saturation != self.saturation {
assert!(self.lightness != 0. && saturation != 0.);
let brightness = self.colorfulness / (saturation / 50.).powi(2);
let jf = (self.lightness / 100.).sqrt();
let f = self.brightness / jf;
let lightness = (brightness / f).powi(2) * 100.;
self.brightness = brightness;
self.lightness = lightness;
self.saturation = saturation;
}
self
}
pub fn set_colorfulness_at_constant_saturation(&mut self, colorfulness: f32) -> &mut Self {
assert!(self.saturation != 0.);
let mq = self.chroma / self.colorfulness;
let chroma = mq * colorfulness;
let brightness = colorfulness / (self.saturation / 50.).powi(2);
let jf = (self.lightness / 100.).sqrt();
let f = self.brightness / jf;
let lightness = (brightness / f).powi(2) * 100.;
self.lightness = lightness;
self.brightness = brightness;
self.chroma = chroma;
self.colorfulness = colorfulness;
self
}
}
fn appearance_to_tristimulus<T: AbsoluteWhite>(
J: f32,
C: f32,
h: f32,
condition: &ViewingCondition,
) -> RelativeXYZ<T> {
let h_rad = to_rad(h);
let t = if C == 0. || J == 0. {
0.
} else {
(C / ((J / 100.).sqrt() * (1.64 - 0.29f32.powf(condition.n)).powf(0.73))).powf(1. / 0.9)
};
let et = 0.25 * ((h_rad + 2.).cos() + 3.8);
let A = condition.Aw * (J / 100.).powf(1. / (condition.c * condition.z));
let p2 = A / condition.Nbb;
//let p2 = A / condition.Nbb + 0.305;
let p1 = (50_000. / 13. * condition.Nc * condition.Nbb) * et;
let h_sin = h_rad.sin();
let h_cos = h_rad.cos();
let gamma = 23. * (p2 + 0.305) * t / (23. * p1 + 11. * t * h_cos + 108. * t * h_sin);
let a = gamma * h_cos;
let b = gamma * h_sin;
//let (a, b) = if t == 0.0 {
// (0., 0.)
//} else {
// let p1 = (50_000. / 13. * condition.Nc * condition.Nbb) * et / t;
// let p3 = 21. / 20.;
// let h_sin = h_rad.sin();
// let h_cos = h_rad.cos();
// if h_sin.abs() >= h_cos.abs() {
// let p4 = p1 / h_sin;
// let h_cotan = h_cos / h_sin;
// let b = p2 * (2. + p3) * (460. / 1403.)
// / (p4 + (2. + p3) * (220. / 1403.) * h_cotan - (27. / 1403.)
// + p3 * (6300. / 1403.));
// (b * h_cotan, b)
// } else {
// let p5 = p1 / h_cos;
// let h_tan = h_sin / h_cos;
// let a = p2 * (2. + p3) * (460. / 1403.)
// / (p5 + (2. + p3) * (220. / 1403.)
// - ((27. / 1403.) - p3 * (6300. / 1403.)) * h_tan);
// (a, a * h_tan)
// }
//};
let Ra = 460. / 1403. * p2 + 451. / 1403. * a + 288. / 1403. * b;
let Ga = 460. / 1403. * p2 - 891. / 1403. * a - 261. / 1403. * b;
let Ba = 460. / 1403. * p2 - 220. / 1403. * a - 6300. / 1403. * b;
let C_rgb = vector![Ra, Ga, Ba].apply_into(|a| {
let am = *a;
//let am = *a - 0.1;
let gc = (27.13 * am.abs() / (400. - am.abs())).clamp(0., f32::MAX);
*a = am.signum() * 1. / condition.Fl * gc.powf(1. / 0.42);
});
let rgb = C_rgb.component_div(&condition.D_rgb);
let xyz =
RelativeXYZ::<Cam16ColorSpace>::from(TristimulusRgb::<Cam16ColorSpace>::from_vector(rgb));
RelativeXYZ::from_vector(xyz.into_vector())
}
impl ColourAppearanceTransform for Cam16 {
fn adaptation_to(adapting_field_luminance: f32, surround_relative_luminance: f32) -> f32 {
let F = 0.8 + surround_relative_luminance / 2.;
(F * (1. - (1. / 3.6) * ((-adapting_field_luminance - 42.) / 92.).exp())).clamp(0., 1.)
}
fn illuminant_transform_matrix(
source_white: RelativeXYZ,
target_white: RelativeXYZ,
adaptation: f32,
) -> Matrix3<f32> {
let e = RelativeXYZ::<Cam16ColorSpace>::from_chroma_relative_luminance(
E::CHROMA,
RELATIVE_TRISTIMULUS_SCALE,
);
let e_rgb: TristimulusRgb<_> = e.into();
let w_source_rgb: TristimulusRgb<_> =
source_white.cast_illuminant::<Cam16ColorSpace>().into();
let w_target_rgb: TristimulusRgb<_> =
target_white.cast_illuminant::<Cam16ColorSpace>().into();
let mut delta_e_target = Matrix3::from_diagonal(
&e_rgb
.into_vector()
.component_div(&w_target_rgb.into_vector()),
);
let mut delta_e_source = Matrix3::from_diagonal(
&e_rgb
.into_vector()
.component_div(&w_source_rgb.into_vector()),
);
delta_e_target.apply(|v| *v = adaptation * *v + 1. - adaptation);
delta_e_source.apply(|v| *v = adaptation * *v + 1. - adaptation);
let delta_e_target_m1 = delta_e_target.try_inverse().unwrap();
Cam16ColorSpace::linear_rgb_to_relative_xyz_matrix()
* delta_e_target_m1
* delta_e_source
* Cam16ColorSpace::relative_xyz_to_linear_rgb_matrix()
}
}
impl ColourAppearanceTransform for Cam16Jab {
fn adaptation_to(adapting_field_luminance: f32, surround_relative_luminance: f32) -> f32 {
Cam16::adaptation_to(adapting_field_luminance, surround_relative_luminance)
}
fn illuminant_transform_matrix(
source_white: RelativeXYZ,
target_white: RelativeXYZ,
adaptation: f32,
) -> Matrix3<f32> {
Cam16::illuminant_transform_matrix(source_white, target_white, adaptation)
}
}
impl ColourAppearanceTransform for Cam16Hcl {
fn adaptation_to(adapting_field_luminance: f32, surround_relative_luminance: f32) -> f32 {
Cam16::adaptation_to(adapting_field_luminance, surround_relative_luminance)
}
fn illuminant_transform_matrix(
source_white: RelativeXYZ,
target_white: RelativeXYZ,
adaptation: f32,
) -> Matrix3<f32> {
Cam16::illuminant_transform_matrix(source_white, target_white, adaptation)
}
}
impl WhitePoint for Cam16ColorSpace {
type WhitePoint = E;
}
impl LinearColorSpacePrimaries for Cam16ColorSpace {
const PRIMARY_RED: ChromaticityXy = ChromaticityXy {
x: 0.83360,
y: 0.17349,
};
const PRIMARY_GREEN: ChromaticityXy = ChromaticityXy {
x: 2.3854,
y: -1.4659,
};
const PRIMARY_BLUE: ChromaticityXy = ChromaticityXy {
x: 0.53343,
y: -0.00402,
};
}
impl ColorModel for Cam16ColorSpace {
type LinearColorSpacePrimaries = Self;
#[allow(clippy::excessive_precision)]
fn linear_rgb_to_relative_xyz_matrix() -> Matrix3<f32> {
matrix![
1.86206786, -1.01125463, 0.14918677;
0.38752654, 0.62144744, -0.00897398;
-0.01584150, -0.03412294, 1.04996444
]
}
#[allow(clippy::excessive_precision)]
fn relative_xyz_to_linear_rgb_matrix() -> Matrix3<f32> {
matrix![
0.401288, 0.650173, -0.051461;
-0.250268, 1.204414, 0.045854;
-0.002079, 0.048952, 0.953127
]
//Self::linear_rgb_to_relative_xyz_matrix()
// .try_inverse()
// .unwrap()
}
}
impl From<SpecViewingCondition> for ViewingCondition {
fn from(v: SpecViewingCondition) -> Self {
let La = v.adapting_field_luminance;
let F = 0.8 + v.surround_relative_luminance / 2.;
let c = if F >= 0.9 {
0.59 + (F - 0.9) * 10. * (0.69 - 0.59)
} else {
0.525 + (F - 0.8) * 10. * (0.69 - 0.525)
};
let Nc = F;
let D = match v.chromatic_adaptation {
ChromaticAdaptation::Full => 1.,
ChromaticAdaptation::None => 0.,
ChromaticAdaptation::Intermediate => {
(F * (1. - (1. / 3.6) * ((-La - 42.) / 92.).exp())).clamp(0., 1.)
}
};
let white_rgb: TristimulusRgb<Cam16ColorSpace> =
RelativeXYZ::<Cam16ColorSpace>::from_vector(v.adopted_white.into_vector()).into();
let Yw = v.adopted_white.y;
let D_rgb = white_rgb
.into_vector()
.apply_into(|v| *v = D * 1. / *v + 1. - D);
//.apply_into(|v| *v = D * Yw / *v + 1. - D);
let k = 1. / (5. * La + 1.);
let k4 = k.powi(4);
let Fl = k4 * La + 0.1 * (1. - k4).powi(2) * (5. * La).cbrt();
let n = v.background_relative_luminance / Yw;
let z = 1.48 + n.sqrt();
let Nbb = 0.725 * (1. / n).powf(0.2);
//let Ncb = Nbb;
let Wc_rgb = D_rgb.component_mul(&white_rgb.into_vector());
let Aw_rgb = Wc_rgb.apply_into(|v| {
let c = (Fl * *v).powf(0.42);
*v = 400. * (c / (c + 27.13))
//*v = 400. * (c / (c + 27.13)) + 0.1
});
let Aw = (2. * Aw_rgb.x + Aw_rgb.y + Aw_rgb.z / 20.) * Nbb;
//let Aw = (2. * Aw_rgb.x + Aw_rgb.y + Aw_rgb.z / 20. - 0.305) * Nbb;
Self {
D_rgb,
Fl,
Nbb,
Aw,
c,
z,
Nc,
n,
}
}
}
Hi! I'm happy to see that you are finding Palette useful. I still think CAM16 could be a nice addition, so it would be amazing if you are up for adapting this implementation. Keep in mind, though, that I haven't gotten around to read up on all the details yet. It's not my main field, so it will be more of a contributor driven feature. I will do my best to look into it along the way, but you'll be the expert and I'll probably have questions. Another alternative is of course to keep CAM16 separate and depend on Palette, if you prefer that.
The funny palette_internal
attribute is just a marker that tells the derive macros that they should look for Palette types under crate::
, instead of palette::
, so it's mostly relevant for color space types. A more general thing to keep in mind is that the code should be generic over the number type and avoid Copy
, if possible. Ideally also allow SIMD types. You can take it one bit at the time, since it can turn a bit ugly with all the traits. The SIMD part is extra credits at the moment, since it's still a bit experimental. I think this code will be pretty straight forward to SIMD convert, though, since it doesn't seem to have a lot of complex branches and loops.
Ok, I'll try to implement it. How do we do? I have never worked with GitHub collaboratively. I have forked your repo. Should I push changes to the forked repo and inform you here about the changes here?
No worries! You will be able to open a pull request here once you have pushed something to your fork. Don't hesitate to push and open a PR for a work in progress if you want feedback or help along the way.
What about caching the rgb_to_xyz matrix and its inverse in the trait Primaries? This will dramatically increase conversion performance.
It would be implemented as trait methods whose default implementation call functions of the module num
. For the standard defined in the crate, we would return directly the matrix value, without any computation. For CAM16 this is important because each conversion may imply few conversions from different linear color spaces.
That would be a very nice way of doing it! I have wanted something like this for a while but not decided on how. I think this is a good starting point.
It seems that the state-of-the-art currently in colour spaces is the CAM16 colour space. @nschloe wrote a paper concerning improvements to the colour space, which they then implemented in Python as part of a larger colour space library. I know very little about colour spaces or mathematics, so I don’t know how much I could do apart from mechanically porting the Python implementation to Rust. Is this something that would be helpful?