use oximedia_lut::{Lut3d, LutInterpolation};
pub struct TetrahedralInterpolator;
impl TetrahedralInterpolator {
#[must_use]
pub fn interpolate(lut: &Lut3d, r: f32, g: f32, b: f32) -> [f32; 3] {
let rgb = [
f64::from(r.clamp(0.0, 1.0)),
f64::from(g.clamp(0.0, 1.0)),
f64::from(b.clamp(0.0, 1.0)),
];
let out = lut.apply(&rgb, LutInterpolation::Tetrahedral);
[out[0] as f32, out[1] as f32, out[2] as f32]
}
#[must_use]
pub fn interpolate_f64(lut: &Lut3d, r: f64, g: f64, b: f64) -> [f64; 3] {
let rgb = [r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)];
lut.apply(&rgb, LutInterpolation::Tetrahedral)
}
#[must_use]
pub fn interpolate_trilinear(lut: &Lut3d, r: f32, g: f32, b: f32) -> [f32; 3] {
let rgb = [
f64::from(r.clamp(0.0, 1.0)),
f64::from(g.clamp(0.0, 1.0)),
f64::from(b.clamp(0.0, 1.0)),
];
let out = lut.apply(&rgb, LutInterpolation::Trilinear);
[out[0] as f32, out[1] as f32, out[2] as f32]
}
pub fn apply_batch(lut: &Lut3d, pixels: &mut [[f32; 3]]) {
for px in pixels.iter_mut() {
*px = Self::interpolate(lut, px[0], px[1], px[2]);
}
}
#[must_use]
pub fn max_error_vs_trilinear(lut: &Lut3d, points: &[(f32, f32, f32)]) -> f32 {
let mut max_err = 0.0_f32;
for &(r, g, b) in points {
let tet = Self::interpolate(lut, r, g, b);
let tri = Self::interpolate_trilinear(lut, r, g, b);
for ch in 0..3 {
let err = (tet[ch] - tri[ch]).abs();
if err > max_err {
max_err = err;
}
}
}
max_err
}
}
#[cfg(test)]
mod tests {
use super::*;
use oximedia_lut::{Lut3d, LutSize};
fn approx_f32(a: f32, b: f32, tol: f32) -> bool {
(a - b).abs() < tol
}
fn rgb_approx_f32(a: &[f32; 3], b: &[f32; 3], tol: f32) -> bool {
approx_f32(a[0], b[0], tol) && approx_f32(a[1], b[1], tol) && approx_f32(a[2], b[2], tol)
}
#[test]
fn test_identity_lut_tetrahedral_midpoint() {
let lut = Lut3d::identity(LutSize::Size33);
let out = TetrahedralInterpolator::interpolate(&lut, 0.5, 0.3, 0.7);
assert!(
rgb_approx_f32(&out, &[0.5, 0.3, 0.7], 1e-5),
"Identity LUT should preserve colour: {out:?}"
);
}
#[test]
fn test_identity_lut_corners() {
let lut = Lut3d::identity(LutSize::Size17);
let corners: &[[f32; 3]] = &[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
];
for c in corners {
let out = TetrahedralInterpolator::interpolate(&lut, c[0], c[1], c[2]);
assert!(
rgb_approx_f32(&out, c, 1e-5),
"Corner {c:?} should be preserved: {out:?}"
);
}
}
#[test]
fn test_clamp_below_zero() {
let lut = Lut3d::identity(LutSize::Size17);
let out = TetrahedralInterpolator::interpolate(&lut, -0.5, 0.5, 0.5);
assert!(
out[0] >= 0.0 && out[0] <= 1.0,
"Clamped R channel out of range: {}",
out[0]
);
}
#[test]
fn test_clamp_above_one() {
let lut = Lut3d::identity(LutSize::Size17);
let out = TetrahedralInterpolator::interpolate(&lut, 1.5, 0.5, 0.5);
assert!(
out[0] <= 1.0 + 1e-5,
"Clamped R channel should be ≤ 1.0: {}",
out[0]
);
}
#[test]
fn test_tetrahedral_vs_trilinear_identity_lut() {
let lut = Lut3d::identity(LutSize::Size33);
let test_points: &[(f32, f32, f32)] = &[
(0.1, 0.2, 0.3),
(0.7, 0.5, 0.9),
(0.33, 0.66, 0.12),
(0.5, 0.5, 0.5),
(0.0, 1.0, 0.5),
];
for &(r, g, b) in test_points {
let tet = TetrahedralInterpolator::interpolate(&lut, r, g, b);
let tri = TetrahedralInterpolator::interpolate_trilinear(&lut, r, g, b);
assert!(
rgb_approx_f32(&tet, &tri, 1e-5),
"tet={tet:?} vs tri={tri:?} for ({r},{g},{b})"
);
}
}
#[test]
fn test_tetrahedral_vs_trilinear_scale_lut() {
let lut = Lut3d::from_fn(LutSize::Size17, |rgb| {
[(rgb[0] * 2.0).min(1.0), rgb[1] * 0.5, rgb[2]]
});
let out_tet = TetrahedralInterpolator::interpolate(&lut, 0.5, 0.5, 0.5);
let out_tri = TetrahedralInterpolator::interpolate_trilinear(&lut, 0.5, 0.5, 0.5);
assert!(
(out_tet[0] - out_tri[0]).abs() < 0.05,
"R: tet={} tri={}",
out_tet[0],
out_tri[0]
);
}
#[test]
fn test_max_error_identity_lut() {
let lut = Lut3d::identity(LutSize::Size33);
let points: Vec<(f32, f32, f32)> = vec![(0.1, 0.2, 0.3), (0.5, 0.5, 0.5), (0.9, 0.8, 0.7)];
let err = TetrahedralInterpolator::max_error_vs_trilinear(&lut, &points);
assert!(
err < 1e-5,
"Max error on identity LUT should be tiny: {err}"
);
}
#[test]
fn test_apply_batch_consistent_with_single() {
let lut = Lut3d::identity(LutSize::Size17);
let pixel = [0.3_f32, 0.5, 0.2];
let expected = TetrahedralInterpolator::interpolate(&lut, pixel[0], pixel[1], pixel[2]);
let mut batch = vec![pixel; 5];
TetrahedralInterpolator::apply_batch(&lut, &mut batch);
for out in &batch {
assert!(
rgb_approx_f32(out, &expected, 1e-6),
"Batch output {out:?} != single {expected:?}"
);
}
}
#[test]
fn test_interpolate_f64_identity() {
let lut = Lut3d::identity(LutSize::Size33);
let out = TetrahedralInterpolator::interpolate_f64(&lut, 0.5, 0.3, 0.7);
assert!((out[0] - 0.5).abs() < 1e-9, "R: {}", out[0]);
assert!((out[1] - 0.3).abs() < 1e-9, "G: {}", out[1]);
assert!((out[2] - 0.7).abs() < 1e-9, "B: {}", out[2]);
}
}