diff --git a/palette/src/cvd.rs b/palette/src/cvd.rs new file mode 100644 index 000000000..f79daf2ff --- /dev/null +++ b/palette/src/cvd.rs @@ -0,0 +1,54 @@ +//! Simulate various types of color vision deficiency. + +use crate::{ + cvd::{cone_response::*, simulation::*}, + lms::matrix::SmithPokorny, +}; + +pub mod cone_response; +pub mod simulation; + +/// Simulator for protanopia, a form of dichromacy correlated to a missing or +/// non-functional long (red) cone. +/// +/// By default this uses the [`Vienot1999`] simulation method for the sake of +/// efficiency and accuracy with extreme values. +pub type ProtanopiaSimul = DichromacySimul; + +/// Simulator for deuteranopia, a form of dichromacy correlated to a missing or +/// non-functional medium (green) cone. +/// +/// By default this uses the [`Vienot1999`] simulation method for the sake of +/// efficiency and accuracy with extreme values. +pub type DeuteranopiaSimul = DichromacySimul; + +/// Simulator for tritanopia, a form of dichromacy correlated to a missing or +/// non-functional short (blue) cone. +/// +/// By default this uses the [`Brettel1997`] since other methods are much less +/// accurate for tritanopia. +pub type TritanopiaSimul = DichromacySimul; + +/// Simulator for protanomaly, a form of anomalous trichromacy correlated to an +/// anomalous long (red) cone. +/// +/// The current default implementation uses linear interpolation, which is not +/// ideal, so this default implementation may change in the future. +pub type ProtanomalySimul = + AnomalousTrichromacySimul; + +/// Simulator for deuteranomaly, a form of anomalous trichromacy correlated to an +/// anomalous medium (green) cone. +/// +/// The current default implementation uses linear interpolation, which is not +/// ideal, so this default implementation may change in the future. +pub type DeuteranomalySimul = + AnomalousTrichromacySimul; + +/// Simulator for tritanomaly, a form of anomalous trichromacy correlated to an +/// anomalous short (blue) cone. +/// +/// The current default implementation uses linear interpolation, which is not +/// ideal, so this default implementation may change in the future. +pub type TritanomalySimul = + AnomalousTrichromacySimul; diff --git a/palette/src/cvd/cone_response.rs b/palette/src/cvd/cone_response.rs new file mode 100644 index 000000000..733bd2159 --- /dev/null +++ b/palette/src/cvd/cone_response.rs @@ -0,0 +1,255 @@ +//! The color vision deficiency cone response types. + +use crate::{ + bool_mask::{HasBoolMask, LazySelect}, + convert::{ConvertOnce, Matrix3}, + lms::Lms, + matrix::matrix_map, + num::{Arithmetics, PartialCmp, Real}, +}; + +/// A type of color deficient cone response. +pub trait ConeResponse { + /// Simulates color vision deficiency for this cone response type by the method, + /// [`Brettel1997`](crate::cvd::simulation::Brettel1997), for given `severity`. + fn projection_brettel_1997(lms: Lms, severity: Option) -> Lms + where + T: Real + Arithmetics + PartialCmp + Clone, + <[T; 9] as HasBoolMask>::Mask: LazySelect<[T; 9]>; + /// Simulates color vision deficiency for this cone response type by the method, + /// [`Vienot1999`](crate::cvd::simulation::Vienot1999), for given `severity`. + fn projection_vienot_1999(lms: Lms, severity: Option) -> Lms + where + T: Real + Arithmetics + Clone; +} + +/// The cone response associated with a deficient long (red) cone. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Protan; + +impl ConeResponse for Protan { + #[inline] + fn projection_brettel_1997(lms: Lms, severity: Option) -> Lms + where + T: Real + Arithmetics + PartialCmp + Clone, + <[T; 9] as HasBoolMask>::Mask: LazySelect<[T; 9]>, + { + // Values calculated using information and methods described at + // https://daltonlens.org/understanding-cvd-simulation/ + let matrix = lazy_select! { + if (T::from_f64(-0.0480077092304) * &lms.medium + + T::from_f64(0.998846965183) * &lms.short) + .gt(&T::from_f64(0.0)) => + { + matrix_map( + [ + 0.0, 2.27376142579, -5.92721533669, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0, + ], + T::from_f64, + ) + }, + else => { + matrix_map( + [ + 0.0, 2.18595044625, -4.10022271756, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0, + ], + T::from_f64, + ) + }}; + if let Some(s) = severity { + let original = lms.clone(); + let clamped: Lms = Matrix3::from_array(matrix).convert_once(lms); + clamped * s.clone() + original * (T::from_f64(1.0) - s) + } else { + Matrix3::from_array(matrix).convert_once(lms) + } + } + + #[inline] + fn projection_vienot_1999(lms: Lms, severity: Option) -> Lms + where + T: Real + Arithmetics + Clone, + { + // Values calculated using information and methods described at + // https://daltonlens.org/understanding-cvd-simulation/ + let matrix = matrix_map( + [ + 0.0, + 2.02344337265, + -2.52580325429, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + ], + T::from_f64, + ); + if let Some(s) = severity { + let original = lms.clone(); + let clamped: Lms = Matrix3::from_array(matrix).convert_once(lms); + clamped * s.clone() + original * (T::from_f64(1.0) - s) + } else { + Matrix3::from_array(matrix).convert_once(lms) + } + } +} + +/// The cone response associated with a deficient medium (green) cone. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Deutan; + +impl ConeResponse for Deutan { + #[inline] + fn projection_brettel_1997(lms: Lms, severity: Option) -> Lms + where + T: Real + Arithmetics + PartialCmp + Clone, + <[T; 9] as HasBoolMask>::Mask: LazySelect<[T; 9]>, + { + // Values calculated using information and methods described at + // https://daltonlens.org/understanding-cvd-simulation/ + let matrix = lazy_select! { + if (T::from_f64(-0.024158861984) * &lms.long + + T::from_f64(0.9997081321) * &lms.short) + .gt(&T::from_f64(0.0)) => + { + matrix_map( + [ + 1.0, 0.0, 0.0, + 0.439799879027, 0.0, 2.60678858804, + 0.0, 0.0, 1.0, + ], + T::from_f64, + ) + }, + else => { + matrix_map( + [ + 1.0, 0.0, 0.0, + 0.457466911802, 0.0, 1.8757162243, + 0.0, 0.0, 1.0, + ], + T::from_f64, + ) + }}; + if let Some(s) = severity { + let original = lms.clone(); + let clamped: Lms = Matrix3::from_array(matrix).convert_once(lms); + clamped * s.clone() + original * (T::from_f64(1.0) - s) + } else { + Matrix3::from_array(matrix).convert_once(lms) + } + } + + #[inline] + fn projection_vienot_1999(lms: Lms, severity: Option) -> Lms + where + T: Real + Arithmetics + Clone, + { + // Values calculated using information and methods described at + // https://daltonlens.org/understanding-cvd-simulation/ + let matrix = matrix_map( + [ + 1.0, + 0.0, + 0.0, + 0.494207059864, + 0.0, + 1.2482698001, + 0.0, + 0.0, + 1.0, + ], + T::from_f64, + ); + if let Some(s) = severity { + let original = lms.clone(); + let clamped: Lms = Matrix3::from_array(matrix).convert_once(lms); + clamped * s.clone() + original * (T::from_f64(1.0) - s) + } else { + Matrix3::from_array(matrix).convert_once(lms) + } + } +} + +/// The cone response associated with a deficient short (blue) cone. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Tritan; + +impl ConeResponse for Tritan { + #[inline] + fn projection_brettel_1997(lms: Lms, severity: Option) -> Lms + where + T: Real + Arithmetics + PartialCmp + Clone, + <[T; 9] as HasBoolMask>::Mask: LazySelect<[T; 9]>, + { + // Values calculated using information and methods described at + // https://daltonlens.org/understanding-cvd-simulation/ + let matrix = lazy_select! { + if (T::from_f64(-0.449210402667) * &lms.long + + T::from_f64(0.893425998131) * &lms.short) + .gt(&T::from_f64(0.0)) => + { + matrix_map( + [ + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + -0.0557429252223, 0.158929167991, 0.0, + ], + T::from_f64, + ) + }, + else => { + matrix_map( + [ + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + -0.00254865354166, 0.0531320960863, 0.0, + ], + T::from_f64, + ) + }}; + if let Some(s) = severity { + let original = lms.clone(); + let clamped: Lms = Matrix3::from_array(matrix).convert_once(lms); + clamped * s.clone() + original * (T::from_f64(1.0) - s) + } else { + Matrix3::from_array(matrix).convert_once(lms) + } + } + + #[inline] + fn projection_vienot_1999(lms: Lms, severity: Option) -> Lms + where + T: Real + Arithmetics + Clone, + { + // Values calculated using information and methods described at + // https://daltonlens.org/understanding-cvd-simulation/ + let matrix = matrix_map( + [ + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + -0.012244922724, + 0.0720343802523, + 0.0, + ], + T::from_f64, + ); + if let Some(s) = severity { + let original = lms.clone(); + let clamped: Lms = Matrix3::from_array(matrix).convert_once(lms); + clamped * s.clone() + original * (T::from_f64(1.0) - s) + } else { + Matrix3::from_array(matrix).convert_once(lms) + } + } +} diff --git a/palette/src/cvd/simulation.rs b/palette/src/cvd/simulation.rs new file mode 100644 index 000000000..e7f808e4d --- /dev/null +++ b/palette/src/cvd/simulation.rs @@ -0,0 +1,128 @@ +//! Simulators and simulation methods for color vision deficiency. + +use core::marker::PhantomData; + +use crate::{ + bool_mask::{HasBoolMask, LazySelect}, + cvd::cone_response::ConeResponse, + lms::{matrix::WithLmsMatrix, Lms}, + num::{Arithmetics, PartialCmp, Real}, + white_point::WhitePoint, + xyz::meta::HasXyzMeta, + FromColor, IntoColor, Xyz, +}; + +/// A color deficiency simulator that converts colors to dichromatic vision +/// based on the [`ConeResponse`]: `Cn`, [`SimulationMethod`]: `S`, and LMS +/// matrix: `M`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct DichromacySimul(PhantomData, PhantomData, PhantomData); + +impl DichromacySimul +where + Cn: ConeResponse, + S: SimulationMethod, + M: HasXyzMeta, +{ + /// Converts a color into the percieved color of an individual with + /// dichromacy using the generic settings of `Self`. + pub fn simulate_deficiency(color: C) -> C + where + C: FromColor> + IntoColor>, + Wp: WhitePoint, + T: Real + Arithmetics + PartialCmp + Clone, + Lms, T>: FromColor> + IntoColor>, + <[T; 9] as HasBoolMask>::Mask: LazySelect<[T; 9]>, + { + let xyz: Xyz = color.into_color(); + let lms = Lms::, T>::from_color(xyz); + let clamped_xyz: Xyz = + S::clamp_by_deficiency::, T>(lms, None).into_color(); + clamped_xyz.into_color() + } +} + +/// A color deficiency simulator that converts colors to anomalous trichromatic +/// vision based on the [`ConeResponse`]: `Cn`, [`SimulationMethod`]: `S`, and +/// LMS matrix: `M`. +/// +/// Currently, all implemented simulation methods use linear interpolation +/// between the original and clamped color, which is not ideal, but is an alright +/// approximation. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct AnomalousTrichromacySimul(PhantomData, PhantomData, PhantomData); + +impl AnomalousTrichromacySimul +where + Cn: ConeResponse, + S: SimulationMethod, +{ + /// Converts a color into the percieved color of an individual with + /// anomalous trichromacy using the generic settings of `Self`. + #[inline] + pub fn simulate_deficiency_by_severity(color: C, severity: T) -> C + where + C: IntoColor> + FromColor>, + T: Real + Arithmetics + PartialCmp + Clone, + <[T; 9] as HasBoolMask>::Mask: LazySelect<[T; 9]>, + { + let lms = color.into_color(); + S::clamp_by_deficiency::(lms, Some(severity)).into_color() + } +} + +/// Represents a method for projecting typical color vision onto that of someone +/// with a color vision deficiency. +/// +/// The generic `Cn` must implement [`ConeResponse`] which contains transformation +/// functions for each method. +pub trait SimulationMethod { + /// Clamps a color in LMS space according to the color deficient cone response. + fn clamp_by_deficiency(lms: Lms<_M, T>, severity: Option) -> Lms<_M, T> + where + Cn: ConeResponse, + T: Real + Arithmetics + PartialCmp + Clone, + <[T; 9] as HasBoolMask>::Mask: LazySelect<[T; 9]>; +} + +/// Color vision deficiency simulation described in +/// "Computerized simulation of color appearance for dichromats" +/// by Brettel, H., Viénot, F., & Mollon, J. D. (1997). +/// +/// This method projects colors in the LMS space onto two half-planes and is the +/// best method for simulating tritan color deficiency. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Brettel1997; + +impl SimulationMethod for Brettel1997 { + #[inline] + fn clamp_by_deficiency(lms: Lms<_M, T>, severity: Option) -> Lms<_M, T> + where + Cn: ConeResponse, + T: Real + Arithmetics + PartialCmp + Clone, + <[T; 9] as HasBoolMask>::Mask: LazySelect<[T; 9]>, + { + Cn::projection_brettel_1997(lms, severity) + } +} + +/// Color vision deficiency simulation described in +/// "Digital video colourmaps for checking the legibility of displays by dichromats" +/// Viénot, F., Brettel, H., & Mollon, J. D. (1999). +/// +/// This method simplifies the method used by [`Brettel1997`] by projecting to a +/// single plane in LMS space, but as a result has a poor approximation of tritan +/// color vision. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Vienot1999; + +impl SimulationMethod for Vienot1999 { + #[inline] + fn clamp_by_deficiency(lms: Lms<_M, T>, severity: Option) -> Lms<_M, T> + where + Cn: ConeResponse, + T: Real + Arithmetics + PartialCmp + Clone, + { + Cn::projection_vienot_1999(lms, severity) + } +} diff --git a/palette/src/lib.rs b/palette/src/lib.rs index 048be73cd..ca141213f 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -353,6 +353,7 @@ pub mod chromatic_adaptation; pub mod color_difference; pub mod color_theory; pub mod convert; +pub mod cvd; pub mod encoding; pub mod hsl; pub mod hsluv; diff --git a/palette/src/lms/matrix.rs b/palette/src/lms/matrix.rs index 04ccb8fb0..f8d75cef2 100644 --- a/palette/src/lms/matrix.rs +++ b/palette/src/lms/matrix.rs @@ -136,6 +136,50 @@ impl HasLmsMatrix for Bradford { type LmsMatrix = Self; } +/// Represents the transformation matrix developed by Smith, V. C. and Pokorny, J. +/// that is frequently used when simulating and/or studying color vision deficiency. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SmithPokorny; + +impl XyzToLms for SmithPokorny +where + T: Real, +{ + #[rustfmt::skip] + #[inline] + fn xyz_to_lms_matrix() -> Mat3 { + // Matrix from http://www.cvrl.org/database/text/cones/sp.htm + [ + T::from_f64( 0.15514), T::from_f64(0.54312), T::from_f64(0.03286), + T::from_f64(-0.15514), T::from_f64(0.45684), T::from_f64(0.03286), + T::from_f64( 0.00000), T::from_f64(0.00000), T::from_f64(0.00801), + ] + } +} + +impl LmsToXyz for SmithPokorny +where + T: Real, +{ + #[rustfmt::skip] + #[inline] + fn lms_to_xyz_matrix() -> Mat3 { + [ + T::from_f64(2.94481), T::from_f64(-3.50098), T::from_f64( 2.28160), + T::from_f64(1.00004), T::from_f64( 1.00004), T::from_f64(-8.20507), + T::from_f64(0.00000), T::from_f64( 0.00000), T::from_f64( 124.844), + ] + } +} + +impl HasXyzMeta for SmithPokorny { + type XyzMeta = Any; +} + +impl HasLmsMatrix for SmithPokorny { + type LmsMatrix = Self; +} + /// Represents a unit matrix, for a 1:1 conversion between XYZ to LMS. /// /// This matrix may be useful in chromatic adaptation, but does otherwise not