pub mod observer_data;
mod rgbxyz;
mod optimal;
pub use optimal::OptimalColors;
mod spectral_locus;
pub use spectral_locus::SpectralLocus;
use crate::{
cam::{CieCam02, CieCam16, ViewConditions},
error::Error,
illuminant::{CieIlluminant, Planck},
lab::CieLab,
observer::observer_data::ObserverData,
rgb::RgbSpace,
spectrum::{to_wavelength, Spectrum, NS, SPECTRUM_WAVELENGTH_RANGE},
traits::{Filter, Light},
xyz::{RelXYZ, XYZ},
};
use nalgebra::{Matrix3, Vector3};
use std::{fmt, ops::RangeInclusive};
use strum::{AsRefStr, EnumCount, EnumIter};
#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug, EnumIter, EnumCount, AsRefStr)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Observer {
#[default]
Cie1931,
Cie1964,
Cie2015,
Cie2015_10,
}
impl Observer {
fn data(&self) -> &'static ObserverData {
match self {
Observer::Cie1931 => &observer_data::CIE1931,
Observer::Cie1964 => &observer_data::CIE1964,
Observer::Cie2015 => &observer_data::CIE2015,
Observer::Cie2015_10 => &observer_data::CIE2015_10,
}
}
pub fn name(&self) -> &'static str {
self.data().name
}
pub fn spectral_locus(&self) -> &SpectralLocus {
self.data()
.spectral_locus
.get_or_init(|| SpectralLocus::new(*self))
}
pub fn rel_xyz(&self, light: &dyn Light, filter: &dyn Filter) -> RelXYZ {
let xyzn = self.xyz_from_spectrum(&light.spectrum());
let s = *light.spectrum() * *filter.spectrum();
let xyz = self.xyz_from_spectrum(&s);
let scale = 100.0 / xyzn.xyz.y;
RelXYZ::from_xyz(xyz * scale, xyzn * scale).unwrap()
}
pub fn xyz(&self, light: &dyn Light, filter: Option<&dyn Filter>) -> XYZ {
let xyzn = light.xyzn(self.data().tag, None);
if let Some(flt) = filter {
let s = *light.spectrum() * *flt.spectrum();
let xyz = self.xyz_from_spectrum(&s);
let scale = 100.0 / xyzn.xyz.y;
xyz * scale
} else {
xyzn
}
}
pub fn lab(&self, light: &dyn Light, filter: &dyn Filter) -> CieLab {
let rxyz = self.rel_xyz(light, filter);
CieLab::from_rxyz(rxyz)
}
pub fn lab_d65(&self, filter: &dyn Filter) -> CieLab {
self.lab(&crate::illuminant::D65, filter)
}
pub fn lab_d50(&self, filter: &dyn Filter) -> CieLab {
self.lab(&crate::illuminant::D50, filter)
}
pub fn ciecam16(&self, light: &dyn Light, filter: &dyn Filter, vc: ViewConditions) -> CieCam16 {
let rxyz = self.rel_xyz(light, filter);
CieCam16::from_xyz(rxyz, vc)
}
pub fn ciecam02(&self, light: &dyn Light, filter: &dyn Filter, vc: ViewConditions) -> CieCam02 {
let rxyz = self.rel_xyz(light, filter);
CieCam02::from_xyz(rxyz, vc)
}
pub fn xyz_from_spectrum(&self, spectrum: &Spectrum) -> XYZ {
let xyz = self.data().data * spectrum.0 * self.data().lumconst;
XYZ::from_vec(xyz, self.data().tag)
}
pub fn y_from_spectrum(&self, spectrum: &Spectrum) -> f64 {
(self.data().data.row(1) * spectrum.0 * self.data().lumconst)[(0, 0)]
}
pub fn xyz_at_wavelength(&self, wavelength: usize) -> Result<XYZ, Error> {
if !SPECTRUM_WAVELENGTH_RANGE.contains(&wavelength) {
return Err(Error::WavelengthOutOfRange);
};
let &[x, y, z] = self
.data()
.data
.column(wavelength - SPECTRUM_WAVELENGTH_RANGE.start())
.as_ref();
Ok(XYZ::from_vec(Vector3::new(x, y, z), self.data().tag))
}
pub fn monochromes(&self, ref_white: CieIlluminant) -> Vec<(usize, RelXYZ)> {
let mut obs = self.data().data;
let white = &ref_white.illuminant().as_ref().0;
for r in 0..3 {
for c in 0..NS {
obs[(r, c)] *= white[c];
}
}
let xyzn_vec = obs.column_sum();
let xyzn = XYZ::from_vec(xyzn_vec, self.data().tag);
let mut v = Vec::with_capacity(NS);
for w in SPECTRUM_WAVELENGTH_RANGE {
let xyz = obs.column(w - SPECTRUM_WAVELENGTH_RANGE.start()).into();
let rxyz = RelXYZ::from_vec(xyz, xyzn);
v.push((w, rxyz.set_illuminance(100.0)));
}
v
}
pub fn trimmed_spectral_locus(&self, ref_white: CieIlluminant) -> Vec<(usize, RelXYZ)> {
let sl_full = self.monochromes(ref_white);
let valid_range = self.spectral_locus_wavelength_range();
sl_full
.into_iter()
.filter(|(w, _)| valid_range.contains(w))
.collect()
}
pub fn xyz_cie_table(&self, std_illuminant: &CieIlluminant, illuminance: Option<f64>) -> XYZ {
let xyz = self.xyz_from_spectrum(std_illuminant.illuminant().as_ref());
if let Some(l) = illuminance {
xyz.set_illuminance(l)
} else {
xyz
}
}
pub fn xyz_d65(&self) -> XYZ {
*self.data().d65.get_or_init(|| {
self.xyz_from_spectrum(CieIlluminant::D65.illuminant().as_ref())
.set_illuminance(100.0)
})
}
pub fn xyz_d50(&self) -> XYZ {
*self.data().d50.get_or_init(|| {
self.xyz_from_spectrum(CieIlluminant::D50.illuminant().as_ref())
.set_illuminance(100.0)
})
}
pub fn xyz_planckian_locus(&self, cct: f64) -> XYZ {
let p = Planck::new(cct);
self.xyz_from_fn(|l| p.at_wavelength(to_wavelength(l, 0.0, 1.0)))
}
pub fn planckian_locus(&self) -> Vec<(f64, f64)> {
let mut v = Vec::with_capacity(100);
for i in 1..100 {
let cct = 1E6 / (i as f64 * 10.0);
let xy = self.xyz_planckian_locus(cct).chromaticity();
v.push((xy.x(), xy.y()));
}
v
}
pub fn xyz_planckian_locus_slope(&self, cct: f64) -> XYZ {
let p = Planck::new(cct);
self.xyz_from_fn(|l| p.slope_at_wavelength(to_wavelength(l, 0.0, 1.0)))
}
pub fn calc_rgb2xyz_matrix_with_alt_white(
&self,
rgbspace: RgbSpace,
opt_white: Option<XYZ>,
) -> Matrix3<f64> {
let mut rgb2xyz: nalgebra::Matrix<
f64,
nalgebra::Const<3>,
nalgebra::Const<3>,
nalgebra::ArrayStorage<f64, 3, 3>,
> = Matrix3::from_iterator(
rgbspace
.primaries()
.iter()
.flat_map(|s| self.xyz_from_spectrum(s).set_illuminance(1.0).to_array()),
);
let xyzw = opt_white
.unwrap_or(self.xyz(&rgbspace.white(), None))
.set_illuminance(1.0);
assert_eq!(&xyzw.observer(), self);
let decomp = rgb2xyz.lu();
let rgbw = decomp.solve(&xyzw.xyz).unwrap();
for (i, mut col) in rgb2xyz.column_iter_mut().enumerate() {
col *= rgbw[i];
}
rgb2xyz
}
pub fn calc_rgb2xyz_matrix(&self, rgbspace: RgbSpace) -> Matrix3<f64> {
let mut rgb2xyz: nalgebra::Matrix<
f64,
nalgebra::Const<3>,
nalgebra::Const<3>,
nalgebra::ArrayStorage<f64, 3, 3>,
> = Matrix3::from_iterator(
rgbspace
.primaries()
.iter()
.flat_map(|s| self.xyz_from_spectrum(s).set_illuminance(1.0).to_array()),
);
let xyzw = self.xyz(&rgbspace.white(), None).set_illuminance(1.0);
let decomp = rgb2xyz.lu();
let rgbw = decomp.solve(&xyzw.xyz).unwrap();
for (i, mut col) in rgb2xyz.column_iter_mut().enumerate() {
col *= rgbw[i];
}
rgb2xyz
}
pub fn calc_xyz2rgb_matrix(&self, rgbspace: RgbSpace) -> Matrix3<f64> {
self.calc_rgb2xyz_matrix(rgbspace).try_inverse().unwrap()
}
pub fn spectral_locus_wavelength_range(&self) -> RangeInclusive<usize> {
self.data().spectral_locus_range.clone()
}
pub fn xyz_from_fn(&self, f: impl Fn(f64) -> f64) -> XYZ {
let xyz = self
.data()
.data
.column_iter()
.enumerate()
.fold(Vector3::zeros(), |acc, (i, cmf)| {
acc + cmf * f(i as f64 / (NS - 1) as f64)
});
XYZ {
xyz,
observer: self.data().tag,
}
}
pub fn xyz_from_colorant_fn(&self, illuminant: &CieIlluminant, f: impl Fn(f64) -> f64) -> XYZ {
let ill = illuminant.illuminant();
let xyzn = self.xyz_cie_table(illuminant, None);
let xyz = self
.data()
.data
.column_iter()
.zip(ill.0 .0.iter())
.enumerate()
.fold(Vector3::zeros(), |acc, (i, (cmfi, sv))| {
acc + cmfi * f(i as f64 / (NS - 1) as f64).clamp(0.0, 1.0) * *sv
});
let scale = 100.0 / xyzn.xyz.y;
XYZ {
xyz: xyz * self.data().lumconst * scale,
observer: self.data().tag,
}
}
}
impl fmt::Display for Observer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.name().fmt(f)
}
}
#[cfg(test)]
mod obs_test {
use super::Observer::Cie1964;
use super::{Observer, Observer::Cie1931};
use crate::{
illuminant::CieIlluminant, rgb::RgbSpace, spectrum::SPECTRUM_WAVELENGTH_RANGE, xyz::XYZ,
};
use approx::{assert_abs_diff_eq, assert_ulps_eq};
use strum::IntoEnumIterator as _;
#[test]
fn test_rel_xyz() {
use crate::colorant::Colorant;
let obs = Cie1931;
let light = CieIlluminant::D65;
let filter = Colorant::gray(0.5);
let rxyz = obs.rel_xyz(&light, &filter);
assert_ulps_eq!(rxyz.xyz() * 2.0, rxyz.white_point(), epsilon = 1E-5);
assert_ulps_eq!(rxyz.white_point(), obs.xyz_d65(), epsilon = 1E-5);
}
#[test]
fn test_cie1931_spectral_locus_min_max() {
let wavelength_lange = Cie1931.spectral_locus_wavelength_range();
let chromaticity = Cie1931
.xyz_at_wavelength(*wavelength_lange.start())
.unwrap()
.chromaticity();
assert_ulps_eq!(chromaticity.x(), 0.17411, epsilon = 1E-5);
assert_ulps_eq!(chromaticity.y(), 0.00496, epsilon = 1E-5);
let chromaticity = Cie1931
.xyz_at_wavelength(*wavelength_lange.end())
.unwrap()
.chromaticity();
assert_ulps_eq!(chromaticity.x(), 0.73469, epsilon = 1E-5);
assert_ulps_eq!(chromaticity.y(), 0.26531, epsilon = 1E-5);
}
#[test]
fn test_spectral_locus_full() {
for observer in Observer::iter() {
let wavelength_range = observer.spectral_locus_wavelength_range();
assert!(wavelength_range.start() >= SPECTRUM_WAVELENGTH_RANGE.start());
assert!(wavelength_range.end() <= SPECTRUM_WAVELENGTH_RANGE.end());
assert!(wavelength_range.start() < wavelength_range.end());
for wavelength in wavelength_range {
let xyz = observer.xyz_at_wavelength(wavelength).unwrap();
let chromaticity = xyz.chromaticity();
assert!((0.0..=1.0).contains(&chromaticity.x()));
assert!((0.0..=1.0).contains(&chromaticity.y()));
let _xyz2 = XYZ::from_chromaticity(chromaticity, None, Some(observer)).unwrap();
}
}
}
#[test]
fn test_spectral_locus_to_rgb() {
for observer in Observer::iter() {
eprintln!("Testing observer {observer:?}");
for wavelength in observer.spectral_locus_wavelength_range() {
let xyz = observer.xyz_at_wavelength(wavelength).unwrap();
for rgbspace in RgbSpace::iter() {
let _rgb = xyz.rgb(rgbspace);
}
}
}
}
#[test]
fn test_rgb2xyz_cie1931() {
let want = nalgebra::Matrix3::new(
0.4124564, 0.3575761, 0.1804375, 0.2126729, 0.7151522, 0.0721750, 0.0193339, 0.1191920,
0.9503041,
);
let got = Cie1931.rgb2xyz_matrix(crate::rgb::RgbSpace::SRGB);
approx::assert_ulps_eq!(want, got, epsilon = 3E-4);
}
#[test]
fn test_xyz2rgb_cie1931() {
let want = nalgebra::Matrix3::new(
3.2404542, -1.5371385, -0.4985314, -0.9692660, 1.8760108, 0.0415560, 0.0556434,
-0.2040259, 1.0572252,
);
let got = Cie1931.xyz2rgb_matrix(crate::rgb::RgbSpace::SRGB);
approx::assert_ulps_eq!(want, got, epsilon = 3E-4);
}
#[test]
fn test_planckian_locus() {
let xy = Cie1931
.xyz_planckian_locus(3000.0)
.chromaticity()
.to_array();
approx::assert_abs_diff_eq!(&xy.as_ref(), &[0.43693, 0.40407].as_ref(), epsilon = 2E-5);
let xy = Cie1931
.xyz_planckian_locus(6500.0)
.chromaticity()
.to_array();
approx::assert_abs_diff_eq!(&xy.as_ref(), &[0.31352, 0.32363].as_ref(), epsilon = 6E-5);
}
#[test]
fn test_xyz_from_illuminant_x_fn() {
let xyz = Cie1931.xyz_from_colorant_fn(&CieIlluminant::D65, |_v| 1.0);
let d65xyz = Cie1931.xyz_d65().xyz;
approx::assert_ulps_eq!(
xyz,
crate::xyz::XYZ::from_vec(d65xyz, crate::observer::Observer::Cie1931)
);
}
#[test]
fn test_xyz_of_sample_with_standard_illuminant() {
use crate::illuminant::CieIlluminant::D65 as d65;
let xyz = Cie1931
.xyz(&d65, Some(&crate::colorant::Colorant::white()))
.set_illuminance(100.0);
approx::assert_ulps_eq!(xyz, Cie1931.xyz_from_colorant_fn(&d65, |_| 1.0));
let xyz = Cie1931.xyz(&d65, Some(&crate::colorant::Colorant::black()));
approx::assert_ulps_eq!(xyz, Cie1931.xyz_from_colorant_fn(&d65, |_| 0.0));
}
#[test]
fn test_xyz_d65_d50() {
let cie1931_d65_xyz = Cie1931.xyz_d65();
approx::assert_ulps_eq!(
cie1931_d65_xyz.to_array().as_ref(),
[95.047, 100.0, 108.883].as_ref(),
epsilon = 5E-2
);
let cie1931_d50_xyz = Cie1931.xyz_d50();
approx::assert_ulps_eq!(
cie1931_d50_xyz.to_array().as_ref(),
[96.421, 100.0, 82.519].as_ref(),
epsilon = 5E-2
);
}
#[test]
fn test_xyz_d65_d50_cie1964() {
let cie1964_d50_xyz = Cie1964.xyz_d50();
approx::assert_ulps_eq!(
cie1964_d50_xyz.to_array().as_ref(),
[96.720, 100.0, 81.427].as_ref(),
epsilon = 5E-2
);
let cie1964_d65_xyz = Cie1964.xyz_d65();
approx::assert_ulps_eq!(
cie1964_d65_xyz.to_array().as_ref(),
[94.811, 100.0, 107.304].as_ref(),
epsilon = 5E-2
);
}
#[test]
fn test_spectral_locus() {
use crate::illuminant::CieIlluminant;
use crate::observer::Observer::Cie1931;
let sl = Cie1931.monochromes(CieIlluminant::D65);
assert_eq!(sl.len(), 401);
let xyz_first = sl[0].1.xyz().to_array();
assert_abs_diff_eq!(
xyz_first.as_ref(),
[6.46976E-04, 1.84445E-05, 3.05044E-03].as_ref(),
epsilon = 1E-5
);
let xyz_last = sl[sl.len() - 1].1.xyz().to_array();
assert_abs_diff_eq!(
xyz_last.as_ref(),
[2.48982E-05, 8.99121E-06, 0.0].as_ref(),
epsilon = 1E-5
);
let xyz_550 = sl[170].1.xyz().to_array();
assert_abs_diff_eq!(
xyz_550.as_ref(),
[0.4268018, 0.9796899, 0.0086158].as_ref(),
epsilon = 1E-4
);
}
#[test]
fn test_as_ref_str() {
assert_eq!(Cie1931.as_ref(), "Cie1931");
assert_eq!(Cie1964.as_ref(), "Cie1964");
assert_eq!(Observer::Cie2015.as_ref(), "Cie2015");
assert_eq!(Observer::Cie2015_10.as_ref(), "Cie2015_10");
}
#[test]
fn test_rgb_xyz_white() {
for obs in Observer::iter() {
for space in RgbSpace::iter() {
let d65 = obs.xyz_d65().set_illuminance(1.0);
let xyz2rgb = obs.xyz2rgb_matrix(space);
let rgb = xyz2rgb * d65.xyz;
rgb.iter().for_each(|&v| {
assert_abs_diff_eq!(v, 1.0, epsilon = 2E-6);
});
let xyz_round_trip = XYZ::from_vec(obs.rgb2xyz_matrix(space) * rgb, obs);
assert_abs_diff_eq!(d65, xyz_round_trip, epsilon = 2E-6);
}
}
}
}