palette/cam16/
math.rs

1use core::{
2    marker::PhantomData,
3    ops::{Div, Mul},
4};
5
6use crate::{
7    angle::{RealAngle, SignedAngle},
8    bool_mask::{LazySelect, Select},
9    clamp,
10    hues::Cam16Hue,
11    num::{
12        Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt,
13        Trigonometry, Zero,
14    },
15    white_point,
16    xyz::Xyz,
17};
18
19use super::{Cam16, Parameters};
20
21use self::{chromaticity::ChromaticityType, luminance::LuminanceType};
22
23pub(crate) mod chromaticity;
24pub(crate) mod luminance;
25
26// This module is originally based on these sources:
27// - https://observablehq.com/@jrus/cam16
28// - "Comprehensive color solutions: CAM16, CAT16, and CAM16-UCS" by Li C, Li Z,
29//   Wang Z, et al.
30//   (https://www.researchgate.net/publication/318152296_Comprehensive_color_solutions_CAM16_CAT16_and_CAM16-UCS)
31// - "Algorithmic improvements for the CIECAM02 and CAM16 color appearance
32//   models" by Nico Schlömer (https://arxiv.org/pdf/1802.06067.pdf)
33// - https://rawpedia.rawtherapee.com/CIECAM02.
34// - "Usage Guidelines for CIECAM97s" by Nathan Moroney
35//   (https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2000/PICS-0-81/1611.pdf)
36// - "CIECAM02 and Its Recent Developments" by Ming Ronnier Luo and Changjun Li
37//   (https://cielab.xyz/pdf/CIECAM02_and_Its_Recent_Developments.pdf)
38// - https://en.wikipedia.org/wiki/CIECAM02
39
40pub(crate) fn xyz_to_cam16<T>(
41    xyz: Xyz<white_point::Any, T>,
42    parameters: DependentParameters<T::Scalar>,
43) -> Cam16<T>
44where
45    T: Real
46        + FromScalar
47        + Arithmetics
48        + Powf
49        + Sqrt
50        + Abs
51        + Signum
52        + Trigonometry
53        + RealAngle
54        + Clone,
55    T::Scalar: Clone,
56{
57    let xyz = xyz.with_white_point() * T::from_f64(100.0); // The reference uses 0.0 to 100.0 instead of 0.0 to 1.0.
58    let d_rgb = map3(parameters.d_rgb.clone(), T::from_scalar);
59
60    let [r_a, g_a, b_a] = map3(mul3(m16(xyz), d_rgb), |component| {
61        parameters.adapt.run(component)
62    });
63    let a = r_a.clone() + (T::from_f64(-12.0) * &g_a + &b_a) / T::from_f64(11.0); // redness-greenness
64    let b = (r_a.clone() + &g_a - T::from_f64(2.0) * &b_a) / T::from_f64(9.0); // yellowness-blueness
65    let h_rad = b.clone().atan2(a.clone()); // hue in radians
66    let h = Cam16Hue::from_radians(h_rad.clone()); // hue in degrees
67    let e_t = T::from_f64(0.25) * (T::cos(h_rad + T::from_f64(2.0)) + T::from_f64(3.8));
68    let capital_a = T::from_scalar(parameters.n_bb)
69        * (T::from_f64(2.0) * &r_a + &g_a + T::from_f64(0.05) * &b_a);
70    let j_root = (capital_a / T::from_scalar(parameters.a_w.clone())).powf(
71        T::from_f64(0.5) * T::from_scalar(parameters.c.clone()) * T::from_scalar(parameters.z),
72    );
73
74    let j = calculate_lightness(j_root.clone()); // lightness
75    let q = calculate_brightness(
76        j_root.clone(),
77        T::from_scalar(parameters.c.clone()),
78        T::from_scalar(parameters.a_w.clone()),
79        T::from_scalar(parameters.f_l_4.clone()),
80    ); // brightness
81
82    let t = T::from_f64(5e4) / T::from_f64(13.0)
83        * T::from_scalar(parameters.n_c)
84        * T::from_scalar(parameters.n_cb)
85        * e_t
86        * (a.clone() * a + b.clone() * b).sqrt()
87        / (r_a + g_a + T::from_f64(1.05) * b_a + T::from_f64(0.305));
88    let alpha = t.powf(T::from_f64(0.9))
89        * (T::from_f64(1.64) - T::from_f64(0.29).powf(T::from_scalar(parameters.n)))
90            .powf(T::from_f64(0.73));
91
92    let c = calculate_chroma(j_root, alpha.clone()); // chroma
93    let m = calculate_colorfulness(T::from_scalar(parameters.f_l_4), c.clone()); // colorfulness
94    let s = calculate_saturation(
95        T::from_scalar(parameters.c),
96        T::from_scalar(parameters.a_w),
97        alpha,
98    ); // saturation
99
100    Cam16 {
101        lightness: j,
102        chroma: c,
103        hue: h,
104        brightness: q,
105        colorfulness: m,
106        saturation: s,
107    }
108}
109
110fn calculate_lightness<T>(j_root: T) -> T
111where
112    T: Real + Arithmetics,
113{
114    T::from_f64(100.0) * &j_root * j_root
115}
116
117fn calculate_brightness<T>(j_root: T, param_c: T, param_a_w: T, param_f_l_4: T) -> T
118where
119    T: Real + Arithmetics,
120{
121    T::from_f64(4.0) / param_c * j_root * (T::from_f64(4.0) + param_a_w) * param_f_l_4
122}
123
124#[inline]
125pub(super) fn calculate_chroma<T>(j_root: T, alpha: T) -> T
126where
127    T: Mul<T, Output = T>,
128{
129    j_root * alpha
130}
131
132#[inline]
133pub(super) fn calculate_colorfulness<T>(param_f_l_4: T, chroma: T) -> T
134where
135    T: Mul<T, Output = T>,
136{
137    param_f_l_4 * chroma
138}
139
140#[inline]
141pub(super) fn calculate_saturation<T>(param_c: T, param_a_w: T, alpha: T) -> T
142where
143    T: Real + Arithmetics + Sqrt,
144{
145    T::from_f64(50.0) * (param_c * alpha / (param_a_w + T::from_f64(4.0))).sqrt()
146}
147
148#[inline]
149pub(crate) fn cam16_to_xyz<T>(
150    cam16: (LuminanceType<T>, ChromaticityType<T>, Cam16Hue<T>),
151    parameters: DependentParameters<T::Scalar>,
152) -> Xyz<white_point::Any, T>
153where
154    T: Real
155        + FromScalar
156        + One
157        + Zero
158        + Sqrt
159        + Powf
160        + Abs
161        + Signum
162        + Arithmetics
163        + Trigonometry
164        + RealAngle
165        + SignedAngle
166        + PartialCmp
167        + Clone,
168    T::Mask: LazySelect<T> + Clone,
169    T::Scalar: Clone,
170{
171    // Weird naming, but we just want to know if it's black or not here.
172    let is_black = match &cam16.0 {
173        LuminanceType::Lightness(lightness) => lightness.eq(&T::zero()),
174        LuminanceType::Brightness(brightness) => brightness.eq(&T::zero()),
175    };
176
177    let xyz = non_black_cam16_to_xyz(cam16, parameters);
178    Xyz {
179        x: is_black.clone().select(T::zero(), xyz.x),
180        y: is_black.clone().select(T::zero(), xyz.y),
181        z: is_black.select(T::zero(), xyz.z),
182        white_point: PhantomData,
183    }
184}
185
186// Assumes that lightness has been checked to be non-zero in `cam16_to_xyz`.
187fn non_black_cam16_to_xyz<T>(
188    cam16: (LuminanceType<T>, ChromaticityType<T>, Cam16Hue<T>),
189    parameters: DependentParameters<T::Scalar>,
190) -> Xyz<white_point::Any, T>
191where
192    T: Real
193        + FromScalar
194        + One
195        + Sqrt
196        + Powf
197        + Abs
198        + Signum
199        + Arithmetics
200        + Trigonometry
201        + RealAngle
202        + SignedAngle
203        + Clone,
204    T::Scalar: Clone,
205{
206    let h_rad = cam16.2.into_radians();
207    let (sin_h, cos_h) = h_rad.clone().sin_cos();
208    let j_root = match cam16.0 {
209        LuminanceType::Lightness(j) => lightness_to_j_root(j),
210        LuminanceType::Brightness(q) => brightness_to_j_root(
211            q,
212            T::from_scalar(parameters.c.clone()),
213            T::from_scalar(parameters.a_w.clone()),
214            T::from_scalar(parameters.f_l_4.clone()),
215        ),
216    };
217    let alpha = match cam16.1 {
218        ChromaticityType::Chroma(c) => c / &j_root,
219        ChromaticityType::Colorfulness(m) => {
220            colorfulness_to_chroma(m, T::from_scalar(parameters.f_l_4)) / &j_root
221        }
222        ChromaticityType::Saturation(s) => saturation_to_alpha(
223            s,
224            T::from_scalar(parameters.c.clone()),
225            T::from_scalar(parameters.a_w.clone()),
226        ),
227    };
228    let t = (alpha
229        * (T::from_f64(1.64) - T::from_f64(0.29).powf(T::from_scalar(parameters.n)))
230            .powf(T::from_f64(-0.73)))
231    .powf(T::from_f64(10.0) / T::from_f64(9.0));
232    let e_t = T::from_f64(0.25) * ((h_rad + T::from_f64(2.0)).cos() + T::from_f64(3.8));
233    let capital_a = T::from_scalar(parameters.a_w)
234        * j_root
235            .powf(T::from_f64(2.0) / T::from_scalar(parameters.c) / T::from_scalar(parameters.z));
236    let p_1 = T::from_f64(5e4) / T::from_f64(13.0)
237        * T::from_scalar(parameters.n_c)
238        * T::from_scalar(parameters.n_cb)
239        * e_t;
240    let p_2 = capital_a / T::from_scalar(parameters.n_bb);
241    let r = T::from_f64(23.0) * (T::from_f64(0.305) + &p_2) * &t
242        / (T::from_f64(23.0) * p_1
243            + t * (T::from_f64(11.0) * &cos_h + T::from_f64(108.0) * &sin_h));
244    let a = cos_h * &r;
245    let b = sin_h * r;
246    let denom = T::one() / T::from_f64(1403.0);
247    let rgb_c = [
248        (T::from_f64(460.0) * &p_2 + T::from_f64(451.0) * &a + T::from_f64(288.0) * &b) * &denom,
249        (T::from_f64(460.0) * &p_2 - T::from_f64(891.0) * &a - T::from_f64(261.0) * &b) * &denom,
250        (T::from_f64(460.0) * p_2 - T::from_f64(220.0) * a - T::from_f64(6300.0) * b) * &denom,
251    ];
252
253    let unadapt = parameters.unadapt;
254    let rgb_c = map3(rgb_c, |component| unadapt.run(component));
255    let d_rgb_inv = map3(parameters.d_rgb_inv, T::from_scalar);
256
257    m16_inv(mul3(rgb_c, d_rgb_inv)) / T::from_f64(100.0) // The reference uses 0.0 to 100.0 instead of 0.0 to 1.0.
258}
259
260pub(super) fn prepare_parameters<T>(
261    parameters: Parameters<Xyz<white_point::Any, T>, T>,
262) -> DependentParameters<T>
263where
264    T: Real
265        + FromScalar<Scalar = T>
266        + One
267        + Zero
268        + Clamp
269        + PartialCmp
270        + Arithmetics
271        + Powf
272        + Sqrt
273        + Exp
274        + Abs
275        + Signum
276        + Clone,
277    T::Mask: LazySelect<T>,
278{
279    // Compute dependent parameters.
280    let xyz_w = parameters.white_point * T::from_f64(100.0); // The reference uses 0.0 to 100.0 instead of 0.0 to 1.0.
281    let l_a = parameters.adapting_luminance;
282    let y_b = parameters.background_luminance * T::from_f64(100.0); // The reference uses 0.0 to 100.0 instead of 0.0 to 1.0.
283    let y_w = xyz_w.y.clone();
284    let surround = parameters.surround.into_percent() * T::from_f64(0.1);
285    let c = lazy_select! {
286        if surround.gt_eq(&T::one()) => lerp(
287            T::from_f64(0.59),
288            T::from_f64(0.69),
289            surround.clone() - T::one(),
290        ),
291        else => lerp(T::from_f64(0.525), T::from_f64(0.59), surround.clone())
292    };
293    let f = lazy_select! {
294        if c.gt_eq(&T::from_f64(0.59)) => lerp(
295            T::from_f64(0.9),
296            T::one(),
297            (c.clone() - T::from_f64(0.59)) / T::from_f64(0.1)),
298        else => lerp(
299            T::from_f64(0.8),
300            T::from_f64(0.9),
301            (c.clone() - T::from_f64(0.525)) / T::from_f64(0.065)
302        )
303    };
304    let n_c = f.clone();
305    let k = T::one() / (T::from_f64(5.0) * &l_a + T::one());
306    let f_l = {
307        // Luminance adaptation factor
308        let k4 = k.clone() * &k * &k * k;
309        let k4_inv = T::one() - &k4;
310        let a_third = T::one() / T::from_f64(3.0);
311
312        k4 * &l_a + T::from_f64(0.1) * &k4_inv * k4_inv * (T::from_f64(5.0) * &l_a).powf(a_third)
313    };
314    let f_l_4 = f_l.clone().powf(T::from_f64(0.25));
315    let n = y_b / &y_w;
316    let z = T::from_f64(1.48) + n.clone().sqrt(); // Lightness non-linearity exponent (modified by `c`).
317    let n_bb = T::from_f64(0.725) * n.clone().powf(T::from_f64(-0.2)); // Chromatic induction factors
318    let n_cb = n_bb.clone();
319    // Illuminant discounting (adaptation). Fully adapted = 1
320    let d = match parameters.discounting {
321        super::Discounting::Auto => {
322            // The default D function.
323            f * (T::one()
324                - T::one() / T::from_f64(3.6)
325                    * Exp::exp((-l_a - T::from_f64(42.0)) / T::from_f64(92.0)))
326        }
327        super::Discounting::Custom(degree) => degree,
328    };
329
330    let d = clamp(d, T::zero(), T::one());
331
332    let rgb_w = m16(xyz_w); // Cone responses of the white point
333    let d_rgb = map3(rgb_w.clone(), |c_w| {
334        lerp(T::one(), y_w.clone() / c_w, d.clone())
335    });
336    let d_rgb_inv = map3(d_rgb.clone(), |d_c| T::one() / d_c);
337    let rgb_cw = mul3(rgb_w, d_rgb.clone());
338
339    let adapt = Adapt { f_l: f_l.clone() };
340
341    let exponent = T::one() / T::from_f64(0.42);
342    let unadapt = Unadapt {
343        constant: T::from_f64(100.0) / f_l * T::from_f64(27.13).powf(exponent.clone()),
344        exponent,
345    };
346
347    let [rgb_aw1, rgb_aw2, rgb_aw3] = map3(rgb_cw, |component| adapt.run(component));
348    let a_w = n_bb.clone() * (T::from_f64(2.0) * rgb_aw1 + rgb_aw2 + T::from_f64(0.05) * rgb_aw3);
349
350    DependentParameters {
351        d_rgb,
352        d_rgb_inv,
353        n,
354        n_bb,
355        n_c,
356        n_cb,
357        a_w,
358        c,
359        z,
360        f_l_4,
361        adapt,
362        unadapt,
363    }
364}
365
366#[inline]
367pub(super) fn lightness_to_brightness<T>(
368    lightness: T,
369    param_c: T,
370    param_a_w: T,
371    param_f_l_4: T,
372) -> T
373where
374    T: Real + Arithmetics + Sqrt,
375{
376    let j_root = lightness_to_j_root(lightness);
377    calculate_brightness(j_root, param_c, param_a_w, param_f_l_4)
378}
379
380#[inline]
381pub(super) fn brightness_to_lightness<T>(
382    brightness: T,
383    param_c: T,
384    param_a_w: T,
385    param_f_l_4: T,
386) -> T
387where
388    T: Real + Arithmetics,
389{
390    let j_root = brightness_to_j_root(brightness, param_c, param_a_w, param_f_l_4);
391    calculate_lightness(j_root)
392}
393
394#[inline]
395pub(super) fn chroma_to_colorfulness<T>(chroma: T, param_f_l_4: T) -> T
396where
397    T: Mul<T, Output = T>,
398{
399    param_f_l_4 * chroma
400}
401
402#[inline]
403pub(super) fn chroma_to_saturation<T>(chroma: T, lightness: T, param_c: T, param_a_w: T) -> T
404where
405    T: Real + Arithmetics + Sqrt + Clone,
406{
407    let j_root = lightness_to_j_root(lightness);
408    let alpha = chroma / &j_root;
409
410    calculate_saturation(param_c, param_a_w, alpha)
411}
412
413#[inline]
414pub(super) fn colorfulness_to_chroma<T>(colorfulness: T, param_f_l_4: T) -> T
415where
416    T: Div<T, Output = T>,
417{
418    colorfulness / param_f_l_4
419}
420
421#[inline]
422pub(super) fn saturation_to_chroma<T>(saturation: T, lightness: T, param_c: T, param_a_w: T) -> T
423where
424    T: Real + Arithmetics + Sqrt,
425{
426    let j_root = lightness_to_j_root(lightness);
427    let alpha = saturation_to_alpha(saturation, param_c, param_a_w);
428
429    calculate_chroma(j_root, alpha)
430}
431
432#[inline]
433fn lightness_to_j_root<T>(lightness: T) -> T
434where
435    T: Real + Mul<T, Output = T> + Sqrt,
436{
437    lightness.sqrt() * T::from_f64(0.1)
438}
439
440#[inline]
441fn brightness_to_j_root<T>(brightness: T, param_c: T, param_a_w: T, param_f_l_4: T) -> T
442where
443    T: Real + Arithmetics,
444{
445    T::from_f64(0.25) * param_c * brightness / ((T::from_f64(4.0) + param_a_w) * param_f_l_4)
446}
447
448#[inline]
449fn saturation_to_alpha<T>(saturation: T, param_c: T, param_a_w: T) -> T
450where
451    T: Real + Arithmetics,
452{
453    T::from_f64(0.0004) * &saturation * saturation * (T::from_f64(4.0) + param_a_w) / param_c
454}
455
456#[derive(Clone, Copy)]
457pub(crate) struct DependentParameters<T> {
458    d_rgb: [T; 3],
459    d_rgb_inv: [T; 3],
460    n: T,
461    n_bb: T,
462    n_c: T,
463    n_cb: T,
464    pub(super) a_w: T,
465    pub(super) c: T,
466    z: T,
467    pub(super) f_l_4: T,
468    adapt: Adapt<T>,
469    unadapt: Unadapt<T>,
470}
471
472#[derive(Clone, Copy)]
473struct Adapt<T> {
474    f_l: T,
475}
476
477impl<T> Adapt<T> {
478    fn run<V>(&self, component: V) -> V
479    where
480        V: Real + FromScalar<Scalar = T> + Abs + Signum + Powf + Arithmetics + Clone,
481        T: Clone,
482    {
483        let x = (V::from_scalar(self.f_l.clone()) * component.clone().abs() * V::from_f64(0.01))
484            .powf(V::from_f64(0.42));
485        component.signum() * V::from_f64(400.0) * &x / (x + V::from_f64(27.13))
486    }
487}
488
489#[derive(Clone, Copy)]
490struct Unadapt<T> {
491    constant: T,
492    exponent: T,
493}
494
495impl<T> Unadapt<T> {
496    fn run<V>(&self, component: V) -> V
497    where
498        V: Real + FromScalar<Scalar = T> + Abs + Signum + Powf + Arithmetics + Clone,
499        T: Clone,
500    {
501        let c_abs = component.clone().abs();
502        component.signum()
503            * V::from_scalar(self.constant.clone())
504            * (c_abs.clone() / (V::from_f64(400.0) - c_abs))
505                .powf(V::from_scalar(self.exponent.clone()))
506    }
507}
508
509fn lerp<T>(from: T, to: T, factor: T) -> T
510where
511    T: One + Arithmetics,
512{
513    (T::one() - &factor) * from + factor * to
514}
515
516fn m16<T>(xyz: Xyz<white_point::Any, T>) -> [T; 3]
517where
518    T: Real + Arithmetics,
519{
520    let Xyz { x, y, z, .. } = xyz;
521
522    #[rustfmt::skip]
523    let rgb = [
524        T::from_f64( 0.401288) * &x + T::from_f64(0.650173) * &y - T::from_f64(0.051461) * &z,
525        T::from_f64(-0.250268) * &x + T::from_f64(1.204414) * &y + T::from_f64(0.045854) * &z,
526        T::from_f64(-0.002079) *  x + T::from_f64(0.048952) *  y + T::from_f64(0.953127) *  z,
527    ];
528
529    rgb
530}
531
532fn m16_inv<T>(rgb: [T; 3]) -> Xyz<white_point::Any, T>
533where
534    T: Real + Arithmetics,
535{
536    let [r, g, b] = rgb;
537
538    #[rustfmt::skip]
539    #[allow(clippy::excessive_precision)] // Clippy didn't like the e+0
540    let xyz = Xyz {
541        x: T::from_f64( 1.862067855087233e+0) * &r - T::from_f64(1.011254630531685e+0) * &g + T::from_f64(1.491867754444518e-1) * &b,
542        y: T::from_f64( 3.875265432361372e-1) * &r + T::from_f64(6.214474419314753e-1) * &g - T::from_f64(8.973985167612518e-3) * &b,
543        z: T::from_f64(-1.584149884933386e-2) *  r - T::from_f64(3.412293802851557e-2) *  g + T::from_f64(1.049964436877850e+0) *  b,
544        white_point: PhantomData
545    };
546
547    xyz
548}
549
550fn map3<T, U>(array: [T; 3], mut map: impl FnMut(T) -> U) -> [U; 3] {
551    let [a1, a2, a3] = array;
552    [map(a1), map(a2), map(a3)]
553}
554
555fn mul3<T>(lhs: [T; 3], rhs: [T; 3]) -> [T; 3]
556where
557    T: Mul<T, Output = T>,
558{
559    let [l1, l2, l3] = lhs;
560    let [r1, r2, r3] = rhs;
561
562    [l1 * r1, l2 * r2, l3 * r3]
563}