Skip to content

Commit

Permalink
Improve CAM16 parameters and adjust documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ogeon committed Apr 7, 2024
1 parent 22880dd commit 285cb2a
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 60 deletions.
36 changes: 24 additions & 12 deletions palette/src/cam16/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,19 @@ use self::{chromaticity::ChromaticityType, luminance::LuminanceType};
pub(crate) mod chromaticity;
pub(crate) mod luminance;

// This module is originally based on https://observablehq.com/@jrus/cam16.
// https://rawpedia.rawtherapee.com/CIECAM02 is also informative.
// This module is originally based on these sources:
// - https://observablehq.com/@jrus/cam16
// - "Comprehensive color solutions: CAM16, CAT16, and CAM16-UCS" by Li C, Li Z,
// Wang Z, et al.
// (https://www.researchgate.net/publication/318152296_Comprehensive_color_solutions_CAM16_CAT16_and_CAM16-UCS)
// - "Algorithmic improvements for the CIECAM02 and CAM16 color appearance
// models" by Nico Schlömer (https://arxiv.org/pdf/1802.06067.pdf)
// - https://rawpedia.rawtherapee.com/CIECAM02.
// - "Usage Guidelines for CIECAM97s" by Nathan Moroney
// (https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2000/PICS-0-81/1611.pdf)
// - "CIECAM02 and Its Recent Developments" by Ming Ronnier Luo and Changjun Li
// (https://cielab.xyz/pdf/CIECAM02_and_Its_Recent_Developments.pdf)
// - https://en.wikipedia.org/wiki/CIECAM02

pub(crate) fn xyz_to_cam16<T>(
xyz: Xyz<white_point::Any, T>,
Expand Down Expand Up @@ -268,9 +279,9 @@ where
// Compute dependent parameters.
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.
let l_a = parameters.adapting_luminance;
let y_b = parameters.background_luminance;
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.
let y_w = xyz_w.y.clone();
let surround = parameters.surround.into_value();
let surround = parameters.surround.into_percent() * T::from_f64(0.1);
let c = lazy_select! {
if surround.gt_eq(&T::one()) => lerp(
T::from_f64(0.59),
Expand Down Expand Up @@ -306,17 +317,18 @@ where
let n_bb = T::from_f64(0.725) * n.clone().powf(T::from_f64(-0.2)); // Chromatic induction factors
let n_cb = n_bb.clone();
// Illuminant discounting (adaptation). Fully adapted = 1
let d = if !parameters.discounting {
let d = f
* (T::one()
let d = match parameters.discounting {
super::Discounting::Auto => {
// The default D function.
f * (T::one()
- T::one() / T::from_f64(3.6)
* Exp::exp((-l_a - T::from_f64(42.0)) / T::from_f64(92.0)));

clamp(d, T::zero(), T::one())
} else {
T::one()
* Exp::exp((-l_a - T::from_f64(42.0)) / T::from_f64(92.0)))
}
super::Discounting::Custom(degree) => degree,
};

let d = clamp(d, T::zero(), T::one());

let rgb_w = m16(xyz_w); // Cone responses of the white point
let d_rgb = map3(rgb_w.clone(), |c_w| {
lerp(T::one(), y_w.clone() / c_w, d.clone())
Expand Down
104 changes: 56 additions & 48 deletions palette/src/cam16/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
num::{
Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt, Zero,
},
white_point::{self, WhitePoint, D65},
white_point::{self, WhitePoint},
Xyz,
};

Expand Down Expand Up @@ -42,7 +42,7 @@ pub type ParametersDynamicWp<T> = Parameters<Xyz<white_point::Any, T>, T>;
///
/// let mut example_parameters = Parameters::default_static_wp();
/// example_parameters.adapting_luminance = 40.0;
/// example_parameters.background_luminance = 20.0;
/// example_parameters.background_luminance = 0.2;
///
/// let example_color_xyz = Srgb::from(0x5588cc).into_linear().into_color();
/// let cam16: Cam16<f64> = Cam16::from_xyz(example_color_xyz, example_parameters);
Expand All @@ -56,31 +56,36 @@ pub type ParametersDynamicWp<T> = Parameters<Xyz<white_point::Any, T>, T>;
#[derive(Clone, Copy)]
#[non_exhaustive]
pub struct Parameters<WpParam, T> {
/// The reference white point. Defaults to `Wp` when it implements
/// [`WhitePoint`], or [`D65`] when `Wp` is [`white_point::Any`]. It can
/// also be set to a custom value if `Wp` results in the wrong white point.
/// White point of the test illuminant, *X<sub>w</sub>* *Y<sub>w</sub>*
/// *Z<sub>w</sub>*. *Y<sub>w</sub>* should be normalized to 1.0.
///
/// Defaults to `Wp` when it implements [`WhitePoint`]. It can also be set
/// to a custom value if `Wp` results in the wrong white point.
pub white_point: WpParam,

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

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

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

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

impl<WpParam, T> Parameters<WpParam, T>
Expand Down Expand Up @@ -142,9 +147,9 @@ impl<Wp> Parameters<StaticWp<Wp>, f64> {
pub(crate) const TEST_DEFAULTS: Self = Self {
white_point: StaticWp(PhantomData),
adapting_luminance: 40.0f64,
background_luminance: 20.0f64,
background_luminance: 0.2f64, // 20 / 100, since our XYZ is in the range from 0.0 to 1.0
surround: Surround::Average,
discounting: false,
discounting: Discounting::Auto,
};
}

Expand All @@ -159,23 +164,7 @@ where
adapting_luminance: T::default(),
background_luminance: T::default(),
surround: Surround::Average,
discounting: false,
}
}
}

impl<T> Default for Parameters<Xyz<white_point::Any, T>, T>
where
T: Real + Default,
{
#[inline]
fn default() -> Self {
Self {
white_point: D65::get_xyz(),
adapting_luminance: T::default(),
background_luminance: T::default(),
surround: Surround::Average,
discounting: false,
discounting: Discounting::Auto,
}
}
}
Expand Down Expand Up @@ -235,35 +224,54 @@ where
#[non_exhaustive]
pub enum Surround<T> {
/// Represents a dark room, such as a movie theatre. Corresponds to a
/// surround value of `0`.
/// surround value of 0%.
Dark,

/// Represents a dimly lit room with a bright TV or monitor. Corresponds to
/// a surround value of `1`.
/// a surround value of 10%.
Dim,

/// Represents a surface color. Corresponds to a surround value of `2`.
/// Represents a surface color, such as a print on a 20% reflective,
/// uniformly lit background surface. Corresponds to a surround value of
/// 20%.
Average,

/// Any custom value from `0` to `2`. Any value outside that range will be
/// clamped to either `0` or `2`.
Custom(T),
/// Any custom value from 0% to 20%. Any value outside that range will be
/// clamped to either `0.0` or `20.0`.
Percent(T),
}

impl<T> Surround<T> {
pub(crate) fn into_value(self) -> T
pub(crate) fn into_percent(self) -> T
where
T: Real + Clamp,
{
match self {
Surround::Dark => T::from_f64(0.0),
Surround::Dim => T::from_f64(1.0),
Surround::Average => T::from_f64(2.0),
Surround::Custom(value) => value.clamp(T::from_f64(0.0), T::from_f64(2.0)),
Surround::Dim => T::from_f64(10.0),
Surround::Average => T::from_f64(20.0),
Surround::Percent(value) => value.clamp(T::from_f64(0.0), T::from_f64(20.0)),
}
}
}

/// The degree of discounting of (or adaptation to) the illuminant.
///
/// See also: https://en.wikipedia.org/wiki/CIECAM02#CAT02.
#[derive(Clone, Copy)]
#[non_exhaustive]
pub enum Discounting<T> {
/// Uses luminance levels and surround conditions to calculate the
/// discounting, using the original CIECAM16 *D* function. Ranges from
/// `0.65` to `1.0`.
Auto,

/// A value between `0.0` and `1.0`, where `0.0` represents no adaptation,
/// and `1.0` represents that the observer's vision is fully adapted to the
/// illuminant. Values outside that range will be clamped.
Custom(T),
}

/// A trait for types that can be used as white point parameters in
/// [`Parameters`].
pub trait WhitePointParameter<T> {
Expand Down

0 comments on commit 285cb2a

Please sign in to comment.