palette/cam16/parameters.rs
1use core::marker::PhantomData;
2
3use crate::{
4 bool_mask::LazySelect,
5 num::{
6 Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt, Zero,
7 },
8 white_point::{self, WhitePoint},
9 Xyz,
10};
11
12/// Parameters for CAM16 that describe the viewing conditions.
13///
14/// These parameters describe the viewing conditions for a more accurate color
15/// appearance metric. The CAM16 attributes and derived values are only really
16/// comparable if they were calculated with the same parameters. The parameters
17/// are, however, too dynamic to all be part of the type parameters of
18/// [`Cam16`][super::Cam16].
19///
20/// The default values are mostly a "blank slate", with a couple of educated
21/// guesses. Be sure to at least customize the luminances according to the
22/// expected environment:
23///
24/// ```
25/// use palette::{Srgb, Xyz, IntoColor, cam16::{Parameters, Surround, Cam16}};
26///
27/// // 40 nits, 50% background luminance and a dim surrounding:
28/// let mut example_parameters = Parameters::default_static_wp(40.0);
29/// example_parameters.background_luminance = 0.5;
30/// example_parameters.surround = Surround::Dim;
31///
32/// let example_color_xyz = Srgb::from(0x5588cc).into_linear().into_color();
33/// let cam16: Cam16<f64> = Cam16::from_xyz(example_color_xyz, example_parameters);
34/// ```
35///
36/// See also Moroney (2000) [Usage Guidelines for CIECAM97s][moroney_2000] for
37/// more information and advice on how to customize these parameters.
38///
39/// [moroney_2000]:
40/// https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2000/PICS-0-81/1611.pdf
41#[derive(Clone, Copy)]
42#[non_exhaustive]
43pub struct Parameters<WpParam, T> {
44 /// White point of the test illuminant, *X<sub>w</sub>* *Y<sub>w</sub>*
45 /// *Z<sub>w</sub>*. *Y<sub>w</sub>* should be normalized to 1.0.
46 ///
47 /// Defaults to `Wp` when it implements [`WhitePoint`]. It can also be set
48 /// to a custom value if `Wp` results in the wrong white point.
49 pub white_point: WpParam,
50
51 /// The average luminance of the environment (test adapting field)
52 /// (*L<sub>A</sub>*) in *cd/m<sup>2</sup>* (nits).
53 ///
54 /// Under a “gray world” assumption this is 20% of the luminance of the
55 /// reference white.
56 pub adapting_luminance: T,
57
58 /// The luminance factor of the background (*Y<sub>b</sub>*), on a scale
59 /// from `0.0` to `1.0` (relative to *Y<sub>w</sub>* = 1.0).
60 ///
61 /// Defaults to `0.2`, medium grey.
62 pub background_luminance: T,
63
64 /// A description of the peripheral area, with a value from 0% to 20%. Any
65 /// value outside that range will be clamped to 0% or 20%. It has presets
66 /// for "dark", "dim" and "average".
67 ///
68 /// Defaults to "average" (20%).
69 pub surround: Surround<T>,
70
71 /// The degree of discounting of (or adaptation to) the reference
72 /// illuminant. Defaults to `Auto`, making the degree of discounting depend
73 /// on the other parameters, but can be customized if necessary.
74 pub discounting: Discounting<T>,
75}
76
77impl<WpParam, T> Parameters<WpParam, T>
78where
79 WpParam: WhitePointParameter<T>,
80{
81 fn into_any_white_point(self) -> Parameters<Xyz<white_point::Any, T>, T> {
82 Parameters {
83 white_point: self.white_point.into_xyz(),
84 adapting_luminance: self.adapting_luminance,
85 background_luminance: self.background_luminance,
86 surround: self.surround,
87 discounting: self.discounting,
88 }
89 }
90}
91
92impl<Wp, T> Parameters<StaticWp<Wp>, T> {
93 /// Creates a new set of parameters with a static white point and their
94 /// default values set.
95 ///
96 /// These parameters may need to be further customized according to the
97 /// viewing conditions.
98 #[inline]
99 pub fn default_static_wp(adapting_luminance: T) -> Self
100 where
101 T: Real,
102 {
103 Self {
104 white_point: StaticWp(PhantomData),
105 adapting_luminance,
106 background_luminance: T::from_f64(0.2),
107 surround: Surround::Average,
108 discounting: Discounting::Auto,
109 }
110 }
111}
112
113impl<T> Parameters<Xyz<white_point::Any, T>, T> {
114 /// Creates a new set of parameters with a dynamic white point and their
115 /// default values set.
116 ///
117 /// These parameters may need to be further customized according to the
118 /// viewing conditions.
119 #[inline]
120 pub fn default_dynamic_wp(white_point: Xyz<white_point::Any, T>, adapting_luminance: T) -> Self
121 where
122 T: Real,
123 {
124 Self {
125 white_point,
126 adapting_luminance,
127 background_luminance: T::from_f64(0.2),
128 surround: Surround::Average,
129 discounting: Discounting::Auto,
130 }
131 }
132}
133
134impl<WpParam, T> Parameters<WpParam, T> {
135 /// Pre-bakes the parameters to avoid repeating parts of the calculaitons
136 /// when converting to and from CAM16.
137 pub fn bake(self) -> BakedParameters<WpParam, T>
138 where
139 BakedParameters<WpParam, T>: From<Self>,
140 {
141 self.into()
142 }
143}
144
145#[cfg(all(test, feature = "approx"))]
146impl<Wp> Parameters<StaticWp<Wp>, f64> {
147 /// Only used in unit tests and corresponds to the defaults from https://observablehq.com/@jrus/cam16.
148 pub(crate) const TEST_DEFAULTS: Self = Self {
149 white_point: StaticWp(PhantomData),
150 adapting_luminance: 40.0f64,
151 background_luminance: 0.2f64, // 20 / 100, since our XYZ is in the range from 0.0 to 1.0
152 surround: Surround::Average,
153 discounting: Discounting::Auto,
154 };
155}
156
157/// Pre-calculated variables for CAM16, that only depend on the viewing
158/// conditions.
159///
160/// Derived from [`Parameters`], the `BakedParameters` can help reducing the
161/// amount of repeated work required for converting multiple colors.
162pub struct BakedParameters<WpParam, T> {
163 pub(crate) inner: super::math::DependentParameters<T>,
164 white_point: PhantomData<WpParam>,
165}
166
167impl<WpParam, T> Clone for BakedParameters<WpParam, T>
168where
169 T: Clone,
170{
171 fn clone(&self) -> Self {
172 Self {
173 inner: self.inner.clone(),
174 white_point: PhantomData,
175 }
176 }
177}
178
179impl<WpParam, T> Copy for BakedParameters<WpParam, T> where T: Copy {}
180
181impl<WpParam, T> From<Parameters<WpParam, T>> for BakedParameters<WpParam, T>
182where
183 WpParam: WhitePointParameter<T>,
184 T: Real
185 + FromScalar<Scalar = T>
186 + One
187 + Zero
188 + Clamp
189 + PartialCmp
190 + Arithmetics
191 + Powf
192 + Sqrt
193 + Exp
194 + Abs
195 + Signum
196 + Clone,
197 T::Mask: LazySelect<T>,
198{
199 fn from(value: Parameters<WpParam, T>) -> Self {
200 Self {
201 inner: super::math::prepare_parameters(value.into_any_white_point()),
202 white_point: PhantomData,
203 }
204 }
205}
206
207/// A description of the peripheral area.
208#[derive(Clone, Copy)]
209#[non_exhaustive]
210pub enum Surround<T> {
211 /// Represents a dark room, such as a movie theatre. Corresponds to a
212 /// surround value of 0%.
213 Dark,
214
215 /// Represents a dimly lit room with a bright TV or monitor. Corresponds to
216 /// a surround value of 10%.
217 Dim,
218
219 /// Represents a surface color, such as a print on a 20% reflective,
220 /// uniformly lit background surface. Corresponds to a surround value of
221 /// 20%.
222 Average,
223
224 /// Any custom value from 0% to 20%. Any value outside that range will be
225 /// clamped to either `0.0` or `20.0`.
226 Percent(T),
227}
228
229impl<T> Surround<T> {
230 pub(crate) fn into_percent(self) -> T
231 where
232 T: Real + Clamp,
233 {
234 match self {
235 Surround::Dark => T::from_f64(0.0),
236 Surround::Dim => T::from_f64(10.0),
237 Surround::Average => T::from_f64(20.0),
238 Surround::Percent(value) => value.clamp(T::from_f64(0.0), T::from_f64(20.0)),
239 }
240 }
241}
242
243/// The degree of discounting of (or adaptation to) the illuminant.
244///
245/// See also: <https://en.wikipedia.org/wiki/CIECAM02#CAT02>.
246#[derive(Clone, Copy)]
247#[non_exhaustive]
248pub enum Discounting<T> {
249 /// Uses luminance levels and surround conditions to calculate the
250 /// discounting, using the original CIECAM16 *D* function. Ranges from
251 /// `0.65` to `1.0`.
252 Auto,
253
254 /// A value between `0.0` and `1.0`, where `0.0` represents no adaptation,
255 /// and `1.0` represents that the observer's vision is fully adapted to the
256 /// illuminant. Values outside that range will be clamped.
257 Custom(T),
258}
259
260/// A trait for types that can be used as white point parameters in
261/// [`Parameters`].
262pub trait WhitePointParameter<T> {
263 /// The static representation of this white point, or [`white_point::Any`]
264 /// if it's dynamic.
265 type StaticWp;
266
267 /// Returns the XYZ value for this white point.
268 fn into_xyz(self) -> Xyz<white_point::Any, T>;
269}
270
271impl<T> WhitePointParameter<T> for Xyz<white_point::Any, T> {
272 type StaticWp = white_point::Any;
273
274 fn into_xyz(self) -> Xyz<white_point::Any, T> {
275 self
276 }
277}
278
279/// Represents a static white point in [`Parameters`], as opposed to a dynamic
280/// [`Xyz`] value.
281pub struct StaticWp<Wp>(PhantomData<Wp>);
282
283impl<T, Wp> WhitePointParameter<T> for StaticWp<Wp>
284where
285 Wp: WhitePoint<T>,
286{
287 type StaticWp = Wp;
288
289 fn into_xyz(self) -> Xyz<white_point::Any, T> {
290 Wp::get_xyz()
291 }
292}
293
294impl<Wp> Clone for StaticWp<Wp> {
295 fn clone(&self) -> Self {
296 *self
297 }
298}
299
300impl<Wp> Copy for StaticWp<Wp> {}