1use crate::gamma::{pq_from_linearf, pq_to_linearf};
30use crate::{Matrix3f, Rgb, Vector3f, Xyz};
31
32const CROSSTALK: Matrix3f = Matrix3f {
33 v: [[0.92, 0.04, 0.04], [0.04, 0.92, 0.04], [0.04, 0.04, 0.92]],
34};
35
36const HPE_LMS: Matrix3f = Matrix3f {
37 v: [
38 [0.4002, 0.7076, -0.0808],
39 [-0.2263, 1.1653, 0.0457],
40 [0f32, 0f32, 0.9182],
41 ],
42};
43
44const XYZ_TO_LMS: Matrix3f = CROSSTALK.mat_mul_const(HPE_LMS);
45
46const LMS_TO_XYZ: Matrix3f = XYZ_TO_LMS.inverse();
47
48const L_LMS_TO_ICTCP: Matrix3f = Matrix3f {
49 v: [
50 [2048. / 4096., 2048. / 4096., 0.],
51 [6610. / 4096., -13613. / 4096., 7003. / 4096.],
52 [17933. / 4096., -17390. / 4096., -543. / 4096.],
53 ],
54};
55
56const ICTCP_TO_L_LMS: Matrix3f = L_LMS_TO_ICTCP.inverse();
57
58#[derive(Copy, Clone, Default, PartialOrd, PartialEq)]
59pub struct ICtCp {
60 pub i: f32,
62 pub ct: f32,
64 pub cp: f32,
66}
67
68impl ICtCp {
69 #[inline]
70 pub const fn new(i: f32, ct: f32, cp: f32) -> ICtCp {
71 ICtCp { i, ct, cp }
72 }
73
74 #[inline]
76 pub fn from_xyz(xyz: Xyz) -> ICtCp {
77 let lms = XYZ_TO_LMS.mul_vector(xyz.to_vector());
78 let lin_l = pq_from_linearf(lms.v[0]);
79 let lin_m = pq_from_linearf(lms.v[1]);
80 let lin_s = pq_from_linearf(lms.v[2]);
81 let ictcp = L_LMS_TO_ICTCP.mul_vector(Vector3f {
82 v: [lin_l, lin_m, lin_s],
83 });
84 ICtCp {
85 i: ictcp.v[0],
86 ct: ictcp.v[1],
87 cp: ictcp.v[2],
88 }
89 }
90
91 #[inline]
96 pub fn from_linear_rgb(rgb: Rgb<f32>, matrix: Matrix3f) -> ICtCp {
97 let lms = matrix.mul_vector(rgb.to_vector());
98 let lin_l = pq_from_linearf(lms.v[0]);
99 let lin_m = pq_from_linearf(lms.v[1]);
100 let lin_s = pq_from_linearf(lms.v[2]);
101 let ictcp = L_LMS_TO_ICTCP.mul_vector(Vector3f {
102 v: [lin_l, lin_m, lin_s],
103 });
104 ICtCp {
105 i: ictcp.v[0],
106 ct: ictcp.v[1],
107 cp: ictcp.v[2],
108 }
109 }
110
111 #[inline]
115 pub fn to_linear_rgb(&self, matrix: Matrix3f) -> Rgb<f32> {
116 let l_lms = ICTCP_TO_L_LMS.mul_vector(Vector3f {
117 v: [self.i, self.ct, self.cp],
118 });
119 let gamma_l = pq_to_linearf(l_lms.v[0]);
120 let gamma_m = pq_to_linearf(l_lms.v[1]);
121 let gamma_s = pq_to_linearf(l_lms.v[2]);
122
123 let lms = matrix.mul_vector(Vector3f {
124 v: [gamma_l, gamma_m, gamma_s],
125 });
126 Rgb {
127 r: lms.v[0],
128 g: lms.v[1],
129 b: lms.v[2],
130 }
131 }
132
133 #[inline]
135 pub fn to_xyz(&self) -> Xyz {
136 let l_lms = ICTCP_TO_L_LMS.mul_vector(Vector3f {
137 v: [self.i, self.ct, self.cp],
138 });
139 let gamma_l = pq_to_linearf(l_lms.v[0]);
140 let gamma_m = pq_to_linearf(l_lms.v[1]);
141 let gamma_s = pq_to_linearf(l_lms.v[2]);
142
143 let lms = LMS_TO_XYZ.mul_vector(Vector3f {
144 v: [gamma_l, gamma_m, gamma_s],
145 });
146 Xyz {
147 x: lms.v[0],
148 y: lms.v[1],
149 z: lms.v[2],
150 }
151 }
152
153 #[inline]
155 pub const fn prepare_to_lms(rgb_to_xyz: Matrix3f) -> Matrix3f {
156 XYZ_TO_LMS.mat_mul_const(rgb_to_xyz)
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn check_roundtrip() {
166 let xyz = Xyz::new(0.5, 0.4, 0.3);
167 let ictcp = ICtCp::from_xyz(xyz);
168 let r_xyz = ictcp.to_xyz();
169 assert!((r_xyz.x - xyz.x).abs() < 1e-4);
170 assert!((r_xyz.y - xyz.y).abs() < 1e-4);
171 assert!((r_xyz.z - xyz.z).abs() < 1e-4);
172 }
173
174 #[test]
175 fn check_roundtrip_rgb() {
176 let rgb_to_xyz = Matrix3f {
177 v: [
178 [0.67345345, 0.165661961, 0.125096574],
179 [0.27903071, 0.675341845, 0.045627553],
180 [-0.00193137419, 0.0299795717, 0.797140181],
181 ],
182 };
183 let prepared_matrix = ICtCp::prepare_to_lms(rgb_to_xyz);
184 let inversed_matrix = prepared_matrix.inverse();
185 let rgb = Rgb::new(0.5, 0.4, 0.3);
186 let ictcp = ICtCp::from_linear_rgb(rgb, prepared_matrix);
187 let r_xyz = ictcp.to_linear_rgb(inversed_matrix);
188 assert!((r_xyz.r - rgb.r).abs() < 1e-4);
189 assert!((r_xyz.g - rgb.g).abs() < 1e-4);
190 assert!((r_xyz.b - rgb.b).abs() < 1e-4);
191 }
192}