#![allow(clippy::needless_range_loop)]
use std::error::Error;
use std::fmt;
pub(crate) mod cie1931;
pub mod gamut;
mod lower_upper;
use super::RGB2Spec;
use cie1931 as CIE;
use gamut::*;
const CIE_FINE_SAMPLES: usize = (CIE::SAMPLES - 1) * 3 + 1;
const EPSILON: f64 = 1e-4;
pub fn optimize(gamut: Gamut, resolution: usize) -> Result<RGB2Spec, MathError> {
let tables = init_tables(gamut);
let scale = (0..resolution)
.map(|k| smoothstep(smoothstep(k as f64 / (resolution - 1) as f64)) as f32)
.collect::<Vec<_>>();
let buffer_size = super::N_COEFFS * 3 * resolution * resolution * resolution;
let mut data = vec![0.0; buffer_size];
for l in 0..3 {
for j in 0..resolution {
let y = j as f64 / (resolution - 1) as f64;
for i in 0..resolution {
let x = i as f64 / (resolution - 1) as f64;
let mut rgb = [0.0; 3];
let start = resolution / 5;
let mut iterate = |start, end| {
let mut coeffs = [0.0; 3];
let increment = if start > end { -1 } else { 1 };
let mut k = start;
while k != end {
let b = scale[k as usize] as f64;
rgb[l] = b;
rgb[(l + 1) % 3] = x * b;
rgb[(l + 2) % 3] = y * b;
coeffs = gauss_newton(rgb, coeffs, &tables)?;
let c0 = CIE::LAMBDA_MIN;
let c1 = 1.0 / CIE::LAMBDA_RANGE;
let a = coeffs[0];
let b = coeffs[1];
let c = coeffs[2];
let idx = ((l * resolution + k as usize) * resolution + j) * resolution + i;
data[3 * idx] = (a * (sqr(c1))) as f32;
data[3 * idx + 1] = (b * c1 - 2.0 * a * c0 * (sqr(c1))) as f32;
data[3 * idx + 2] = (c - b * c0 * c1 + a * (sqr(c0 * c1))) as f32;
k += increment;
}
Ok(())
};
iterate(start as i64, resolution as i64)?;
iterate(start as i64, -1)?;
}
}
}
Ok(RGB2Spec::new(resolution as u32, scale, data))
}
#[derive(Debug)]
pub struct MathError {
source: lower_upper::DegenerateMatrixError,
rgb: [f64; 3],
coeffs: [f64; 3],
}
impl fmt::Display for MathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"LU decomposition failed! rgb: {:?}, coeffs: {:?}",
self.rgb, self.coeffs
)
}
}
impl Error for MathError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
fn sigmoid(x: f64) -> f64 {
0.5 * x / (1.0 + x * x).sqrt() + 0.5
}
fn smoothstep(x: f64) -> f64 {
x * x * (3.0 - 2.0 * x)
}
fn sqr(x: f64) -> f64 {
x * x
}
fn cie_lab(p: [f64; 3], data: &Data) -> [f64; 3] {
let mut x = 0.0;
let mut y = 0.0;
let mut z = 0.0;
let xw = data.xyz_whitepoint[0];
let yw = data.xyz_whitepoint[1];
let zw = data.xyz_whitepoint[2];
for j in 0..3 {
x += p[j] * data.rgb_to_xyz[0][j];
y += p[j] * data.rgb_to_xyz[1][j];
z += p[j] * data.rgb_to_xyz[2][j];
}
let f = |t: f64| {
let delta: f64 = 6.0 / 29.0;
if t > delta * delta * delta {
t.cbrt()
} else {
t / (delta * delta * 3.0) + (4.0 / 29.0)
}
};
[
116.0 * f(y / yw) - 16.0,
500.0 * (f(x / xw) - f(y / yw)),
200.0 * (f(y / yw) - f(z / zw)),
]
}
struct Data {
rgb_to_xyz: [[f64; 3]; 3],
lambda_tbl: [f64; CIE_FINE_SAMPLES],
rgb_tbl: [[f64; CIE_FINE_SAMPLES]; 3],
xyz_whitepoint: [f64; 3],
}
fn init_tables(gamut: Gamut) -> Data {
let gamut_data = gamut.get_data();
let mut lambda_tbl = [0.0; CIE_FINE_SAMPLES];
let mut rgb_tbl = [[0.0; CIE_FINE_SAMPLES]; 3];
let mut xyz_whitepoint = [0.0; 3];
let h = CIE::LAMBDA_RANGE / (CIE_FINE_SAMPLES - 1) as f64;
for i in 0..CIE_FINE_SAMPLES {
let lambda = CIE::LAMBDA_MIN + i as f64 * h;
let xyz = [
CIE::cie_interp(CIE::OBSERVER_X, lambda),
CIE::cie_interp(CIE::OBSERVER_Y, lambda),
CIE::cie_interp(CIE::OBSERVER_Z, lambda),
];
let illuminance = CIE::cie_interp(gamut_data.illuminant, lambda);
let mut weight = 3.0 / 8.0 * h;
weight *= if i == 0 || i == CIE_FINE_SAMPLES - 1 {
1.0
} else if (i - 1) % 3 == 2 {
2.0
} else {
3.0
};
lambda_tbl[i] = lambda;
for k in 0..3 {
for j in 0..3 {
rgb_tbl[k][i] += gamut_data.xyz_to_rgb[k][j] * xyz[j] * illuminance * weight;
}
}
for i in 0..3 {
xyz_whitepoint[i] += xyz[i] * illuminance * weight;
}
}
Data {
rgb_to_xyz: gamut_data.rgb_to_xyz,
lambda_tbl,
rgb_tbl,
xyz_whitepoint,
}
}
fn eval_residual(coeffs: [f64; 3], rgb: [f64; 3], data: &Data) -> [f64; 3] {
let mut out = [0.0; 3];
for i in 0..CIE_FINE_SAMPLES {
let lambda = (data.lambda_tbl[i] - CIE::LAMBDA_MIN) / CIE::LAMBDA_RANGE;
let mut x = 0.0;
for c in coeffs {
x = x * lambda + c;
}
for j in 0..3 {
out[j] += data.rgb_tbl[j][i] * sigmoid(x);
}
}
let out = cie_lab(out, data);
let residual = cie_lab(rgb, data);
[
residual[0] - out[0],
residual[1] - out[1],
residual[2] - out[2],
]
}
fn eval_jacobian(coeffs: [f64; 3], rgb: [f64; 3], data: &Data) -> [[f64; 3]; 3] {
let mut jac = [[0.0; 3]; 3];
for i in 0..3 {
let mut tmp = coeffs;
tmp[i] -= EPSILON;
let r0 = eval_residual(tmp, rgb, data);
let mut tmp = coeffs;
tmp[i] += EPSILON;
let r1 = eval_residual(tmp, rgb, data);
for j in 0..3 {
jac[j][i] = (r1[j] - r0[j]) * (1.0 / (2.0 * EPSILON));
}
}
jac
}
fn gauss_newton(rgb: [f64; 3], coeffs: [f64; 3], data: &Data) -> Result<[f64; 3], MathError> {
let mut r;
let mut coeffs = coeffs;
let it = 15;
for _ in 0..it {
let residual = eval_residual(coeffs, rgb, data);
let jacobian = eval_jacobian(coeffs, rgb, data);
let (jacobian, permutation) = match lower_upper::decompose(&jacobian, 1e-15) {
Ok(t) => t,
Err(source) => {
return Err(MathError {
rgb,
coeffs,
source,
});
}
};
let x = lower_upper::solve(jacobian, permutation, residual);
r = 0.0;
for j in 0..3 {
coeffs[j] -= x[j];
r += residual[j] * residual[j];
}
let max = coeffs[0].max(coeffs[1]).max(coeffs[2]);
if max > 200.0 {
for c in &mut coeffs {
*c *= 200.0 / max;
}
}
if r < 1e-6 {
break;
}
}
Ok(coeffs)
}