palette/
chromatic_adaptation.rs

1//! Convert colors from one reference white point to another
2//!
3//! Chromatic adaptation is the human visual system’s ability to adjust to
4//! changes in illumination in order to preserve the appearance of object
5//! colors. It is responsible for the stable appearance of object colours
6//! despite the wide variation of light which might be reflected from an object
7//! and observed by our eyes.
8//!
9//! This library provides three methods for chromatic adaptation Bradford (which
10//! is the default), VonKries and XyzScaling
11//!
12//! ```
13//! use palette::Xyz;
14//! use palette::white_point::{A, C};
15//! use palette::chromatic_adaptation::AdaptInto;
16//!
17//!
18//! let a = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
19//!
20//! //Will convert Xyz<A, f32> to Xyz<C, f32> using Bradford chromatic adaptation
21//! let c: Xyz<C, f32> = a.adapt_into();
22//!
23//! //Should print {x: 0.257963, y: 0.139776,z: 0.058825}
24//! println!("{:?}", c)
25//! ```
26
27use crate::{
28    convert::{FromColorUnclamped, IntoColorUnclamped},
29    matrix::{multiply_3x3, multiply_xyz, Mat3},
30    num::{Arithmetics, Real, Zero},
31    white_point::{Any, WhitePoint},
32    Xyz,
33};
34
35/// Chromatic adaptation methods implemented in the library
36pub enum Method {
37    /// Bradford chromatic adaptation method
38    Bradford,
39    /// VonKries chromatic adaptation method
40    VonKries,
41    /// XyzScaling chromatic adaptation method
42    XyzScaling,
43}
44
45/// Holds the matrix coefficients for the chromatic adaptation methods
46pub struct ConeResponseMatrices<T> {
47    ///3x3 matrix for the cone response domains
48    pub ma: Mat3<T>,
49    ///3x3 matrix for the inverse of the cone response domains
50    pub inv_ma: Mat3<T>,
51}
52
53/// Generates a conversion matrix to convert the Xyz tristimulus values from
54/// one illuminant to another (`source_wp` to `destination_wp`)
55pub trait TransformMatrix<T>
56where
57    T: Zero + Arithmetics + Clone,
58{
59    /// Get the cone response functions for the chromatic adaptation method
60    #[must_use]
61    fn get_cone_response(&self) -> ConeResponseMatrices<T>;
62
63    /// Generates a 3x3 transformation matrix to convert color from one
64    /// reference white point to another with the given cone_response
65    #[must_use]
66    fn generate_transform_matrix(
67        &self,
68        source_wp: Xyz<Any, T>,
69        destination_wp: Xyz<Any, T>,
70    ) -> Mat3<T> {
71        let adapt = self.get_cone_response();
72
73        let resp_src = multiply_xyz(adapt.ma.clone(), source_wp);
74        let resp_dst = multiply_xyz(adapt.ma.clone(), destination_wp);
75
76        #[rustfmt::skip]
77        let resp = [
78            resp_dst.x / resp_src.x, T::zero(), T::zero(),
79            T::zero(), resp_dst.y / resp_src.y, T::zero(),
80            T::zero(), T::zero(), resp_dst.z / resp_src.z,
81        ];
82
83        let tmp = multiply_3x3(resp, adapt.ma);
84        multiply_3x3(adapt.inv_ma, tmp)
85    }
86}
87
88impl<T> TransformMatrix<T> for Method
89where
90    T: Real + Zero + Arithmetics + Clone,
91{
92    #[rustfmt::skip]
93    #[inline]
94    fn get_cone_response(&self) -> ConeResponseMatrices<T> {
95        match *self {
96             Method::Bradford => {
97                ConeResponseMatrices::<T> {
98                    ma: [
99                        T::from_f64(0.8951000), T::from_f64(0.2664000), T::from_f64(-0.1614000),
100                        T::from_f64(-0.7502000), T::from_f64(1.7135000), T::from_f64(0.0367000),
101                        T::from_f64(0.0389000), T::from_f64(-0.0685000), T::from_f64(1.0296000)
102                    ],
103                    inv_ma: [
104                        T::from_f64(0.9869929), T::from_f64(-0.1470543), T::from_f64(0.1599627),
105                        T::from_f64(0.4323053), T::from_f64(0.5183603), T::from_f64(0.0492912),
106                        T::from_f64(-0.0085287), T::from_f64(0.0400428), T::from_f64(0.9684867)
107                    ],
108                }
109            }
110             Method::VonKries => {
111                ConeResponseMatrices::<T> {
112                    ma: [
113                        T::from_f64(0.4002400), T::from_f64(0.7076000), T::from_f64(-0.0808100),
114                        T::from_f64(-0.2263000), T::from_f64(1.1653200), T::from_f64(0.0457000),
115                        T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(0.9182200)
116                    ],
117                    inv_ma: [
118                        T::from_f64(1.8599364), T::from_f64(-1.1293816), T::from_f64(0.2198974),
119                        T::from_f64(0.3611914), T::from_f64(0.6388125), T::from_f64(-0.0000064),
120                        T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(1.0890636)
121                    ],
122                }
123            }
124             Method::XyzScaling => {
125                ConeResponseMatrices::<T> {
126                    ma: [
127                        T::from_f64(1.0000000), T::from_f64(0.0000000), T::from_f64(0.0000000),
128                        T::from_f64(0.0000000), T::from_f64(1.0000000), T::from_f64(0.0000000),
129                        T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(1.0000000)
130                    ],
131                    inv_ma: [
132                        T::from_f64(1.0000000), T::from_f64(0.0000000), T::from_f64(0.0000000),
133                        T::from_f64(0.0000000), T::from_f64(1.0000000), T::from_f64(0.0000000),
134                        T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(1.0000000)
135                    ],
136                }
137            }
138        }
139    }
140}
141
142/// Trait to convert color from one reference white point to another
143///
144/// Converts a color from the source white point (Swp) to the destination white
145/// point (Dwp). Uses the bradford method for conversion by default.
146pub trait AdaptFrom<S, Swp, Dwp, T>: Sized
147where
148    T: Real + Zero + Arithmetics + Clone,
149    Swp: WhitePoint<T>,
150    Dwp: WhitePoint<T>,
151{
152    /// Convert the source color to the destination color using the bradford
153    /// method by default.
154    #[must_use]
155    #[inline]
156    fn adapt_from(color: S) -> Self {
157        Self::adapt_from_using(color, Method::Bradford)
158    }
159    /// Convert the source color to the destination color using the specified
160    /// method.
161    #[must_use]
162    fn adapt_from_using<M: TransformMatrix<T>>(color: S, method: M) -> Self;
163}
164
165impl<S, D, Swp, Dwp, T> AdaptFrom<S, Swp, Dwp, T> for D
166where
167    T: Real + Zero + Arithmetics + Clone,
168    Swp: WhitePoint<T>,
169    Dwp: WhitePoint<T>,
170    S: IntoColorUnclamped<Xyz<Swp, T>>,
171    D: FromColorUnclamped<Xyz<Dwp, T>>,
172{
173    #[inline]
174    fn adapt_from_using<M: TransformMatrix<T>>(color: S, method: M) -> D {
175        let src_xyz = color.into_color_unclamped().with_white_point();
176        let transform_matrix = method.generate_transform_matrix(Swp::get_xyz(), Dwp::get_xyz());
177        let dst_xyz = multiply_xyz(transform_matrix, src_xyz);
178        D::from_color_unclamped(dst_xyz.with_white_point())
179    }
180}
181
182/// Trait to convert color with one reference white point into another
183///
184/// Converts a color with the source white point (Swp) into the destination
185/// white point (Dwp). Uses the bradford method for conversion by default.
186pub trait AdaptInto<D, Swp, Dwp, T>: Sized
187where
188    T: Real + Zero + Arithmetics + Clone,
189    Swp: WhitePoint<T>,
190    Dwp: WhitePoint<T>,
191{
192    /// Convert the source color to the destination color using the bradford
193    /// method by default.
194    #[must_use]
195    #[inline]
196    fn adapt_into(self) -> D {
197        self.adapt_into_using(Method::Bradford)
198    }
199    /// Convert the source color to the destination color using the specified
200    /// method.
201    #[must_use]
202    fn adapt_into_using<M: TransformMatrix<T>>(self, method: M) -> D;
203}
204
205impl<S, D, Swp, Dwp, T> AdaptInto<D, Swp, Dwp, T> for S
206where
207    T: Real + Zero + Arithmetics + Clone,
208    Swp: WhitePoint<T>,
209    Dwp: WhitePoint<T>,
210    D: AdaptFrom<S, Swp, Dwp, T>,
211{
212    #[inline]
213    fn adapt_into_using<M: TransformMatrix<T>>(self, method: M) -> D {
214        D::adapt_from_using(self, method)
215    }
216}
217
218#[cfg(feature = "approx")]
219#[cfg(test)]
220mod test {
221    use super::{AdaptFrom, AdaptInto, Method, TransformMatrix};
222    use crate::white_point::{WhitePoint, A, C, D50, D65};
223    use crate::Xyz;
224
225    #[test]
226    fn d65_to_d50_matrix_xyz_scaling() {
227        let expected = [
228            1.0144665, 0.0000000, 0.0000000, 0.0000000, 1.0000000, 0.0000000, 0.0000000, 0.0000000,
229            0.7578869,
230        ];
231        let xyz_scaling = Method::XyzScaling;
232        let computed = xyz_scaling.generate_transform_matrix(D65::get_xyz(), D50::get_xyz());
233        for (e, c) in expected.iter().zip(computed.iter()) {
234            assert_relative_eq!(e, c, epsilon = 0.0001)
235        }
236    }
237    #[test]
238    fn d65_to_d50_matrix_von_kries() {
239        let expected = [
240            1.0160803, 0.0552297, -0.0521326, 0.0060666, 0.9955661, -0.0012235, 0.0000000,
241            0.0000000, 0.7578869,
242        ];
243        let von_kries = Method::VonKries;
244        let computed = von_kries.generate_transform_matrix(D65::get_xyz(), D50::get_xyz());
245        for (e, c) in expected.iter().zip(computed.iter()) {
246            assert_relative_eq!(e, c, epsilon = 0.0001)
247        }
248    }
249    #[test]
250    fn d65_to_d50_matrix_bradford() {
251        let expected = [
252            1.0478112, 0.0228866, -0.0501270, 0.0295424, 0.9904844, -0.0170491, -0.0092345,
253            0.0150436, 0.7521316,
254        ];
255        let bradford = Method::Bradford;
256        let computed = bradford.generate_transform_matrix(D65::get_xyz(), D50::get_xyz());
257        for (e, c) in expected.iter().zip(computed.iter()) {
258            assert_relative_eq!(e, c, epsilon = 0.0001)
259        }
260    }
261
262    #[test]
263    fn chromatic_adaptation_from_a_to_c() {
264        let input_a = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
265
266        let expected_bradford = Xyz::<C, f32>::new(0.257963, 0.139776, 0.058825);
267        let expected_vonkries = Xyz::<C, f32>::new(0.268446, 0.159139, 0.052843);
268        let expected_xyz_scaling = Xyz::<C, f32>::new(0.281868, 0.162732, 0.052844);
269
270        let computed_bradford: Xyz<C, f32> = Xyz::adapt_from(input_a);
271        assert_relative_eq!(expected_bradford, computed_bradford, epsilon = 0.0001);
272
273        let computed_vonkries: Xyz<C, f32> = Xyz::adapt_from_using(input_a, Method::VonKries);
274        assert_relative_eq!(expected_vonkries, computed_vonkries, epsilon = 0.0001);
275
276        let computed_xyz_scaling: Xyz<C, _> = Xyz::adapt_from_using(input_a, Method::XyzScaling);
277        assert_relative_eq!(expected_xyz_scaling, computed_xyz_scaling, epsilon = 0.0001);
278    }
279
280    #[test]
281    fn chromatic_adaptation_into_a_to_c() {
282        let input_a = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
283
284        let expected_bradford = Xyz::<C, f32>::new(0.257963, 0.139776, 0.058825);
285        let expected_vonkries = Xyz::<C, f32>::new(0.268446, 0.159139, 0.052843);
286        let expected_xyz_scaling = Xyz::<C, f32>::new(0.281868, 0.162732, 0.052844);
287
288        let computed_bradford: Xyz<C, f32> = input_a.adapt_into();
289        assert_relative_eq!(expected_bradford, computed_bradford, epsilon = 0.0001);
290
291        let computed_vonkries: Xyz<C, f32> = input_a.adapt_into_using(Method::VonKries);
292        assert_relative_eq!(expected_vonkries, computed_vonkries, epsilon = 0.0001);
293
294        let computed_xyz_scaling: Xyz<C, _> = input_a.adapt_into_using(Method::XyzScaling);
295        assert_relative_eq!(expected_xyz_scaling, computed_xyz_scaling, epsilon = 0.0001);
296    }
297}