use super::{CamJCh, CamTransforms, ViewConditions};
use nalgebra::Vector3;
use crate::{
error::Error,
observer::Observer,
rgb::{RgbSpace, WideRgb},
xyz::{RelXYZ, XYZ},
};
#[derive(Debug)]
pub struct CieCam16(CamJCh);
impl CieCam16 {
pub fn new(jch: [f64; 3], xyzn: XYZ, vc: ViewConditions) -> Self {
Self(CamJCh {
jch: Vector3::from(jch),
xyzn,
vc,
})
}
pub fn from_xyz(rxyz: RelXYZ, vc: ViewConditions) -> Self {
Self(CamJCh::from_xyz(rxyz, vc, super::Cam::CieCam16))
}
pub fn xyz(
&self,
opt_xyzn: Option<XYZ>,
opt_viewconditions: Option<ViewConditions>,
) -> Result<RelXYZ, Error> {
self.0
.xyz(opt_xyzn, opt_viewconditions, super::Cam::CieCam16)
}
pub fn rgb(
&self,
rgbspace: RgbSpace,
opt_viewconditions: Option<ViewConditions>,
) -> Result<WideRgb, Error> {
self.0
.rgb(rgbspace, opt_viewconditions, super::Cam::CieCam16)
}
pub fn de_ucs(&self, other: &Self) -> Result<f64, crate::Error> {
if self.observer() != other.observer() {
return Err(crate::Error::RequireSameObserver);
}
let jabp1 = self.jab_prime();
let jabp2 = other.jab_prime();
let de_ucs_prime = Self::delta_e_prime_from_jabp(jabp1.as_ref(), jabp2.as_ref());
Ok(1.41 * de_ucs_prime.powf(0.63))
}
}
impl CamTransforms for CieCam16 {
fn jch_vec(&self) -> &Vector3<f64> {
&self.0.jch
}
fn view_conditions(&self) -> &ViewConditions {
&self.0.vc
}
fn observer(&self) -> Observer {
self.0.xyzn.observer
}
fn xyzn(&self) -> &Vector3<f64> {
&self.0.xyzn.xyz
}
}
#[cfg(test)]
mod cam16_test {
use approx::assert_abs_diff_eq;
use nalgebra::Matrix3;
use crate::cam::{CamTransforms, CieCam16, ViewConditions, M16, M16INV};
use crate::observer::Observer;
use crate::xyz::{RelXYZ, XYZ};
#[test]
fn test_m16() {
approx::assert_abs_diff_eq!(M16INV * M16, Matrix3::identity(), epsilon = 1E-8);
}
#[test]
fn test_worked_example() {
let xyz = XYZ::new([60.70, 49.60, 10.29], Observer::Cie1931);
let xyzn = XYZ::new([96.46, 100.0, 108.62], Observer::Cie1931);
let rxyz = RelXYZ::from_xyz(xyz, xyzn).unwrap();
let vc = ViewConditions::new(40.0, 16.0, 0.69, 1.0, 1.0, None);
let cam = CieCam16::from_xyz(rxyz, vc);
let &[j, c, h] = cam.jch_vec().as_ref();
approx::assert_abs_diff_eq!(j, 70.4406, epsilon = 1E-4);
approx::assert_abs_diff_eq!(c, 58.6035, epsilon = 1E-4);
approx::assert_abs_diff_eq!(h, 57.9145, epsilon = 1E-4);
let xyz_rev = cam.xyz(None, Some(vc)).unwrap();
assert_abs_diff_eq!(rxyz, xyz_rev, epsilon = 1E-4);
}
}
#[cfg(test)]
mod cam16_round_trip_tests {
use crate::cam::{CamTransforms, CieCam16, ViewConditions};
use crate::observer::Observer::Cie1931;
use crate::xyz::{RelXYZ, XYZ};
use approx::assert_abs_diff_eq;
#[test]
fn xyz_jch_xyz_round_trip() {
let samples = &[
[19.01, 20.00, 21.78],
[41.24, 21.26, 1.93],
[95.05, 100.00, 108.88],
[50.00, 50.00, 50.0],
[0.00, 0.00, 0.00],
[1.00, 1.00, 1.00],
[70.00, 50.00, 30.00],
[30.00, 60.00, 90.00],
[12.14, 28.56, 5.00],
[5.00, 12.14, 28.56],
[100.00, 100.00, 100.00],
[50.00, 50.00, 50.00],
[20.00, 30.00, 40.00],
[10.00, 20.00, 30.00],
[0.3, 1.0, 0.3],
];
for &xyz_arr in samples {
let xyz = XYZ::new(xyz_arr, Cie1931);
let xyz_d65 = Cie1931.xyz_d65();
let rxyz = RelXYZ::from_xyz(xyz, xyz_d65).unwrap();
let cam = CieCam16::from_xyz(rxyz, ViewConditions::default());
let jch = cam.jch();
let cam_back = CieCam16::new(jch, xyz_d65, ViewConditions::default());
let rxyz_back = cam_back.xyz(None, None).unwrap();
assert_abs_diff_eq!(rxyz, rxyz_back, epsilon = 1e-6);
}
}
}
#[cfg(test)]
mod rgb_test {
#[test]
#[cfg(all(feature = "munsell", feature = "cie-illuminants"))]
fn rgb_match() {
use crate::{
cam::{ViewConditions, CIE248_HOME_SCREEN},
colorant::Munsell,
illuminant::LED_B2,
observer::Observer::{Cie1931, Cie2015_10},
rgb::RgbSpace::SRGB,
};
let paint = Munsell::try_new("5BG5/8").unwrap();
let vc = ViewConditions::average_surround(6.0);
let cam_paint = Cie2015_10.ciecam16(&LED_B2, &paint, vc);
let rgb_2015 = cam_paint
.rgb(SRGB, Some(CIE248_HOME_SCREEN))
.unwrap()
.compress();
let xyz_1931 = Cie1931.xyz(&rgb_2015, None);
let rgb_1931 = xyz_1931.rgb(SRGB).compress();
let [r, g, b]: [u8; 3] = rgb_1931.into();
assert!(r == 0 && g == 113 && b == 138);
}
}