Skip to main content

prisma/
ehsi.rs

1//! The eHSI device-dependent polar color model
2
3use crate::channel::{
4    AngularChannel, AngularChannelScalar, ChannelCast, ChannelFormatCast, ColorChannel,
5    PosNormalBoundedChannel, PosNormalChannelScalar,
6};
7use crate::color;
8use crate::color::{Bounded, Color, FromTuple, Invert, Lerp, PolarColor};
9use crate::convert::{decompose_hue_segment, FromColor, GetHue};
10use crate::encoding::EncodableColor;
11use crate::hsi::Hsi;
12use crate::rgb::Rgb;
13use crate::tags::EHsiTag;
14use angle;
15use angle::{Angle, Deg, FromAngle, IntoAngle, Rad};
16#[cfg(feature = "approx")]
17use approx;
18use num_traits;
19use num_traits::Float;
20use std::fmt;
21
22/// The eHSI device-dependent polar color model
23///
24/// eHSI has the same components as [`Hsi`](../hsi/struct.Hsi.html): hue, saturation, intensity
25/// but has additional logic for rescaling saturation in the case of what would be out-of-gamut
26/// colors in the original HSI model. eHSI was adapted from the algorithm described in:
27///
28/// ```ignore
29/// K. Yoshinari, Y. Hoshi and A. Taguchi, "Color image enhancement in HSI color space
30/// without gamut problem," 2014 6th International Symposium on Communications,
31/// Control and Signal Processing (ISCCSP), Athens, 2014, pp. 578-581.
32/// ```
33///
34/// found freely [here](http://www.ijicic.org/ijicic-10-07057.pdf).
35///
36/// eHSI is fully defined over the cylinder, and is generally visually better at adjusting intensity.
37#[repr(C)]
38#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Hash)]
39pub struct eHsi<T, A = Deg<T>> {
40    hue: AngularChannel<A>,
41    saturation: PosNormalBoundedChannel<T>,
42    intensity: PosNormalBoundedChannel<T>,
43}
44
45impl<T, A> eHsi<T, A>
46where
47    T: PosNormalChannelScalar + Float,
48    A: AngularChannelScalar + Angle<Scalar = T>,
49{
50    /// Construct an eHsi instance from hue, saturation and intensity.
51    pub fn new(hue: A, saturation: T, intensity: T) -> Self {
52        eHsi {
53            hue: AngularChannel::new(hue),
54            saturation: PosNormalBoundedChannel::new(saturation),
55            intensity: PosNormalBoundedChannel::new(intensity),
56        }
57    }
58
59    impl_color_color_cast_angular!(
60        eHsi {
61            hue,
62            saturation,
63            intensity
64        },
65        chan_traits = { PosNormalChannelScalar }
66    );
67
68    /// Returns the hue scalar
69    pub fn hue(&self) -> A {
70        self.hue.0.clone()
71    }
72    /// Returns the saturation scalar
73    pub fn saturation(&self) -> T {
74        self.saturation.0.clone()
75    }
76    /// Returns the intensity scalar
77    pub fn intensity(&self) -> T {
78        self.intensity.0.clone()
79    }
80    /// Returns a mutable reference to the hue scalar
81    pub fn hue_mut(&mut self) -> &mut A {
82        &mut self.hue.0
83    }
84    /// Returns a mutable reference to the saturation scalar
85    pub fn saturation_mut(&mut self) -> &mut T {
86        &mut self.saturation.0
87    }
88    /// Returns a mutable reference to the intensity scalar
89    pub fn intensity_mut(&mut self) -> &mut T {
90        &mut self.intensity.0
91    }
92    /// Set the hue channel value
93    pub fn set_hue(&mut self, val: A) {
94        self.hue.0 = val;
95    }
96    /// Set the saturation channel value
97    pub fn set_saturation(&mut self, val: T) {
98        self.saturation.0 = val;
99    }
100    /// Set the intensity channel value
101    pub fn set_intensity(&mut self, val: T) {
102        self.intensity.0 = val;
103    }
104    /// Returns whether the `eHsi` instance would be the same in `Hsi`
105    pub fn is_same_as_hsi(&self) -> bool {
106        let deg_hue =
107            Deg::from_angle(self.hue().clone()) % Deg(num_traits::cast::<_, T>(120.0).unwrap());
108        let i_limit = num_traits::cast::<_, T>(2.0 / 3.0).unwrap()
109            - (deg_hue - Deg(num_traits::cast::<_, T>(60.0).unwrap()))
110                .scalar()
111                .abs()
112                / Deg(num_traits::cast::<_, T>(180.0).unwrap()).scalar();
113
114        self.intensity() <= i_limit
115    }
116    /// Returns an `Hsi` instance that is the same as `self` if they would be equivalent, or `None` otherwise
117    pub fn to_hsi(&self) -> Option<Hsi<T, A>> {
118        if self.is_same_as_hsi() {
119            Some(Hsi::new(
120                self.hue().clone(),
121                self.saturation().clone(),
122                self.intensity().clone(),
123            ))
124        } else {
125            None
126        }
127    }
128    /// Construct an `eHsi` instance from an `Hsi` instance if both would be equivalent
129    ///
130    /// If they would not be equivalent, returns `None`.
131    pub fn from_hsi(hsi: &Hsi<T, A>) -> Option<eHsi<T, A>> {
132        let out = eHsi::new(
133            hsi.hue().clone(),
134            hsi.saturation().clone(),
135            hsi.intensity().clone(),
136        );
137        if out.is_same_as_hsi() {
138            Some(out)
139        } else {
140            None
141        }
142    }
143}
144
145impl<T, A> PolarColor for eHsi<T, A>
146where
147    T: PosNormalChannelScalar,
148    A: AngularChannelScalar,
149{
150    type Angular = A;
151    type Cartesian = T;
152}
153
154impl<T, A> Color for eHsi<T, A>
155where
156    T: PosNormalChannelScalar,
157    A: AngularChannelScalar,
158{
159    type Tag = EHsiTag;
160    type ChannelsTuple = (A, T, T);
161
162    fn num_channels() -> u32 {
163        3
164    }
165    fn to_tuple(self) -> Self::ChannelsTuple {
166        (self.hue.0, self.saturation.0, self.intensity.0)
167    }
168}
169
170impl<T, A> FromTuple for eHsi<T, A>
171where
172    T: PosNormalChannelScalar + Float,
173    A: AngularChannelScalar + Angle<Scalar = T>,
174{
175    fn from_tuple(values: Self::ChannelsTuple) -> Self {
176        eHsi::new(values.0, values.1, values.2)
177    }
178}
179
180impl<T, A> Invert for eHsi<T, A>
181where
182    T: PosNormalChannelScalar,
183    A: AngularChannelScalar,
184{
185    impl_color_invert!(eHsi {
186        hue,
187        saturation,
188        intensity
189    });
190}
191
192impl<T, A> Lerp for eHsi<T, A>
193where
194    T: PosNormalChannelScalar + color::Lerp,
195    A: AngularChannelScalar + color::Lerp,
196{
197    type Position = A::Position;
198
199    impl_color_lerp_angular!(eHsi<T> {hue, saturation, intensity});
200}
201
202impl<T, A> Bounded for eHsi<T, A>
203where
204    T: PosNormalChannelScalar,
205    A: AngularChannelScalar,
206{
207    impl_color_bounded!(eHsi {
208        hue,
209        saturation,
210        intensity
211    });
212}
213
214impl<T, A> EncodableColor for eHsi<T, A>
215where
216    T: PosNormalChannelScalar + num_traits::Float,
217    A: AngularChannelScalar + Angle<Scalar = T> + FromAngle<angle::Turns<T>>,
218{
219}
220
221#[cfg(feature = "approx")]
222impl<T, A> approx::AbsDiffEq for eHsi<T, A>
223where
224    T: PosNormalChannelScalar + approx::AbsDiffEq<Epsilon = A::Epsilon>,
225    A: AngularChannelScalar + approx::AbsDiffEq,
226    A::Epsilon: Clone + num_traits::Float,
227{
228    impl_abs_diff_eq!({hue, saturation, intensity});
229}
230#[cfg(feature = "approx")]
231impl<T, A> approx::RelativeEq for eHsi<T, A>
232where
233    T: PosNormalChannelScalar + approx::RelativeEq<Epsilon = A::Epsilon>,
234    A: AngularChannelScalar + approx::RelativeEq,
235    A::Epsilon: Clone + num_traits::Float,
236{
237    impl_rel_eq!({hue, saturation, intensity});
238}
239#[cfg(feature = "approx")]
240impl<T, A> approx::UlpsEq for eHsi<T, A>
241where
242    T: PosNormalChannelScalar + approx::UlpsEq<Epsilon = A::Epsilon>,
243    A: AngularChannelScalar + approx::UlpsEq,
244    A::Epsilon: Clone + num_traits::Float,
245{
246    impl_ulps_eq!({hue, saturation, intensity});
247}
248
249impl<T, A> Default for eHsi<T, A>
250where
251    T: PosNormalChannelScalar + num_traits::Zero,
252    A: AngularChannelScalar + num_traits::Zero,
253{
254    impl_color_default!(eHsi {
255        hue: AngularChannel,
256        saturation: PosNormalBoundedChannel,
257        intensity: PosNormalBoundedChannel
258    });
259}
260
261impl<T, A> fmt::Display for eHsi<T, A>
262where
263    T: PosNormalChannelScalar + fmt::Display,
264    A: AngularChannelScalar + fmt::Display,
265{
266    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
267        write!(
268            f,
269            "eHsi({}, {}, {})",
270            self.hue, self.saturation, self.intensity
271        )
272    }
273}
274
275impl<T, A> GetHue for eHsi<T, A>
276where
277    T: PosNormalChannelScalar,
278    A: AngularChannelScalar,
279{
280    impl_color_get_hue_angular!(eHsi);
281}
282
283impl<T, A> FromColor<Rgb<T>> for eHsi<T, A>
284where
285    T: PosNormalChannelScalar + num_traits::Float,
286    A: AngularChannelScalar + Angle<Scalar = T> + FromAngle<Rad<T>>,
287{
288    fn from_color(from: &Rgb<T>) -> Self {
289        let epsilon: T = num_traits::cast(1e-10).unwrap();
290        let coords = from.chromaticity_coordinates();
291
292        let hue_unnormal: A = coords.get_hue::<A>();
293        let hue = Angle::normalize(hue_unnormal);
294        let deg_hue = Deg::from_angle(hue.clone()) % Deg(num_traits::cast::<_, T>(120.0).unwrap());
295
296        let min = from.red().min(from.green().min(from.blue()));
297        let max = from.red().max(from.green().max(from.blue()));
298
299        let sum = from.red() + from.green() + from.blue();
300        let intensity = num_traits::cast::<_, T>(1.0 / 3.0).unwrap() * sum;
301
302        let i_limit: T = num_traits::cast::<_, T>(2.0 / 3.0).unwrap()
303            - (deg_hue - Deg(num_traits::cast::<_, T>(60.0).unwrap()))
304                .scalar()
305                .abs()
306                / Deg(num_traits::cast::<_, T>(180.0).unwrap()).scalar();
307
308        let one: T = num_traits::cast(1.0).unwrap();
309
310        let saturation = if intensity <= i_limit {
311            if intensity != num_traits::cast::<_, T>(0.0).unwrap() {
312                one - min / intensity
313            } else {
314                num_traits::cast(0.0).unwrap()
315            }
316        } else {
317            let three: T = num_traits::cast(3.0).unwrap();
318            one - ((three * (one - max)) / (three - sum + epsilon))
319        };
320
321        eHsi::new(hue, saturation, intensity)
322    }
323}
324
325impl<T, A> FromColor<eHsi<T, A>> for Rgb<T>
326where
327    T: PosNormalChannelScalar + num_traits::Float,
328    A: AngularChannelScalar + Angle<Scalar = T>,
329{
330    fn from_color(from: &eHsi<T, A>) -> Rgb<T> {
331        let one = num_traits::cast::<_, T>(1.0).unwrap();
332        let one_eighty = num_traits::cast::<_, T>(180.0).unwrap();
333
334        let (hue_seg, _) = decompose_hue_segment(from);
335        let scaled_frac = Deg::from_angle(from.hue()) % Deg(num_traits::cast(120.0).unwrap());
336
337        // I < i_threshold => Use standard Hsi -> Rgb method.
338        // Otherwise, we use the eHsi method.
339        let i_threshold = num_traits::cast::<_, T>(2.0 / 3.0).unwrap()
340            - (scaled_frac.scalar() - num_traits::cast(60.0).unwrap()).abs() / (one_eighty);
341
342        // Standard Hsi conversion
343        if from.intensity() < i_threshold {
344            let c1 = from.intensity() * (one - from.saturation());
345            let c2 = from.intensity()
346                * (one
347                    + (from.saturation() * scaled_frac.cos())
348                        / (Angle::cos(Deg(num_traits::cast(60.0).unwrap()) - scaled_frac)));
349
350            let c3 = num_traits::cast::<_, T>(3.0).unwrap() * from.intensity() - (c1 + c2);
351
352            match hue_seg {
353                0 | 1 => Rgb::new(c2, c3, c1),
354                2 | 3 => Rgb::new(c1, c2, c3),
355                4 | 5 => Rgb::new(c3, c1, c2),
356                _ => unreachable!(),
357            }
358        // eHsi conversion
359        } else {
360            let deg_hue = Deg::from_angle(from.hue());
361            let shifted_hue = match hue_seg {
362                1 | 2 => deg_hue - Deg(num_traits::cast(240.0).unwrap()),
363                3 | 4 => deg_hue,
364                5 | 0 => deg_hue - Deg(num_traits::cast(120.0).unwrap()),
365                _ => unreachable!(),
366            };
367
368            let c1 = from.intensity() * (one - from.saturation()) + from.saturation();
369            let c2 = one
370                - (one - from.intensity())
371                    * (one
372                        + (from.saturation() * shifted_hue.cos())
373                            / (Deg(num_traits::cast::<_, T>(60.0).unwrap()) - shifted_hue).cos());
374
375            let c3 = num_traits::cast::<_, T>(3.0).unwrap() * from.intensity() - (c1 + c2);
376
377            match hue_seg {
378                1 | 2 => Rgb::new(c3, c1, c2),
379                3 | 4 => Rgb::new(c2, c3, c1),
380                5 | 0 => Rgb::new(c1, c2, c3),
381                _ => unreachable!(),
382            }
383        }
384    }
385}
386
387#[cfg(test)]
388mod test {
389    use super::*;
390    use crate::hsi::Hsi;
391    use crate::rgb::Rgb;
392    use crate::test;
393    use angle::Turns;
394    use approx::*;
395
396    #[test]
397    fn test_construct() {
398        let c1 = eHsi::new(Deg(140.0), 0.68, 0.22);
399        assert_relative_eq!(c1.hue(), Deg(140.0));
400        assert_relative_eq!(c1.saturation(), 0.68);
401        assert_relative_eq!(c1.intensity(), 0.22);
402        assert_eq!(c1.to_tuple(), (Deg(140.0), 0.68, 0.22));
403        assert_eq!(eHsi::from_tuple(c1.to_tuple()), c1);
404
405        let c2 = eHsi::new(Rad(2.0f32), 0.33f32, 0.10);
406        assert_relative_eq!(c2.hue(), Rad(2.0f32));
407        assert_relative_eq!(c2.saturation(), 0.33);
408        assert_relative_eq!(c2.intensity(), 0.10);
409        assert_eq!(c2.to_tuple(), (Rad(2.0f32), 0.33f32, 0.10f32));
410        assert_eq!(eHsi::from_tuple(c2.to_tuple()), c2);
411    }
412
413    #[test]
414    fn test_invert() {
415        let c1 = eHsi::new(Deg(198.0), 0.33, 0.49);
416        assert_relative_eq!(c1.invert(), eHsi::new(Deg(18.0), 0.67, 0.51));
417        assert_relative_eq!(c1.invert().invert(), c1);
418
419        let c2 = eHsi::from_tuple((Turns(0.40), 0.50, 0.00));
420        assert_relative_eq!(c2.invert(), eHsi::new(Turns(0.90), 0.50, 1.00));
421        assert_relative_eq!(c2.invert().invert(), c2);
422    }
423
424    #[test]
425    fn test_lerp() {
426        let c1 = eHsi::new(Turns(0.9), 0.46, 0.20);
427        let c2 = eHsi::new(Turns(0.3), 0.50, 0.50);
428        assert_relative_eq!(c1.lerp(&c2, 0.0), c1);
429        assert_relative_eq!(c1.lerp(&c2, 1.0), c2);
430        assert_relative_eq!(c1.lerp(&c2, 0.5), eHsi::new(Turns(0.1), 0.48, 0.35));
431        assert_relative_eq!(c1.lerp(&c2, 0.25), eHsi::new(Turns(0.0), 0.47, 0.275));
432    }
433
434    #[test]
435    fn test_normalize() {
436        let c1 = eHsi::new(Deg(400.0), 1.25, -0.33);
437        assert!(!c1.is_normalized());
438        assert_relative_eq!(c1.normalize(), eHsi::new(Deg(40.0), 1.00, 0.00));
439        assert_eq!(c1.normalize().normalize(), c1.normalize());
440
441        let c2 = eHsi::new(Deg(20.0), 0.35, 0.99);
442        assert!(c2.is_normalized());
443        assert_eq!(c2.normalize(), c2);
444    }
445
446    #[test]
447    fn hsi_ehsi_convert() {
448        let hsi1 = Hsi::new(Deg(120.0), 0.0, 0.0);
449        let ehsi1 = eHsi::from_hsi(&hsi1);
450        assert_eq!(ehsi1, Some(eHsi::new(Deg(120.0), 0.0, 0.0)));
451        assert_eq!(hsi1, ehsi1.unwrap().to_hsi().unwrap());
452
453        let ehsi2 = eHsi::from_hsi(&Hsi::new(Deg(120.0), 1.0, 1.0));
454        assert_eq!(ehsi2, None);
455
456        let hsi3 = Hsi::new(Deg(180.0), 1.0, 0.60);
457        let ehsi3 = eHsi::from_hsi(&hsi3);
458        assert_relative_eq!(ehsi3.unwrap(), eHsi::new(Deg(180.0), 1.0, 0.60));
459        assert_relative_eq!(hsi3, &ehsi3.unwrap().to_hsi().unwrap());
460
461        let hsi3 = Hsi::new(Deg(180.0), 1.0, 0.70);
462        let ehsi3 = eHsi::from_hsi(&hsi3);
463        assert_eq!(ehsi3, None);
464    }
465
466    #[test]
467    fn test_ehsi_to_rgb() {
468        let test_data = test::build_hs_test_data();
469
470        for item in test_data.iter() {
471            let hsi = eHsi::<_, Deg<_>>::from_color(&item.rgb);
472            let rgb = Rgb::from_color(&hsi);
473            assert_relative_eq!(rgb, item.rgb, epsilon = 2e-3);
474        }
475
476        let big_test_data = test::build_hwb_test_data();
477
478        for item in big_test_data.iter() {
479            let hsi = eHsi::<_, Deg<_>>::from_color(&item.rgb);
480            let rgb = Rgb::from_color(&hsi);
481            assert_relative_eq!(rgb, item.rgb, epsilon = 2e-3);
482        }
483    }
484
485    #[test]
486    fn test_rgb_to_ehsi() {
487        let test_data = test::build_hs_test_data();
488
489        for item in test_data.iter() {
490            let hsi = eHsi::from_color(&item.rgb);
491            if hsi.is_same_as_hsi() {
492                println!("{}; {}; {}", hsi, item.hsi, item.rgb);
493                assert_relative_eq!(hsi.hue(), item.hsi.hue(), epsilon = 1e-1);
494                assert_relative_eq!(hsi.saturation(), item.hsi.saturation(), epsilon = 2e-3);
495                assert_relative_eq!(hsi.intensity(), item.hsi.intensity(), epsilon = 2e-3);
496            }
497        }
498
499        let c1 = Rgb::new(1.0, 1.0, 1.0);
500        let h1 = eHsi::from_color(&c1);
501        assert_relative_eq!(h1, eHsi::new(Deg(0.0), 1.0, 1.0));
502
503        let c2 = Rgb::new(0.5, 1.0, 1.0);
504        let h2 = eHsi::from_color(&c2);
505        assert_relative_eq!(h2, eHsi::new(Deg(180.0), 1.0, 0.833333333), epsilon = 1e-3);
506    }
507
508    #[test]
509    fn test_color_cast() {
510        let c1 = eHsi::new(Deg(240.0f32), 0.22f32, 0.81f32);
511        assert_relative_eq!(c1.color_cast::<f32, Turns<f32>>().color_cast(), c1);
512        assert_relative_eq!(c1.color_cast(), c1);
513        assert_relative_eq!(
514            c1.color_cast(),
515            eHsi::new(Turns(0.6666666), 0.22, 0.81),
516            epsilon = 1e-5
517        );
518    }
519}