Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement simulators for dichromacy and anomalous trichromacy #414

Closed
wants to merge 9 commits into from
54 changes: 54 additions & 0 deletions palette/src/cvd.rs
Original file line number Diff line number Diff line change
@@ -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<S = Vienot1999, M = SmithPokorny> = DichromacySimul<Protan, S, M>;

/// 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<S = Vienot1999, M = SmithPokorny> = DichromacySimul<Deutan, S, M>;

/// 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<S = Brettel1997, M = SmithPokorny> = DichromacySimul<Tritan, S, M>;

/// 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<S = Vienot1999, M = SmithPokorny> =
AnomalousTrichromacySimul<Protan, S, M>;

/// 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<S = Vienot1999, M = SmithPokorny> =
AnomalousTrichromacySimul<Deutan, S, M>;

/// 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<S = Brettel1997, M = SmithPokorny> =
AnomalousTrichromacySimul<Tritan, S, M>;
255 changes: 255 additions & 0 deletions palette/src/cvd/cone_response.rs
Original file line number Diff line number Diff line change
@@ -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<M, T>(lms: Lms<M, T>, severity: Option<T>) -> Lms<M, T>
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<M, T>(lms: Lms<M, T>, severity: Option<T>) -> Lms<M, T>
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<M, T>(lms: Lms<M, T>, severity: Option<T>) -> Lms<M, T>
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<M, T> = 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<M, T>(lms: Lms<M, T>, severity: Option<T>) -> Lms<M, T>
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<M, T> = 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<M, T>(lms: Lms<M, T>, severity: Option<T>) -> Lms<M, T>
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<M, T> = 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<M, T>(lms: Lms<M, T>, severity: Option<T>) -> Lms<M, T>
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<M, T> = 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<M, T>(lms: Lms<M, T>, severity: Option<T>) -> Lms<M, T>
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<M, T> = 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<M, T>(lms: Lms<M, T>, severity: Option<T>) -> Lms<M, T>
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<M, T> = 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)
}
}
}
Loading
Loading