Skip to main content

prisma/
lms.rs

1//! The LMS cone response device-independent color space
2//!
3//! [`Lms`](struct.Lms.html) aims to represent the cone responses of human vision. See the struct
4//! level documentation for more information.
5
6use crate::channel::{
7    ChannelCast, ChannelFormatCast, ColorChannel, FreeChannel, FreeChannelScalar,
8};
9use crate::color::{Bounded, Broadcast, Color, Flatten, FromTuple, HomogeneousColor, Lerp};
10use crate::convert::FromColor;
11use crate::linalg::Matrix3;
12use crate::tags::LmsTag;
13use crate::xyz::Xyz;
14#[cfg(feature = "approx")]
15use approx;
16use num_traits;
17use std::fmt;
18use std::marker::PhantomData;
19use std::mem;
20use std::slice;
21
22/// A model for transforming from XYZ to LMS and back
23pub trait LmsModel<T>: Clone + PartialEq {
24    /// Get the conversion matrix to convert from XYZ to LMS
25    fn forward_transform() -> Matrix3<T>;
26    /// Get the conversion matrix to convert from LMS to XYZ
27    fn inverse_transform() -> Matrix3<T>;
28}
29
30/// The `LMS` cone response device-independent color space
31///
32/// `LMS` is a device-independent color space created to map to the average response of the three
33/// cones in the human eye. There is no single `LMS` space, but rather several different models defining
34/// a transformation from `XYZ` to `LMS`. `LMS` is well suited for use in chromatic adaptation as well
35/// as for simulating the effects of color blindness in humans. `LMS` is also widely used in "color
36/// adaptation models" such as CIECAM2002.
37///
38/// `LMS` is a linear transformation from `XYZ`, and each model is defined by a matrix `M` that
39/// multiplies a `XYZ` value to produce an `LMS` value.
40///
41/// Note that presently, the `Model` type parameter to `LMS` must be data-less. This may be changed
42/// in the future if a use case arises.
43#[repr(C)]
44#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
45pub struct Lms<T, Model> {
46    l: FreeChannel<T>,
47    m: FreeChannel<T>,
48    s: FreeChannel<T>,
49    model: PhantomData<Model>,
50}
51
52/// The `LMS` transform defined in the CIECAM2002 color adaptation model
53#[derive(Copy, Clone, Debug, PartialEq)]
54pub struct CieCam2002;
55/// The `LMS` transform defined in the CIECAM97s color adaptation model
56#[derive(Copy, Clone, Debug, PartialEq)]
57pub struct CieCam97s;
58/// The Bradford `LMS` transform
59#[derive(Copy, Clone, Debug, PartialEq)]
60pub struct Bradford;
61
62/// An `LMS` space using the [`CieCam2002`](struct.CieCam2002.html) model
63pub type LmsCam2002<T> = Lms<T, CieCam2002>;
64/// An `LMS` space using the [`CieCam97s`](struct.CieCam97s.html) model
65pub type LmsCam97s<T> = Lms<T, CieCam97s>;
66/// An `LMS` space using the [`Bradford`](struct.Bradford.html) model
67pub type LmsBradford<T> = Lms<T, Bradford>;
68
69impl<T, Model> Lms<T, Model>
70where
71    T: FreeChannelScalar,
72    Model: LmsModel<T>,
73{
74    /// Construct an `LMS` instance from `l`, `m` and `s`
75    pub fn new(l: T, m: T, s: T) -> Self {
76        Lms {
77            l: FreeChannel::new(l),
78            m: FreeChannel::new(m),
79            s: FreeChannel::new(s),
80            model: PhantomData,
81        }
82    }
83
84    /// Cast the channel representation type
85    pub fn color_cast<TOut>(&self) -> Lms<TOut, Model>
86    where
87        T: ChannelFormatCast<TOut>,
88        TOut: FreeChannelScalar,
89    {
90        Lms {
91            l: self.l.clone().channel_cast(),
92            m: self.m.clone().channel_cast(),
93            s: self.s.clone().channel_cast(),
94            model: PhantomData,
95        }
96    }
97
98    /// Returns the `L` value
99    pub fn l(&self) -> T {
100        self.l.0.clone()
101    }
102    /// Returns the `M` value
103    pub fn m(&self) -> T {
104        self.m.0.clone()
105    }
106    /// Returns the `S` value
107    pub fn s(&self) -> T {
108        self.s.0.clone()
109    }
110    /// Returns a mutable reference to the `L` value
111    pub fn l_mut(&mut self) -> &mut T {
112        &mut self.l.0
113    }
114    /// Returns a mutable reference to the `M` value
115    pub fn m_mut(&mut self) -> &mut T {
116        &mut self.m.0
117    }
118    /// Returns a mutable reference to the `S` value
119    pub fn s_mut(&mut self) -> &mut T {
120        &mut self.s.0
121    }
122    /// Set the `L` value
123    pub fn set_l(&mut self, val: T) {
124        self.l.0 = val;
125    }
126    /// Set the `M` value
127    pub fn set_m(&mut self, val: T) {
128        self.m.0 = val;
129    }
130    /// Set the `S` value
131    pub fn set_s(&mut self, val: T) {
132        self.s.0 = val;
133    }
134}
135
136impl<T, Model> Color for Lms<T, Model>
137where
138    T: FreeChannelScalar,
139    Model: LmsModel<T>,
140{
141    type Tag = LmsTag;
142    type ChannelsTuple = (T, T, T);
143
144    #[inline]
145    fn num_channels() -> u32 {
146        3
147    }
148    fn to_tuple(self) -> Self::ChannelsTuple {
149        (self.l.0, self.m.0, self.s.0)
150    }
151}
152
153impl<T, Model> FromTuple for Lms<T, Model>
154where
155    T: FreeChannelScalar,
156    Model: LmsModel<T>,
157{
158    fn from_tuple(values: (T, T, T)) -> Self {
159        Lms::new(values.0, values.1, values.2)
160    }
161}
162
163impl<T, Model> HomogeneousColor for Lms<T, Model>
164where
165    T: FreeChannelScalar,
166    Model: LmsModel<T>,
167{
168    type ChannelFormat = T;
169
170    impl_color_homogeneous_color_square!(Lms<T> {l, m, s}, phantom={model});
171}
172
173impl<T, Model> Bounded for Lms<T, Model>
174where
175    T: FreeChannelScalar,
176    Model: LmsModel<T>,
177{
178    impl_color_bounded!(Lms { l, m, s }, phantom = { model });
179}
180
181impl<T, Model> Broadcast for Lms<T, Model>
182where
183    T: FreeChannelScalar,
184    Model: LmsModel<T>,
185{
186    impl_color_broadcast!(Lms<T> {l, m, s}, chan=FreeChannel, phantom={model});
187}
188
189impl<T, Model> Lerp for Lms<T, Model>
190where
191    T: FreeChannelScalar,
192    Model: LmsModel<T>,
193    FreeChannel<T>: Lerp,
194{
195    type Position = <FreeChannel<T> as Lerp>::Position;
196    impl_color_lerp_square!(Lms { l, m, s }, phantom = { model });
197}
198
199impl<T, Model> Flatten for Lms<T, Model>
200where
201    T: FreeChannelScalar,
202    Model: LmsModel<T>,
203{
204    impl_color_as_slice!(T);
205    impl_color_from_slice_square!(Lms<T> {l:FreeChannel - 0, m:FreeChannel - 1,
206        s:FreeChannel - 2});
207}
208
209#[cfg(feature = "approx")]
210impl<T, Model> approx::AbsDiffEq for Lms<T, Model>
211where
212    T: FreeChannelScalar + approx::AbsDiffEq,
213    T::Epsilon: Clone,
214    Model: LmsModel<T>,
215{
216    impl_abs_diff_eq!({l, m, s});
217}
218#[cfg(feature = "approx")]
219impl<T, Model> approx::RelativeEq for Lms<T, Model>
220where
221    T: FreeChannelScalar + approx::RelativeEq,
222    T::Epsilon: Clone,
223    Model: LmsModel<T>,
224{
225    impl_rel_eq!({l, m, s});
226}
227#[cfg(feature = "approx")]
228impl<T, Model> approx::UlpsEq for Lms<T, Model>
229where
230    T: FreeChannelScalar + approx::UlpsEq,
231    T::Epsilon: Clone,
232    Model: LmsModel<T>,
233{
234    impl_ulps_eq!({l, m, s});
235}
236
237impl<T, Model> Default for Lms<T, Model>
238where
239    T: FreeChannelScalar,
240    Model: LmsModel<T>,
241{
242    impl_color_default!(
243        Lms {
244            l: FreeChannel,
245            m: FreeChannel,
246            s: FreeChannel
247        },
248        phantom = { model }
249    );
250}
251
252impl<T, Model> fmt::Display for Lms<T, Model>
253where
254    T: FreeChannelScalar + fmt::Display,
255    Model: LmsModel<T>,
256{
257    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
258        write!(f, "LMS({}, {}, {})", self.l, self.m, self.s)
259    }
260}
261
262impl<T, Model> FromColor<Xyz<T>> for Lms<T, Model>
263where
264    T: FreeChannelScalar,
265    Model: LmsModel<T>,
266{
267    fn from_color(from: &Xyz<T>) -> Self {
268        let transform = Model::forward_transform();
269        let (l, m, s) = transform.transform_vector(from.clone().to_tuple());
270        Lms::new(l, m, s)
271    }
272}
273
274impl<T, Model> FromColor<Lms<T, Model>> for Xyz<T>
275where
276    T: FreeChannelScalar,
277    Model: LmsModel<T>,
278{
279    fn from_color(from: &Lms<T, Model>) -> Self {
280        let transform = Model::inverse_transform();
281        let (x, y, z) = transform.transform_vector(from.clone().to_tuple());
282        Xyz::new(x, y, z)
283    }
284}
285
286impl<T> LmsModel<T> for CieCam2002
287where
288    T: FreeChannelScalar,
289{
290    fn forward_transform() -> Matrix3<T> {
291        Matrix3::<T>::new([
292            num_traits::cast(0.7328).unwrap(),
293            num_traits::cast(0.4296).unwrap(),
294            num_traits::cast(-0.1624).unwrap(),
295            num_traits::cast(-0.7036).unwrap(),
296            num_traits::cast(1.6975).unwrap(),
297            num_traits::cast(0.0061).unwrap(),
298            num_traits::cast(0.0030).unwrap(),
299            num_traits::cast(0.0136).unwrap(),
300            num_traits::cast(0.9834).unwrap(),
301        ])
302    }
303
304    fn inverse_transform() -> Matrix3<T> {
305        Matrix3::<T>::new([
306            num_traits::cast(1.09612).unwrap(),
307            num_traits::cast(-0.27887).unwrap(),
308            num_traits::cast(0.18275).unwrap(),
309            num_traits::cast(0.45437).unwrap(),
310            num_traits::cast(0.47353).unwrap(),
311            num_traits::cast(0.07209).unwrap(),
312            num_traits::cast(-0.009628).unwrap(),
313            num_traits::cast(-0.005698).unwrap(),
314            num_traits::cast(1.015326).unwrap(),
315        ])
316    }
317}
318
319impl<T> LmsModel<T> for CieCam97s
320where
321    T: FreeChannelScalar,
322{
323    fn forward_transform() -> Matrix3<T> {
324        Matrix3::<T>::new([
325            num_traits::cast(0.8562).unwrap(),
326            num_traits::cast(0.3372).unwrap(),
327            num_traits::cast(-0.1934).unwrap(),
328            num_traits::cast(-0.8360).unwrap(),
329            num_traits::cast(1.8327).unwrap(),
330            num_traits::cast(0.0033).unwrap(),
331            num_traits::cast(0.0357).unwrap(),
332            num_traits::cast(-0.0469).unwrap(),
333            num_traits::cast(1.0112).unwrap(),
334        ])
335    }
336
337    fn inverse_transform() -> Matrix3<T> {
338        Matrix3::<T>::new([
339            num_traits::cast(0.98740).unwrap(),
340            num_traits::cast(-0.17683).unwrap(),
341            num_traits::cast(0.18943).unwrap(),
342            num_traits::cast(0.45044).unwrap(),
343            num_traits::cast(0.46493).unwrap(),
344            num_traits::cast(0.08463).unwrap(),
345            num_traits::cast(-0.01397).unwrap(),
346            num_traits::cast(0.027807).unwrap(),
347            num_traits::cast(0.98616).unwrap(),
348        ])
349    }
350}
351
352impl<T> LmsModel<T> for Bradford
353where
354    T: FreeChannelScalar,
355{
356    fn forward_transform() -> Matrix3<T> {
357        Matrix3::<T>::new([
358            num_traits::cast(0.8951).unwrap(),
359            num_traits::cast(0.2664).unwrap(),
360            num_traits::cast(-0.1614).unwrap(),
361            num_traits::cast(-0.7502).unwrap(),
362            num_traits::cast(1.7135).unwrap(),
363            num_traits::cast(0.0367).unwrap(),
364            num_traits::cast(0.0389).unwrap(),
365            num_traits::cast(-0.0685).unwrap(),
366            num_traits::cast(1.0296).unwrap(),
367        ])
368    }
369
370    fn inverse_transform() -> Matrix3<T> {
371        Matrix3::<T>::new([
372            num_traits::cast(0.98699).unwrap(),
373            num_traits::cast(-0.14705).unwrap(),
374            num_traits::cast(0.15996).unwrap(),
375            num_traits::cast(0.43231).unwrap(),
376            num_traits::cast(0.51836).unwrap(),
377            num_traits::cast(0.04929).unwrap(),
378            num_traits::cast(-0.00853).unwrap(),
379            num_traits::cast(0.040043).unwrap(),
380            num_traits::cast(0.96849).unwrap(),
381        ])
382    }
383}
384
385#[cfg(test)]
386mod test {
387    use super::*;
388    use crate::xyz::Xyz;
389    use approx::*;
390
391    #[test]
392    fn test_construct() {
393        let c1 = LmsCam2002::new(0.4, 0.6, 0.2);
394        assert_relative_eq!(c1.l(), 0.4);
395        assert_relative_eq!(c1.m(), 0.6);
396        assert_relative_eq!(c1.s(), 0.2);
397        assert_eq!(c1.to_tuple(), (0.4, 0.6, 0.2));
398        assert_relative_eq!(LmsCam2002::from_tuple(c1.to_tuple()), c1);
399    }
400
401    #[test]
402    fn test_lerp() {
403        let c1 = LmsCam97s::new(0.5, 0.9, 0.0);
404        let c2 = LmsCam97s::new(0.7, 0.0, 0.4);
405        assert_relative_eq!(c1.lerp(&c2, 0.0), c1);
406        assert_relative_eq!(c1.lerp(&c2, 1.0), c2);
407        assert_relative_eq!(c1.lerp(&c2, 0.5), LmsCam97s::new(0.6, 0.45, 0.2));
408        assert_relative_eq!(c1.lerp(&c2, 0.75), LmsCam97s::new(0.65, 0.225, 0.3));
409    }
410
411    #[test]
412    fn test_normalize() {
413        let c1 = LmsCam2002::new(-50.0, 50.0, 1e7);
414        assert!(c1.is_normalized());
415        assert_relative_eq!(c1.normalize(), c1);
416    }
417
418    #[test]
419    fn test_flatten() {
420        let c1 = LmsBradford::new(0.2, 0.5, 1.0);
421        assert_eq!(c1.as_slice(), &[0.2, 0.5, 1.0]);
422        assert_relative_eq!(LmsBradford::from_slice(c1.as_slice()), c1);
423    }
424
425    #[test]
426    fn test_from_xyz() {
427        let c1 = Xyz::new(0.5, 0.2, 0.0);
428        let t1 = Lms::<_, CieCam2002>::from_color(&c1);
429        assert_relative_eq!(t1, Lms::new(0.45232, -0.01230, 0.00422), epsilon = 1e-4);
430        assert_relative_eq!(Xyz::from_color(&t1), c1, epsilon = 1e-4);
431
432        let c2 = Xyz::new(0.3, 0.3, 0.3);
433        let t2 = Lms::<_, CieCam2002>::from_color(&c2);
434        assert_relative_eq!(t2, Lms::new(0.3, 0.3, 0.3), epsilon = 1e-4);
435        assert_relative_eq!(Xyz::from_color(&t2), c2, epsilon = 1e-4);
436
437        let c3 = Xyz::new(0.6, 0.4, 0.5);
438        let t3 = Lms::<_, CieCam97s>::from_color(&c3);
439        assert_relative_eq!(t3, Lms::new(0.5519, 0.23313, 0.50826), epsilon = 1e-4);
440        assert_relative_eq!(Xyz::from_color(&t3), c3, epsilon = 1e-4);
441
442        let c4 = Xyz::new(0.2, 0.3, 0.6);
443        let t4 = Lms::<_, Bradford>::from_color(&c4);
444        assert_relative_eq!(t4, Lms::new(0.1621, 0.38603, 0.6050), epsilon = 1e-4);
445        assert_relative_eq!(Xyz::from_color(&t4), c4, epsilon = 1e-4);
446    }
447
448    #[test]
449    fn test_to_xyz() {
450        let c1 = LmsCam2002::new(0.25, 0.50, 0.75);
451        let t1 = Xyz::from_color(&c1);
452        assert_relative_eq!(t1, Xyz::new(0.2716575, 0.404425, 0.75624), epsilon = 1e-4);
453        assert_relative_eq!(LmsCam2002::from_color(&t1), c1, epsilon = 1e-4);
454    }
455
456    #[test]
457    fn test_color_cast() {
458        let c1 = LmsCam2002::new(0.25, 0.50, 0.75);
459        assert_relative_eq!(c1.color_cast(), c1);
460        assert_relative_eq!(c1.color_cast(), LmsCam2002::new(0.25f32, 0.50f32, 0.75f32));
461        assert_relative_eq!(c1.color_cast::<f32>().color_cast(), c1);
462    }
463}