Skip to content

Commit 7498ce7

Browse files
committed
Improve CAM16 parameters and adjust documentation
1 parent 22880dd commit 7498ce7

File tree

2 files changed

+78
-58
lines changed

2 files changed

+78
-58
lines changed

palette/src/cam16/math.rs

+24-12
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,19 @@ use self::{chromaticity::ChromaticityType, luminance::LuminanceType};
2323
pub(crate) mod chromaticity;
2424
pub(crate) mod luminance;
2525

26-
// This module is originally based on https://observablehq.com/@jrus/cam16.
27-
// https://rawpedia.rawtherapee.com/CIECAM02 is also informative.
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
2839

2940
pub(crate) fn xyz_to_cam16<T>(
3041
xyz: Xyz<white_point::Any, T>,
@@ -268,9 +279,9 @@ where
268279
// Compute dependent parameters.
269280
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.
270281
let l_a = parameters.adapting_luminance;
271-
let y_b = parameters.background_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.
272283
let y_w = xyz_w.y.clone();
273-
let surround = parameters.surround.into_value();
284+
let surround = parameters.surround.into_percent() * T::from_f64(0.1);
274285
let c = lazy_select! {
275286
if surround.gt_eq(&T::one()) => lerp(
276287
T::from_f64(0.59),
@@ -306,17 +317,18 @@ where
306317
let n_bb = T::from_f64(0.725) * n.clone().powf(T::from_f64(-0.2)); // Chromatic induction factors
307318
let n_cb = n_bb.clone();
308319
// Illuminant discounting (adaptation). Fully adapted = 1
309-
let d = if !parameters.discounting {
310-
let d = f
311-
* (T::one()
320+
let d = match parameters.discounting {
321+
super::Discounting::Auto => {
322+
// The default D function.
323+
f * (T::one()
312324
- T::one() / T::from_f64(3.6)
313-
* Exp::exp((-l_a - T::from_f64(42.0)) / T::from_f64(92.0)));
314-
315-
clamp(d, T::zero(), T::one())
316-
} else {
317-
T::one()
325+
* Exp::exp((-l_a - T::from_f64(42.0)) / T::from_f64(92.0)))
326+
}
327+
super::Discounting::Custom(degree) => degree,
318328
};
319329

330+
let d = clamp(d, T::zero(), T::one());
331+
320332
let rgb_w = m16(xyz_w); // Cone responses of the white point
321333
let d_rgb = map3(rgb_w.clone(), |c_w| {
322334
lerp(T::one(), y_w.clone() / c_w, d.clone())

palette/src/cam16/parameters.rs

+54-46
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
num::{
66
Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt, Zero,
77
},
8-
white_point::{self, WhitePoint, D65},
8+
white_point::{self, WhitePoint},
99
Xyz,
1010
};
1111

@@ -56,31 +56,36 @@ pub type ParametersDynamicWp<T> = Parameters<Xyz<white_point::Any, T>, T>;
5656
#[derive(Clone, Copy)]
5757
#[non_exhaustive]
5858
pub struct Parameters<WpParam, T> {
59-
/// The reference white point. Defaults to `Wp` when it implements
60-
/// [`WhitePoint`], or [`D65`] when `Wp` is [`white_point::Any`]. It can
61-
/// also be set to a custom value if `Wp` results in the wrong white point.
59+
/// White point of the test illuminant, *X<sub>w</sub>* *Y<sub>w</sub>*
60+
/// *Z<sub>w</sub>*. *Y<sub>w</sub>* should be normalized to 1.0.
61+
///
62+
/// Defaults to `Wp` when it implements [`WhitePoint`]. It can also be set
63+
/// to a custom value if `Wp` results in the wrong white point.
6264
pub white_point: WpParam,
6365

64-
/// The average luminance of the environment (*L<sub>A</sub>*) in
65-
/// *cd/m<sup>2</sup>* (nits). Under a “gray world” assumption this is 20%
66-
/// of the luminance of a white reference. Defaults to `T::default()` (0.0
67-
/// for `f32` and `f64`).
66+
/// The average luminance of the environment (test adapting field)
67+
/// (*L<sub>A</sub>*) in *cd/m<sup>2</sup>* (nits).
68+
///
69+
/// Under a “gray world” assumption this is 20% of the luminance of a white
70+
/// reference. Defaults to `T::default()` (0.0 for `f32` and `f64`).
6871
pub adapting_luminance: T,
6972

70-
/// The relative luminance of the nearby background (*Y<sub>b</sub>*), out
71-
/// to 10°, on a scale of 0 to 100. Defaults to `T::default()` (0.0 for
72-
/// `f32` and `f64`).
73+
/// The luminance factor of the background (*Y<sub>b</sub>*), on a scale
74+
/// from `0.0` to `1.0` (relative to *Y<sub>w</sub>* = 1.0). Medium grey
75+
/// would be `0.2`.
76+
///
77+
/// Defaults to `T::default()` (0.0 for `f32` and `f64`).
7378
pub background_luminance: T,
7479

75-
/// A description of the peripheral area, with a value from `0` to `2`. Any
76-
/// value outside that range will be clamped to `0` or `2`. It has presets
77-
/// for "dark", "dim" and "average". Defaults to "average" (`2`).
80+
/// A description of the peripheral area, with a value from 0% to 20%. Any
81+
/// value outside that range will be clamped to 0% or 20%. It has presets
82+
/// for "dark", "dim" and "average". Defaults to "average" (20%).
7883
pub surround: Surround<T>,
7984

80-
/// Set to `true` to assume that the observer's eyes have fully adapted to
81-
/// the illuminant. The degree of discounting will be set based on the other
82-
/// parameters. Defaults to `false`.
83-
pub discounting: bool,
85+
/// The degree of discounting of (or adaptation to) the reference
86+
/// illuminant. Defaults to `Auto`, making the degree of discounting depend
87+
/// on the other parameters, but can be customized if necessary.
88+
pub discounting: Discounting<T>,
8489
}
8590

8691
impl<WpParam, T> Parameters<WpParam, T>
@@ -144,7 +149,7 @@ impl<Wp> Parameters<StaticWp<Wp>, f64> {
144149
adapting_luminance: 40.0f64,
145150
background_luminance: 20.0f64,
146151
surround: Surround::Average,
147-
discounting: false,
152+
discounting: Discounting::Auto,
148153
};
149154
}
150155

@@ -159,23 +164,7 @@ where
159164
adapting_luminance: T::default(),
160165
background_luminance: T::default(),
161166
surround: Surround::Average,
162-
discounting: false,
163-
}
164-
}
165-
}
166-
167-
impl<T> Default for Parameters<Xyz<white_point::Any, T>, T>
168-
where
169-
T: Real + Default,
170-
{
171-
#[inline]
172-
fn default() -> Self {
173-
Self {
174-
white_point: D65::get_xyz(),
175-
adapting_luminance: T::default(),
176-
background_luminance: T::default(),
177-
surround: Surround::Average,
178-
discounting: false,
167+
discounting: Discounting::Auto,
179168
}
180169
}
181170
}
@@ -235,35 +224,54 @@ where
235224
#[non_exhaustive]
236225
pub enum Surround<T> {
237226
/// Represents a dark room, such as a movie theatre. Corresponds to a
238-
/// surround value of `0`.
227+
/// surround value of 0%.
239228
Dark,
240229

241230
/// Represents a dimly lit room with a bright TV or monitor. Corresponds to
242-
/// a surround value of `1`.
231+
/// a surround value of 10%.
243232
Dim,
244233

245-
/// Represents a surface color. Corresponds to a surround value of `2`.
234+
/// Represents a surface color, such as a print on a 20% reflective,
235+
/// uniformly lit background surface. Corresponds to a surround value of
236+
/// 20%.
246237
Average,
247238

248-
/// Any custom value from `0` to `2`. Any value outside that range will be
249-
/// clamped to either `0` or `2`.
250-
Custom(T),
239+
/// Any custom value from 0% to 20%. Any value outside that range will be
240+
/// clamped to either `0.0` or `20.0`.
241+
Percent(T),
251242
}
252243

253244
impl<T> Surround<T> {
254-
pub(crate) fn into_value(self) -> T
245+
pub(crate) fn into_percent(self) -> T
255246
where
256247
T: Real + Clamp,
257248
{
258249
match self {
259250
Surround::Dark => T::from_f64(0.0),
260-
Surround::Dim => T::from_f64(1.0),
261-
Surround::Average => T::from_f64(2.0),
262-
Surround::Custom(value) => value.clamp(T::from_f64(0.0), T::from_f64(2.0)),
251+
Surround::Dim => T::from_f64(10.0),
252+
Surround::Average => T::from_f64(20.0),
253+
Surround::Percent(value) => value.clamp(T::from_f64(0.0), T::from_f64(20.0)),
263254
}
264255
}
265256
}
266257

258+
/// The degree of discounting of (or adaptation to) the illuminant.
259+
///
260+
/// See also: https://en.wikipedia.org/wiki/CIECAM02#CAT02.
261+
#[derive(Clone, Copy)]
262+
#[non_exhaustive]
263+
pub enum Discounting<T> {
264+
/// Uses luminance levels and surround conditions to calculate the
265+
/// discounting, using the original CIECAM16 *D* function. Ranges from
266+
/// `0.65` to `1.0`.
267+
Auto,
268+
269+
/// A value between `0.0` and `1.0`, where `0.0` represents no adaptation,
270+
/// and `1.0` represents that the observer's vision is fully adapted to the
271+
/// illuminant. Values outside that range will be clamped.
272+
Custom(T),
273+
}
274+
267275
/// A trait for types that can be used as white point parameters in
268276
/// [`Parameters`].
269277
pub trait WhitePointParameter<T> {

0 commit comments

Comments
 (0)