Skip to main content

palette/
chromatic_adaptation.rs

1//! Convert colors from one reference white point to another
2//!
3//! Chromatic adaptation is the ability to adjust the appearance of colors to
4//! changes in illumination. This happens naturally in our body's visual system,
5//! and can be emulated with a "chromatic adaptation transform" (CAT).
6//!
7//! This library implements a one-step adaptation transform, known as the von
8//! Kries method. It's provided as [`AdaptFromUnclamped`] or
9//! [`AdaptIntoUnclamped`] for convenience, or [`adaptation_matrix`] for control
10//! and reusability. All of them can be customized with different LMS matrices.
11//!
12//! The provided LMS matrices are:
13//!
14//! - [`Bradford`] - A "spectrally sharpened" matrix, which may improve
15//!   chromatic adaptation. This is the default for [`AdaptFromUnclamped`] and
16//!   [`AdaptIntoUnclamped`].
17//! - [`VonKries`][lms::matrix::VonKries] - Produces cone-describing LMS values,
18//!   as opposed to many other matrices, but may perform worse than other
19//!   matrices.
20//! - [`UnitMatrix`][lms::matrix::UnitMatrix] - Included for completeness, but
21//!   generally considered a bad option. Also called "XYZ scaling" or "wrong von
22//!   Kries".
23//!
24//! ```
25//! use palette::{
26//!     Xyz, white_point::{A, C},
27//!     chromatic_adaptation::AdaptIntoUnclamped,
28//! };
29//! use approx::assert_relative_eq;
30//!
31//! let input = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
32//!
33//! //Will convert Xyz<A, f32> to Xyz<C, f32> using Bradford chromatic adaptation;
34//! let output: Xyz<C, f32> = input.adapt_into_unclamped();
35//!
36//! let expected = Xyz::new(0.257963, 0.139776, 0.058825);
37//! assert_relative_eq!(output, expected, epsilon = 0.0001);
38//! ```
39
40use core::ops::Div;
41
42use crate::{
43    convert::{FromColorUnclamped, IntoColorUnclamped, Matrix3},
44    lms::{
45        self,
46        matrix::{Bradford, LmsToXyz, WithLmsMatrix, XyzToLms},
47        Lms,
48    },
49    matrix::{multiply_3x3, multiply_3x3_and_vec3, Mat3},
50    num::{Arithmetics, Real, Zero},
51    white_point::{Any, WhitePoint},
52    xyz::meta::HasXyzMeta,
53    Xyz,
54};
55
56/// Construct a one-step chromatic adaptation matrix.
57///
58/// The matrix uses the von Kries method to fully adapt a color from an input
59/// white point to an output white point, using a provided LMS matrix. See the
60/// [`chromatic_adaptation`][self] module for more details.
61///
62/// ## Static White Points
63///
64/// The `input_wp` and `output_wp` parameters represent the color "white" for
65/// the input and output colors, respectively. Passing `None` will make it use
66/// `I` and `O` to calculate the white points:
67///
68/// ```
69/// use palette::{
70///     chromatic_adaptation::adaptation_matrix,
71///     lms::matrix::Bradford,
72///     convert::Convert,
73///     white_point::{A, C},
74///     Xyz,
75/// };
76/// use approx::assert_relative_eq;
77///
78/// // Adapts from white point A to white point C:
79/// let matrix = adaptation_matrix::<f32, A, C, Bradford>(None, None);
80///
81/// // Explicit types added for illustration.
82/// let input: Xyz<A> = Xyz::new(0.315756, 0.162732, 0.015905);
83/// let output: Xyz<C> = matrix.convert(input);
84///
85/// let expected = Xyz::new(0.257963, 0.139776, 0.058825);
86/// assert_relative_eq!(output, expected, epsilon = 0.0001);
87/// ```
88///
89/// ## Dynamic White Points
90///
91/// It's also possible to use arbitrary colors as white points, as long as they
92/// are brighter than black. This can be useful for white balancing a photo,
93/// where we may want to use the same static white point for both the input and
94/// the output:
95///
96/// ```
97/// use palette::{
98///     chromatic_adaptation::adaptation_matrix,
99///     lms::matrix::Bradford,
100///     convert::{FromColorUnclampedMut, Convert},
101///     Srgb, Xyz,
102/// };
103/// use approx::assert_relative_eq;
104///
105/// fn simple_white_balance(image: &mut [Srgb<f32>]) {
106///     // Temporarily convert to Xyz:
107///     let mut image = <[Xyz<_, f32>]>::from_color_unclamped_mut(image);
108///
109///     // Find the average Xyz color:
110///     let sum = image.iter().fold(Xyz::new(0.0, 0.0, 0.0), |sum, &c| sum + c);
111///     let average = sum / image.len() as f32;
112///
113///     // Considering the average color to be "white", this matrix adapts from the
114///     // average to default sRGB white, D65:
115///     let matrix = adaptation_matrix::<_, _, _, Bradford>(Some(average), None);
116///
117///     for pixel in &mut *image {
118///         *pixel = matrix.convert(*pixel);
119///     }
120/// }
121///
122/// // Minimal test case. This one pixel becomes gray after white balancing:
123/// let mut image = [Srgb::new(0.8, 0.3, 0.9)];
124/// simple_white_balance(&mut image);
125///
126/// let expected = Srgb::new(0.524706, 0.524706, 0.524706);
127/// assert_relative_eq!(image[0], expected, epsilon = 0.00001);
128/// ```
129///
130/// See also [Wikipedia - Von Kries transform][wikipedia].
131///
132/// [wikipedia]:
133///     https://en.wikipedia.org/wiki/Chromatic_adaptation#Von_Kries_transform
134pub fn adaptation_matrix<T, I, O, M>(
135    input_wp: Option<Xyz<I, T>>,
136    output_wp: Option<Xyz<O, T>>,
137) -> Matrix3<Xyz<I, T>, Xyz<O, T>>
138where
139    T: Zero + Arithmetics + Clone,
140    I: WhitePoint<T> + HasXyzMeta<XyzMeta = I>,
141    O: WhitePoint<T> + HasXyzMeta<XyzMeta = O>,
142    M: XyzToLms<T> + LmsToXyz<T>,
143    Xyz<I, T>: IntoColorUnclamped<Lms<WithLmsMatrix<I, M>, T>>,
144    Xyz<O, T>: IntoColorUnclamped<Lms<WithLmsMatrix<O, M>, T>>,
145{
146    let input_to_lms = Lms::<WithLmsMatrix<I, M>, T>::matrix_from_xyz();
147    let lms_to_output = Xyz::<O, T>::matrix_from_lms::<WithLmsMatrix<O, M>>();
148
149    let input_wp = input_wp
150        .unwrap_or_else(|| I::get_xyz().with_white_point())
151        .normalize()
152        .into_color_unclamped();
153
154    let output_wp = output_wp
155        .unwrap_or_else(|| O::get_xyz().with_white_point())
156        .normalize()
157        .into_color_unclamped();
158
159    input_to_lms
160        .then(diagonal_matrix(input_wp, output_wp))
161        .then(lms_to_output)
162}
163
164/// Construct a diagonal matrix for full adaptation of [`Lms`] colors.
165///
166/// This is the core matrix in the von Kries adaptation method and is a central
167/// part of the matrix from [`adaptation_matrix`]. It's offered separately, as
168/// an option for building more advanced adaptation matrices.
169///
170/// The produced matrix is a diagonal matrix, containing the output white point
171/// divided by the input white point:
172///
173/// ```text
174/// [out.l / in.l,            0,            0]
175/// [           0, out.m / in.m,            0]
176/// [           0,            0, out.s / in.s]
177/// ```
178///
179/// See also [Wikipedia - Von Kries transform][wikipedia].
180///
181/// [wikipedia]:
182///     https://en.wikipedia.org/wiki/Chromatic_adaptation#Von_Kries_transform
183#[inline]
184pub fn diagonal_matrix<T, I, O>(
185    input_wp: Lms<I, T>,
186    output_wp: Lms<O, T>,
187) -> Matrix3<Lms<I, T>, Lms<O, T>>
188where
189    T: Zero + Div<Output = T>,
190{
191    let gain = output_wp / input_wp.with_meta();
192
193    #[rustfmt::skip]
194    let matrix = [
195        gain.long, T::zero(),   T::zero(),
196        T::zero(), gain.medium, T::zero(),
197        T::zero(), T::zero(),   gain.short,
198    ];
199
200    Matrix3::from_array(matrix)
201}
202
203/// A trait for unchecked conversion of one color from another via chromatic
204/// adaptation.
205///
206/// See [`FromColor`][crate::convert::FromColor],
207/// [`TryFromColor`][crate::convert::TryFromColor] and [`FromColorUnclamped`]
208/// for when there's no need for chromatic adaptation.
209///
210/// Some conversions require the reference white point to be changed, while
211/// maintaining the appearance of the color. This is called "chromatic
212/// adaptation" or "white balancing", and typically involves converting the
213/// color to the [`Lms`] color space. This trait defaults to using the
214/// [`Bradford`] matrix as part of the process, but other options are available
215/// in [`lms::matrix`].
216///
217/// The [`adaptation_matrix`] function offers more options and control. This
218/// trait can be a convenient alternative when the source and destination white
219/// points are statically known.
220pub trait AdaptFromUnclamped<T>: Sized {
221    /// The number type that's used as the color's components.
222    type Scalar;
223
224    /// Adapt a color of type `T` into a color of type `Self`, using the
225    /// [`Bradford`] matrix.
226    ///
227    /// ```
228    /// use palette::{
229    ///     Xyz, white_point::{A, C},
230    ///     chromatic_adaptation::AdaptFromUnclamped,
231    /// };
232    ///
233    /// let input = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
234    ///
235    /// //Will convert Xyz<A, f32> to Xyz<C, f32> using Bradford chromatic adaptation:
236    /// let output = Xyz::<C, f32>::adapt_from_unclamped(input);
237    /// ```
238    #[must_use]
239    #[inline]
240    fn adapt_from_unclamped(input: T) -> Self
241    where
242        Bradford: LmsToXyz<Self::Scalar> + XyzToLms<Self::Scalar>,
243    {
244        Self::adapt_from_unclamped_with::<Bradford>(input)
245    }
246
247    /// Adapt a color of type `T` into a color of type `Self`, using the custom
248    /// matrix `M`.
249    ///
250    /// ```
251    /// use palette::{
252    ///     Xyz, white_point::{A, C}, lms::matrix::VonKries,
253    ///     chromatic_adaptation::AdaptFromUnclamped,
254    /// };
255    ///
256    /// let input = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
257    ///
258    /// //Will convert Xyz<A, f32> to Xyz<C, f32> using von Kries chromatic adaptation:
259    /// let output = Xyz::<C, f32>::adapt_from_unclamped_with::<VonKries>(input);
260    /// ```
261    #[must_use]
262    fn adapt_from_unclamped_with<M>(input: T) -> Self
263    where
264        M: LmsToXyz<Self::Scalar> + XyzToLms<Self::Scalar>;
265}
266
267/// A trait for unchecked conversion of one color into another via chromatic
268/// adaptation.
269///
270/// See [`IntoColor`][crate::convert::IntoColor],
271/// [`TryIntoColor`][crate::convert::TryIntoColor] and [`IntoColorUnclamped`]
272/// for when there's no need for chromatic adaptation.
273///
274/// Some conversions require the reference white point to be changed, while
275/// maintaining the appearance of the color. This is called "chromatic
276/// adaptation" or "white balancing", and typically involves converting the
277/// color to the [`Lms`] color space. This trait defaults to using the
278/// [`Bradford`] matrix as part of the process, but other options are available
279/// in [`lms::matrix`].
280///
281/// The [`adaptation_matrix`] function offers more options and control. This
282/// trait can be a convenient alternative when the source and destination white
283/// points are statically known.
284pub trait AdaptIntoUnclamped<T>: Sized {
285    /// The number type that's used as the color's components.
286    type Scalar;
287
288    /// Adapt a color of type `Self` into a color of type `T`, using the
289    /// [`Bradford`] matrix.
290    ///
291    /// ```
292    /// use palette::{
293    ///     Xyz, white_point::{A, C},
294    ///     chromatic_adaptation::AdaptIntoUnclamped,
295    /// };
296    ///
297    /// let input = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
298    ///
299    /// //Will convert Xyz<A, f32> to Xyz<C, f32> using Bradford chromatic adaptation:
300    /// let output: Xyz<C, f32> = input.adapt_into_unclamped();
301    /// ```
302    #[must_use]
303    #[inline]
304    fn adapt_into_unclamped(self) -> T
305    where
306        Bradford: LmsToXyz<Self::Scalar> + XyzToLms<Self::Scalar>,
307    {
308        self.adapt_into_unclamped_with::<Bradford>()
309    }
310
311    /// Adapt a color of type `Self` into a color of type `T`, using the custom
312    /// matrix `M`.
313    ///
314    /// ```
315    /// use palette::{
316    ///     Xyz, white_point::{A, C}, lms::matrix::VonKries,
317    ///     chromatic_adaptation::AdaptIntoUnclamped,
318    /// };
319    ///
320    /// let input = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
321    ///
322    /// //Will convert Xyz<A, f32> to Xyz<C, f32> using von Kries chromatic adaptation:
323    /// let output: Xyz<C, f32> = input.adapt_into_unclamped_with::<VonKries>();
324    /// ```
325    #[must_use]
326    fn adapt_into_unclamped_with<M>(self) -> T
327    where
328        M: LmsToXyz<Self::Scalar> + XyzToLms<Self::Scalar>;
329}
330
331impl<T, C> AdaptIntoUnclamped<T> for C
332where
333    T: AdaptFromUnclamped<C>,
334{
335    type Scalar = T::Scalar;
336
337    #[inline]
338    fn adapt_into_unclamped_with<M>(self) -> T
339    where
340        M: LmsToXyz<Self::Scalar> + XyzToLms<Self::Scalar>,
341    {
342        T::adapt_from_unclamped_with::<M>(self)
343    }
344}
345
346/// Chromatic adaptation methods implemented in the library
347#[deprecated(
348    since = "0.7.7",
349    note = "use the options from `palette::lms::matrix` or a custom matrix"
350)]
351pub enum Method {
352    /// Bradford chromatic adaptation method
353    Bradford,
354    /// VonKries chromatic adaptation method
355    VonKries,
356    /// XyzScaling chromatic adaptation method
357    XyzScaling,
358}
359
360/// Holds the matrix coefficients for the chromatic adaptation methods
361#[deprecated(
362    since = "0.7.7",
363    note = "use the options from `palette::lms::matrix` or a custom matrix"
364)]
365pub struct ConeResponseMatrices<T> {
366    ///3x3 matrix for the cone response domains
367    pub ma: Mat3<T>,
368    ///3x3 matrix for the inverse of the cone response domains
369    pub inv_ma: Mat3<T>,
370}
371
372/// Generates a conversion matrix to convert the Xyz tristimulus values from
373/// one illuminant to another (`source_wp` to `destination_wp`)
374#[deprecated(
375    since = "0.7.7",
376    note = "use the options from `palette::lms::matrix` or a custom matrix"
377)]
378#[allow(deprecated)]
379pub trait TransformMatrix<T>
380where
381    T: Zero + Arithmetics + Clone,
382{
383    /// Get the cone response functions for the chromatic adaptation method
384    #[must_use]
385    fn get_cone_response(&self) -> ConeResponseMatrices<T>;
386
387    /// Generates a 3x3 transformation matrix to convert color from one
388    /// reference white point to another with the given cone_response
389    #[must_use]
390    fn generate_transform_matrix(
391        &self,
392        source_wp: Xyz<Any, T>,
393        destination_wp: Xyz<Any, T>,
394    ) -> Mat3<T> {
395        let adapt = self.get_cone_response();
396
397        let resp_src: Lms<Any, T> =
398            multiply_3x3_and_vec3(adapt.ma.clone(), source_wp.into()).into();
399        let resp_dst: Lms<Any, T> =
400            multiply_3x3_and_vec3(adapt.ma.clone(), destination_wp.into()).into();
401
402        let resp = diagonal_matrix(resp_src, resp_dst).into_array();
403
404        let tmp = multiply_3x3(resp, adapt.ma);
405        multiply_3x3(adapt.inv_ma, tmp)
406    }
407}
408
409#[allow(deprecated)]
410impl<T> TransformMatrix<T> for Method
411where
412    T: Real + Zero + Arithmetics + Clone,
413{
414    #[rustfmt::skip]
415    #[inline]
416    fn get_cone_response(&self) -> ConeResponseMatrices<T> {
417        match *self {
418             Method::Bradford => {
419                ConeResponseMatrices::<T> {
420                    ma: lms::matrix::Bradford::xyz_to_lms_matrix(),
421                    inv_ma: lms::matrix::Bradford::lms_to_xyz_matrix(),
422                }
423            }
424             Method::VonKries => {
425                ConeResponseMatrices::<T> {
426                    ma: lms::matrix::VonKries::xyz_to_lms_matrix(),
427                    inv_ma: lms::matrix::VonKries::lms_to_xyz_matrix(),
428                }
429            }
430             Method::XyzScaling => {
431                ConeResponseMatrices::<T> {
432                    ma: lms::matrix::UnitMatrix::xyz_to_lms_matrix(),
433                    inv_ma: lms::matrix::UnitMatrix::lms_to_xyz_matrix(),
434                }
435            }
436        }
437    }
438}
439
440/// Trait to convert color from one reference white point to another
441///
442/// Converts a color from the source white point (Swp) to the destination white
443/// point (Dwp). Uses the bradford method for conversion by default.
444#[deprecated(
445    since = "0.7.7",
446    note = "replaced by `palette::chromatic_adaptation::AdaptFromUnclamped`"
447)]
448#[allow(deprecated)]
449pub trait AdaptFrom<S, Swp, Dwp, T>: Sized
450where
451    T: Real + Zero + Arithmetics + Clone,
452    Swp: WhitePoint<T>,
453    Dwp: WhitePoint<T>,
454{
455    /// Convert the source color to the destination color using the bradford
456    /// method by default.
457    #[must_use]
458    #[inline]
459    fn adapt_from(color: S) -> Self {
460        Self::adapt_from_using(color, Method::Bradford)
461    }
462    /// Convert the source color to the destination color using the specified
463    /// method.
464    #[must_use]
465    fn adapt_from_using<M: TransformMatrix<T>>(color: S, method: M) -> Self;
466}
467
468#[allow(deprecated)]
469impl<S, D, Swp, Dwp, T> AdaptFrom<S, Swp, Dwp, T> for D
470where
471    T: Real + Zero + Arithmetics + Clone,
472    Swp: WhitePoint<T>,
473    Dwp: WhitePoint<T>,
474    S: IntoColorUnclamped<Xyz<Swp, T>>,
475    D: FromColorUnclamped<Xyz<Dwp, T>>,
476{
477    #[inline]
478    fn adapt_from_using<M: TransformMatrix<T>>(color: S, method: M) -> D {
479        let src_xyz: Xyz<Swp, T> = color.into_color_unclamped();
480        let transform_matrix = method.generate_transform_matrix(Swp::get_xyz(), Dwp::get_xyz());
481        let dst_xyz: Xyz<Dwp, T> = multiply_3x3_and_vec3(transform_matrix, src_xyz.into()).into();
482        D::from_color_unclamped(dst_xyz)
483    }
484}
485
486/// Trait to convert color with one reference white point into another
487///
488/// Converts a color with the source white point (Swp) into the destination
489/// white point (Dwp). Uses the bradford method for conversion by default.
490#[deprecated(
491    since = "0.7.7",
492    note = "replaced by `palette::chromatic_adaptation::AdaptIntoUnclamped`"
493)]
494#[allow(deprecated)]
495pub trait AdaptInto<D, Swp, Dwp, T>: Sized
496where
497    T: Real + Zero + Arithmetics + Clone,
498    Swp: WhitePoint<T>,
499    Dwp: WhitePoint<T>,
500{
501    /// Convert the source color to the destination color using the bradford
502    /// method by default.
503    #[must_use]
504    #[inline]
505    fn adapt_into(self) -> D {
506        self.adapt_into_using(Method::Bradford)
507    }
508    /// Convert the source color to the destination color using the specified
509    /// method.
510    #[must_use]
511    fn adapt_into_using<M: TransformMatrix<T>>(self, method: M) -> D;
512}
513
514#[allow(deprecated)]
515impl<S, D, Swp, Dwp, T> AdaptInto<D, Swp, Dwp, T> for S
516where
517    T: Real + Zero + Arithmetics + Clone,
518    Swp: WhitePoint<T>,
519    Dwp: WhitePoint<T>,
520    D: AdaptFrom<S, Swp, Dwp, T>,
521{
522    #[inline]
523    fn adapt_into_using<M: TransformMatrix<T>>(self, method: M) -> D {
524        D::adapt_from_using(self, method)
525    }
526}
527
528#[cfg(feature = "approx")]
529#[cfg(test)]
530mod test {
531    #![allow(deprecated)]
532
533    use super::{AdaptFrom, AdaptInto, Method, TransformMatrix};
534    use crate::{
535        encoding::{Linear, Srgb},
536        Xyz,
537    };
538    use crate::{
539        rgb::Rgb,
540        white_point::{WhitePoint, A, C, D50, D65},
541    };
542
543    #[test]
544    fn d65_to_d50_matrix_xyz_scaling() {
545        let expected = [
546            1.0144665, 0.0000000, 0.0000000, 0.0000000, 1.0000000, 0.0000000, 0.0000000, 0.0000000,
547            0.7578869,
548        ];
549        let xyz_scaling = Method::XyzScaling;
550        let computed = xyz_scaling.generate_transform_matrix(D65::get_xyz(), D50::get_xyz());
551        for (e, c) in expected.iter().zip(computed.iter()) {
552            assert_relative_eq!(e, c, epsilon = 0.0001)
553        }
554    }
555    #[test]
556    fn d65_to_d50_matrix_von_kries() {
557        let expected = [
558            1.0160803, 0.0552297, -0.0521326, 0.0060666, 0.9955661, -0.0012235, 0.0000000,
559            0.0000000, 0.7578869,
560        ];
561        let von_kries = Method::VonKries;
562        let computed = von_kries.generate_transform_matrix(D65::get_xyz(), D50::get_xyz());
563        for (e, c) in expected.iter().zip(computed.iter()) {
564            assert_relative_eq!(e, c, epsilon = 0.0001)
565        }
566    }
567    #[test]
568    fn d65_to_d50_matrix_bradford() {
569        let expected = [
570            1.0478112, 0.0228866, -0.0501270, 0.0295424, 0.9904844, -0.0170491, -0.0092345,
571            0.0150436, 0.7521316,
572        ];
573        let bradford = Method::Bradford;
574        let computed = bradford.generate_transform_matrix(D65::get_xyz(), D50::get_xyz());
575        for (e, c) in expected.iter().zip(computed.iter()) {
576            assert_relative_eq!(e, c, epsilon = 0.0001)
577        }
578    }
579
580    #[test]
581    fn chromatic_adaptation_from_a_to_c() {
582        let input_a = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
583
584        let expected_bradford = Xyz::<C, f32>::new(0.257963, 0.139776, 0.058825);
585        let expected_vonkries = Xyz::<C, f32>::new(0.268446, 0.159139, 0.052843);
586        let expected_xyz_scaling = Xyz::<C, f32>::new(0.281868, 0.162732, 0.052844);
587
588        let computed_bradford: Xyz<C, f32> = Xyz::adapt_from(input_a);
589        assert_relative_eq!(expected_bradford, computed_bradford, epsilon = 0.0001);
590
591        let computed_vonkries: Xyz<C, f32> = Xyz::adapt_from_using(input_a, Method::VonKries);
592        assert_relative_eq!(expected_vonkries, computed_vonkries, epsilon = 0.0001);
593
594        let computed_xyz_scaling: Xyz<C, _> = Xyz::adapt_from_using(input_a, Method::XyzScaling);
595        assert_relative_eq!(expected_xyz_scaling, computed_xyz_scaling, epsilon = 0.0001);
596    }
597
598    #[test]
599    fn chromatic_adaptation_into_a_to_c() {
600        let input_a = Xyz::<A, f32>::new(0.315756, 0.162732, 0.015905);
601
602        let expected_bradford = Xyz::<C, f32>::new(0.257963, 0.139776, 0.058825);
603        let expected_vonkries = Xyz::<C, f32>::new(0.268446, 0.159139, 0.052843);
604        let expected_xyz_scaling = Xyz::<C, f32>::new(0.281868, 0.162732, 0.052844);
605
606        let computed_bradford: Xyz<C, f32> = input_a.adapt_into();
607        assert_relative_eq!(expected_bradford, computed_bradford, epsilon = 0.0001);
608
609        let computed_vonkries: Xyz<C, f32> = input_a.adapt_into_using(Method::VonKries);
610        assert_relative_eq!(expected_vonkries, computed_vonkries, epsilon = 0.0001);
611
612        let computed_xyz_scaling: Xyz<C, _> = input_a.adapt_into_using(Method::XyzScaling);
613        assert_relative_eq!(expected_xyz_scaling, computed_xyz_scaling, epsilon = 0.0001);
614    }
615
616    #[test]
617    fn d65_to_d50() {
618        let input: Rgb<Linear<Srgb>> = Rgb::new(1.0, 1.0, 1.0);
619        let expected: Rgb<Linear<(Srgb, D50)>> = Rgb::new(1.0, 1.0, 1.0);
620
621        let computed: Rgb<Linear<(Srgb, D50)>> = input.adapt_into();
622        assert_relative_eq!(expected, computed, epsilon = 0.000001);
623    }
624}