pub mod common;
pub mod lut_1d;
pub mod lut_1d_processing;
pub mod lut_3d;
pub mod lut_3d_processing;
use crate::error::Result;
use image::{DynamicImage, ImageBuffer};
pub use common::Rgb;
pub use lut_1d::Lut1D;
pub use lut_3d::Lut3D;
pub use lut_1d_processing::{
apply_to_image_buffer_luma, apply_to_image_buffer_luma_a,
apply_to_image_buffer_rgb as apply_1d_to_image_buffer_rgb,
apply_to_image_buffer_rgb_mut as apply_1d_to_image_buffer_rgb_mut,
apply_to_image_buffer_rgba as apply_1d_to_image_buffer_rgba,
apply_to_image_buffer_rgba_mut as apply_1d_to_image_buffer_rgba_mut,
};
pub use lut_3d_processing::{
apply_to_image_buffer_rgb_mut as apply_3d_to_image_buffer_rgb_mut,
apply_to_image_buffer_rgb_unchecked as apply_3d_to_image_buffer_rgb,
apply_to_image_buffer_rgba_mut as apply_3d_to_image_buffer_rgba_mut,
apply_to_image_buffer_rgba_unchecked as apply_3d_to_image_buffer_rgba,
};
pub use lut_3d::Lut3DSoA;
pub fn apply_rgb(
cube_lut: &CubeLut,
image: &ImageBuffer<image::Rgb<u8>, Vec<u8>>,
) -> ImageBuffer<image::Rgb<u8>, Vec<u8>> {
match &cube_lut.lut {
LutData::Lut1D(lut) => lut_1d_processing::apply_to_image_buffer_rgb(cube_lut, image, lut),
LutData::Lut3D(lut) => {
lut_3d_processing::apply_to_image_buffer_rgb_unchecked(cube_lut, image, lut)
}
}
}
#[inline]
pub fn apply_rgb_mut(cube_lut: &CubeLut, image: &mut ImageBuffer<image::Rgb<u8>, Vec<u8>>) {
match &cube_lut.lut {
LutData::Lut1D(lut) => {
lut_1d_processing::apply_to_image_buffer_rgb_mut(cube_lut, image, lut)
}
LutData::Lut3D(lut) => {
lut_3d_processing::apply_to_image_buffer_rgb_mut(cube_lut, image, lut)
}
}
}
pub fn apply_rgba(
cube_lut: &CubeLut,
image: &ImageBuffer<image::Rgba<u8>, Vec<u8>>,
) -> ImageBuffer<image::Rgba<u8>, Vec<u8>> {
match &cube_lut.lut {
LutData::Lut1D(lut) => lut_1d_processing::apply_to_image_buffer_rgba(cube_lut, image, lut),
LutData::Lut3D(lut) => {
lut_3d_processing::apply_to_image_buffer_rgba_unchecked(cube_lut, image, lut)
}
}
}
#[inline]
pub fn apply_rgba_mut(cube_lut: &CubeLut, image: &mut ImageBuffer<image::Rgba<u8>, Vec<u8>>) {
match &cube_lut.lut {
LutData::Lut1D(lut) => {
lut_1d_processing::apply_to_image_buffer_rgba_mut(cube_lut, image, lut)
}
LutData::Lut3D(lut) => {
lut_3d_processing::apply_to_image_buffer_rgba_mut(cube_lut, image, lut)
}
}
}
#[cfg(test)]
pub use lut_1d_processing::{__normal_test_apply_1d_lut, __normal_test_interpolate_1d};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LutType {
Lut1DFixed,
Lut1DOther,
Lut3DFixed,
Lut3DOther,
}
impl std::fmt::Display for LutType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LutType::Lut1DFixed => write!(f, "1D LUT (Fixed)"),
LutType::Lut1DOther => write!(f, "1D LUT (Other)"),
LutType::Lut3DFixed => write!(f, "3D LUT (Fixed)"),
LutType::Lut3DOther => write!(f, "3D LUT (Other)"),
}
}
}
#[derive(Debug, Clone)]
pub enum LutData {
Lut1D(Lut1D),
Lut3D(Lut3D),
}
impl LutData {
pub fn get_type(&self) -> LutType {
match self {
LutData::Lut1D(lut_1d) => {
match lut_1d {
lut_1d::Lut1D::Bit10 { .. } => LutType::Lut1DFixed,
lut_1d::Lut1D::Bit12 { .. } => LutType::Lut1DFixed,
lut_1d::Lut1D::Bit14 { .. } => LutType::Lut1DFixed,
lut_1d::Lut1D::Bit16 { .. } => LutType::Lut1DFixed,
lut_1d::Lut1D::Other { .. } => LutType::Lut1DOther,
}
}
LutData::Lut3D(_lut_3d) => {
LutType::Lut3DFixed
}
}
}
pub fn is_1d(&self) -> bool {
matches!(self, LutData::Lut1D(_))
}
pub fn is_3d(&self) -> bool {
matches!(self, LutData::Lut3D(_))
}
pub fn as_1d(&self) -> Option<&Lut1D> {
match self {
LutData::Lut1D(lut) => Some(lut),
LutData::Lut3D(_) => None,
}
}
pub fn as_1d_mut(&mut self) -> Option<&mut Lut1D> {
match self {
LutData::Lut1D(lut) => Some(lut),
LutData::Lut3D(_) => None,
}
}
pub fn as_3d(&self) -> Option<&Lut3D> {
match self {
LutData::Lut1D(_) => None,
LutData::Lut3D(lut) => Some(lut),
}
}
pub fn as_3d_mut(&mut self) -> Option<&mut Lut3D> {
match self {
LutData::Lut1D(_) => None,
LutData::Lut3D(lut) => Some(lut),
}
}
}
#[derive(Debug, Clone)]
pub struct CubeLut {
pub title: Option<String>,
pub domain_min: Rgb,
pub domain_max: Rgb,
pub lut: LutData,
}
impl CubeLut {
pub fn with_1d(lut_1d: Lut1D) -> Self {
CubeLut {
title: None,
domain_min: [0.0, 0.0, 0.0],
domain_max: [1.0, 1.0, 1.0],
lut: LutData::Lut1D(lut_1d),
}
}
pub fn with_3d(lut_3d: Lut3D) -> Self {
CubeLut {
title: None,
domain_min: [0.0, 0.0, 0.0],
domain_max: [1.0, 1.0, 1.0],
lut: LutData::Lut3D(lut_3d),
}
}
pub fn validate(&self) -> crate::error::Result<()> {
for i in 0..3 {
if self.domain_min[i] >= self.domain_max[i] {
return Err(crate::error::CubeError::DomainBoundsReversed);
}
}
Ok(())
}
pub fn get_lut_type(&self) -> LutType {
self.lut.get_type()
}
pub fn is_1d(&self) -> bool {
self.lut.is_1d()
}
pub fn is_3d(&self) -> bool {
self.lut.is_3d()
}
pub fn lut_1d(&self) -> Option<&Lut1D> {
self.lut.as_1d()
}
pub fn lut_3d(&self) -> Option<&Lut3D> {
self.lut.as_3d()
}
pub fn apply_to_color(&self, color: Rgb) -> Result<Rgb> {
let normalized_r = self.normalize(color[0], 0);
let normalized_g = self.normalize(color[1], 1);
let normalized_b = self.normalize(color[2], 2);
let lut_type = self.get_lut_type();
let output = match lut_type {
LutType::Lut1DFixed | LutType::Lut1DOther => {
if let Some(lut_1d) = self.lut_1d() {
lut_1d_processing::apply_1d_lut_simd(
lut_1d,
normalized_r,
normalized_g,
normalized_b,
)
} else {
return Err(crate::error::CubeError::InvalidFormat(
"1D LUT not available".to_string(),
));
}
}
LutType::Lut3DFixed | LutType::Lut3DOther => {
if let Some(lut_3d) = self.lut_3d() {
let (r_ptr, g_ptr, b_ptr) = unsafe { lut_3d.channel_pointers() };
let size = lut_3d.size();
lut_3d_processing::apply_3d_lut_soa(
r_ptr,
g_ptr,
b_ptr,
normalized_r,
normalized_g,
normalized_b,
size,
(size - 1) as f32,
size * size,
)
} else {
return Err(crate::error::CubeError::InvalidFormat(
"3D LUT not available".to_string(),
));
}
}
};
Ok(output)
}
pub fn apply_to_image(&self, image: &DynamicImage) -> Result<DynamicImage> {
match &self.lut {
LutData::Lut1D(lut) => lut_1d_processing::apply_to_image_1d(self, image, lut),
LutData::Lut3D(lut) => lut_3d_processing::apply_to_image_3d(self, image, lut),
}
}
#[inline]
fn normalize(&self, value: f32, channel: usize) -> f32 {
let min = self.domain_min[channel];
let max = self.domain_max[channel];
(value - min) / (max - min)
}
}
impl Default for CubeLut {
fn default() -> Self {
let lut_1d = Lut1D::new(256).unwrap_or_else(|_| Lut1D::new(2).unwrap());
CubeLut::with_1d(lut_1d)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simd_vs_scalar_1d_lut() {
let mut lut_1d = Lut1D::new(256).unwrap();
for i in 0..256 {
let value = i as f32 / 255.0;
lut_1d.set_rgb(i, [value, value, value]).unwrap();
}
let cube_lut = CubeLut::with_1d(lut_1d);
let test_colors = [
[0.0, 0.0, 0.0],
[0.5, 0.5, 0.5],
[1.0, 1.0, 1.0],
[0.25, 0.75, 0.5],
[0.1, 0.9, 0.3],
[0.33, 0.66, 0.99],
];
for color in test_colors {
let normalized_r = cube_lut.normalize(color[0], 0);
let normalized_g = cube_lut.normalize(color[1], 1);
let normalized_b = cube_lut.normalize(color[2], 2);
let simd_result = lut_1d_processing::apply_1d_lut_simd(
cube_lut.lut_1d().unwrap(),
normalized_r,
normalized_g,
normalized_b,
);
let scalar_result = __normal_test_apply_1d_lut(
cube_lut.lut_1d().unwrap(),
normalized_r,
normalized_g,
normalized_b,
);
for c in 0..3 {
let diff = (simd_result[c] - scalar_result[c]).abs();
assert!(
diff < f32::EPSILON,
"SIMD and scalar results differ at channel {}: SIMD={:.10}, Scalar={:.10}, Diff={:.10}",
c, simd_result[c], scalar_result[c], diff
);
}
}
}
#[test]
fn test_simd_vs_scalar_3d_lut() {
let mut lut_3d = Lut3D::new(17).unwrap();
for r in 0..17 {
for g in 0..17 {
for b in 0..17 {
let v_r = r as f32 / 16.0;
let v_g = g as f32 / 16.0;
let v_b = b as f32 / 16.0;
lut_3d.set_rgb(r, g, b, [v_r, v_g, v_b]).unwrap();
}
}
}
let cube_lut = CubeLut::with_3d(lut_3d);
let test_colors = [
[0.0, 0.0, 0.0],
[0.5, 0.5, 0.5],
[1.0, 1.0, 1.0],
[0.25, 0.75, 0.5],
[0.1, 0.9, 0.3],
[0.33, 0.66, 0.99],
];
for color in test_colors {
let result = cube_lut.apply_to_color(color).unwrap();
#[allow(clippy::needless_range_loop)]
for c in 0..3 {
assert!(
result[c] >= 0.0 && result[c] <= 1.0,
"3D LUT result out of range at channel {}: Result={:.10}",
c,
result[c]
);
}
}
}
}