From 21a79eb21ddda0760a4ea91fa03bcc81a9aa7507 Mon Sep 17 00:00:00 2001 From: Beinsezii Date: Wed, 13 Dec 2023 17:55:33 -0800 Subject: [PATCH] Refactor around function graph New fns convert_space_sliced and convert_space_chunked for operating efficiently on large pixel buffers without many function lookups. This is also necessary for more sane implementations of further colorspaces. Unfortunately this has the side effect that the per-pixel `convert_space` fn is much slower as it needs to compute the graph for each pixel. AFAIK the amount of situations where speed is paramount and space isn't converted across the entire buffer at once is mostly just my own Pixelbuster which is probably adjustable to this change. --- Cargo.toml | 5 +- benches/conversions.rs | 47 +++++++++--- src/lib.rs | 163 ++++++++++++++++++++++++----------------- 3 files changed, 133 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8d2a366..6c35cab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "colcon" -version = "0.3.0" +version = "0.4.0" edition = "2021" license = "MIT" description = "Simple colorspace conversions in Rust." @@ -32,9 +32,6 @@ harness = false [lib] crate-type = ["lib", "cdylib"] -[features] -D50 = [] - [profile.release] lto = true opt-level = 3 diff --git a/benches/conversions.rs b/benches/conversions.rs index aa10f14..c4abc84 100644 --- a/benches/conversions.rs +++ b/benches/conversions.rs @@ -17,43 +17,43 @@ pub fn conversions(c: &mut Criterion) { let pixels = pixels(); c.bench_function("srgb_to_lrgb", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::SRGB, Space::LRGB, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::srgb_to_lrgb(pixel.try_into().unwrap()))); } )); c.bench_function("lrgb_to_xyz", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::LRGB, Space::XYZ, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::lrgb_to_xyz(pixel.try_into().unwrap()))); } )); c.bench_function("xyz_to_lab", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::XYZ, Space::LAB, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::xyz_to_lab(pixel.try_into().unwrap()))); } )); c.bench_function("lab_to_lch", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::LAB, Space::LCH, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::lab_to_lch(pixel.try_into().unwrap()))); } )); c.bench_function("lch_to_lab", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::LCH, Space::LAB, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::lch_to_lab(pixel.try_into().unwrap()))); } )); c.bench_function("lab_to_xyz", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::LAB, Space::XYZ, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::lab_to_xyz(pixel.try_into().unwrap()))); } )); c.bench_function("xyz_to_lrgb", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::XYZ, Space::LRGB, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::xyz_to_lrgb(pixel.try_into().unwrap()))); } )); c.bench_function("lrgb_to_srgb", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::LRGB, Space::SRGB, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::lrgb_to_srgb(pixel.try_into().unwrap()))); } )); c.bench_function("srgb_to_hsv", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::SRGB, Space::HSV, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::srgb_to_hsv(pixel.try_into().unwrap()))); } )); c.bench_function("hsv_to_srgb", |b| b.iter(|| { - black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::HSV, Space::SRGB, pixel.try_into().unwrap()))); + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| colcon::hsv_to_srgb(pixel.try_into().unwrap()))); } )); c.bench_function("expand_gamma", |b| b.iter(|| { @@ -72,6 +72,33 @@ pub fn conversions(c: &mut Criterion) { black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::LCH, Space::SRGB, pixel.try_into().unwrap()))); } )); + c.bench_function("full_to_chunk", |b| b.iter(|| { + black_box(colcon::convert_space_chunked(Space::LCH, Space::SRGB, pixels.chunks_exact(3).map(|chunk| chunk.try_into().unwrap()).collect::>().as_mut_slice())); + } )); + + c.bench_function("full_from_chunk", |b| b.iter(|| { + black_box(colcon::convert_space_chunked(Space::LCH, Space::SRGB, &mut pixels.chunks_exact(3).map(|chunk| chunk.try_into().unwrap()).collect::>().as_mut_slice())); + } )); + + c.bench_function("full_to_slice", |b| b.iter(|| { + black_box(colcon::convert_space_sliced(Space::LCH, Space::SRGB, &mut pixels.clone())); + } )); + + c.bench_function("full_from_slice", |b| b.iter(|| { + black_box(colcon::convert_space_sliced(Space::LCH, Space::SRGB, &mut pixels.clone())); + } )); + + c.bench_function("single", |b| b.iter(|| { + black_box(pixels.clone().chunks_exact_mut(3).for_each(|pixel| convert_space(Space::LRGB, Space::XYZ, pixel.try_into().unwrap()))); + } )); + + c.bench_function("single_chunk", |b| b.iter(|| { + black_box(colcon::convert_space_chunked(Space::LRGB, Space::XYZ, pixels.chunks_exact(3).map(|chunk| chunk.try_into().unwrap()).collect::>().as_mut_slice())); + } )); + + c.bench_function("single_slice", |b| b.iter(|| { + black_box(colcon::convert_space_sliced(Space::LRGB, Space::XYZ, &mut pixels.clone())); + } )); } criterion_group!(benches, conversions); diff --git a/src/lib.rs b/src/lib.rs index 9221f8a..6925349 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,31 +14,17 @@ //! //! This crate references Standard Illuminant D65 //! when converting to/from the CIE colorspace. -//! The feature flag `D50` changes this to Illuminant D50, -//! used by BABL (GIMP) and possibly other programs. -const LAB_DELTA: f32 = 6.0 / 29.0; +use std::cmp::Ordering; -const D65_X: f32 = 0.950489; -const D65_Y: f32 = 1.000000; -const D65_Z: f32 = 1.088840; +const LAB_DELTA: f32 = 6.0 / 29.0; /// 'Standard' Illuminant D65. -pub const D65: [f32; 3] = [D65_X, D65_Y, D65_Z]; - -const D50_X: f32 = 0.964212; -const D50_Y: f32 = 1.000000; -const D50_Z: f32 = 0.825188; +pub const D65: [f32; 3] = [0.950489, 1.000000, 1.088840]; /// Illuminant D50, aka "printing" illuminant. /// Used by BABL/GIMP + others over D65, not sure why. -pub const D50: [f32; 3] = [D50_X, D50_Y, D50_Z]; - -#[cfg(feature = "D50")] -const ILLUMINANT: [f32; 3] = D50; - -#[cfg(not(feature = "D50"))] -const ILLUMINANT: [f32; 3] = D65; +pub const D50: [f32; 3] = [0.964212, 1.000000, 0.825188]; /// Expand gamma of a single value to linear light #[inline] @@ -122,12 +108,12 @@ pub enum Space { impl ToString for Space { fn to_string(&self) -> String { match self { - Space::SRGB => String::from("rgba"), - Space::HSV => String::from("hsva"), - Space::LRGB => String::from("rgba"), - Space::XYZ => String::from("xyza"), - Space::LAB => String::from("laba"), - Space::LCH => String::from("lcha"), + Space::SRGB => String::from("rgb"), + Space::HSV => String::from("hsv"), + Space::LRGB => String::from("rgb"), + Space::XYZ => String::from("xyz"), + Space::LAB => String::from("lab"), + Space::LCH => String::from("lch"), } } } @@ -147,51 +133,76 @@ impl TryFrom<&str> for Space { } } -#[rustfmt::skip] +impl PartialOrd for Space { + fn partial_cmp(&self, other: &Self) -> Option { + if self == other { return Some(Ordering::Equal) } + Some(match self { + // Base + Space::SRGB => match other {_ => Ordering::Less}, + + // Endcaps + Space::HSV => match other {_ => Ordering::Greater}, + Space::LCH => match other {_ => Ordering::Greater}, + + // Intermittents + Space::LRGB => match other {Space::SRGB => Ordering::Greater, _ => Ordering::Less} + Space::XYZ => match other {Space::SRGB | Space::LRGB => Ordering::Greater, _ => Ordering::Less} + Space::LAB => match other {Space::SRGB | Space::LRGB | Space::XYZ => Ordering::Greater, _ => Ordering::Less} + }) + } +} + + +fn graph(mut from: Space, to: Space) -> Vec { + let mut result: Vec = Vec::with_capacity(6 * 2); + loop { + if from > to { + match from { + Space::SRGB => unreachable!(), + Space::HSV => {result.push(hsv_to_srgb); break}, + Space::LRGB => {result.push(lrgb_to_srgb); break}, + Space::XYZ => {result.push(xyz_to_lrgb); from = Space::LRGB}, + Space::LAB => {result.push(lab_to_xyz); from = Space::XYZ}, + Space::LCH => {result.push(lch_to_lab); from = Space::LAB}, + } + } else if from < to { + match from { + // Endcaps + Space::LCH => unreachable!(), + Space::HSV => unreachable!(), + + Space::SRGB => match to { + Space::HSV => {result.push(srgb_to_hsv); break}, + _ => {result.push(srgb_to_lrgb); from = Space::LRGB} + } + Space::LRGB => {result.push(lrgb_to_xyz); from = Space::XYZ}, + Space::XYZ => {result.push(xyz_to_lab); from = Space::LAB}, + Space::LAB => {result.push(lab_to_lch); break}, + } + } else { + break + } + } + result +} + /// Runs conversion functions to convert `pixel` from one `Space` to another /// in the least possible moves. pub fn convert_space(from: Space, to: Space, pixel: &mut [f32; 3]) { - match (from, to) { - // No-op - (Space::SRGB, Space::SRGB) - | (Space::HSV, Space::HSV) - | (Space::LRGB, Space::LRGB) - | (Space::XYZ, Space::XYZ) - | (Space::LAB, Space::LAB) - | (Space::LCH, Space::LCH) => (), - // Up - (Space::SRGB, Space::HSV) => srgb_to_hsv(pixel), - (Space::SRGB, Space::LRGB) => srgb_to_lrgb(pixel), - (Space::SRGB, Space::XYZ) => {srgb_to_lrgb(pixel); lrgb_to_xyz(pixel)}, - (Space::SRGB, Space::LAB) => {srgb_to_lrgb(pixel); lrgb_to_xyz(pixel); xyz_to_lab(pixel)}, - (Space::SRGB, Space::LCH) => {srgb_to_lrgb(pixel); lrgb_to_xyz(pixel); xyz_to_lab(pixel); lab_to_lch(pixel)}, - (Space::LRGB, Space::XYZ) => lrgb_to_xyz(pixel), - (Space::LRGB, Space::LAB) => {lrgb_to_xyz(pixel); xyz_to_lab(pixel)}, - (Space::LRGB, Space::LCH) => {lrgb_to_xyz(pixel); xyz_to_lab(pixel); lab_to_lch(pixel)}, - (Space::XYZ, Space::LAB) => xyz_to_lab(pixel), - (Space::XYZ, Space::LCH) => {xyz_to_lab(pixel); lab_to_lch(pixel)}, - (Space::LAB, Space::LCH) => lab_to_lch(pixel), - (Space::HSV, Space::LRGB) => {hsv_to_srgb(pixel); srgb_to_lrgb(pixel)}, - (Space::HSV, Space::XYZ) => {hsv_to_srgb(pixel); srgb_to_lrgb(pixel); lrgb_to_xyz(pixel)}, - (Space::HSV, Space::LAB) => {hsv_to_srgb(pixel); srgb_to_lrgb(pixel); lrgb_to_xyz(pixel); xyz_to_lab(pixel)}, - (Space::HSV, Space::LCH) => {hsv_to_srgb(pixel); srgb_to_lrgb(pixel); lrgb_to_xyz(pixel); xyz_to_lab(pixel); lab_to_lch(pixel)}, - // Down - (Space::LCH, Space::LAB) => lch_to_lab(pixel), - (Space::LCH, Space::XYZ) => {lch_to_lab(pixel); lab_to_xyz(pixel)}, - (Space::LCH, Space::LRGB) => {lch_to_lab(pixel); lab_to_xyz(pixel); xyz_to_lrgb(pixel)}, - (Space::LCH, Space::SRGB) => {lch_to_lab(pixel); lab_to_xyz(pixel); xyz_to_lrgb(pixel); lrgb_to_srgb(pixel)}, - (Space::LCH, Space::HSV) => {lch_to_lab(pixel); lab_to_xyz(pixel); xyz_to_lrgb(pixel); lrgb_to_srgb(pixel); srgb_to_hsv(pixel)}, - (Space::LAB, Space::XYZ) => lab_to_xyz(pixel), - (Space::LAB, Space::LRGB) => {lab_to_xyz(pixel); xyz_to_lrgb(pixel)}, - (Space::LAB, Space::SRGB) => {lab_to_xyz(pixel); xyz_to_lrgb(pixel); lrgb_to_srgb(pixel)}, - (Space::LAB, Space::HSV) => {lab_to_xyz(pixel); xyz_to_lrgb(pixel); lrgb_to_srgb(pixel); srgb_to_hsv(pixel)}, - (Space::XYZ, Space::SRGB) => {xyz_to_lrgb(pixel); lrgb_to_srgb(pixel)}, - (Space::XYZ, Space::LRGB) => xyz_to_lrgb(pixel), - (Space::XYZ, Space::HSV) => {xyz_to_lrgb(pixel); lrgb_to_srgb(pixel); srgb_to_hsv(pixel)}, - (Space::LRGB, Space::SRGB) => lrgb_to_srgb(pixel), - (Space::LRGB, Space::HSV) => {lrgb_to_srgb(pixel); srgb_to_hsv(pixel)}, - (Space::HSV, Space::SRGB) => hsv_to_srgb(pixel), - } + graph(from, to).into_iter().for_each(|f| f(pixel)) +} + +/// Runs conversion functions to convert `pixel` from one `Space` to another +/// in the least possible moves. Caches conversion graph for faster iteration. +pub fn convert_space_chunked(from: Space, to: Space, pixels: &mut [[f32; 3]]) { + graph(from, to).into_iter().for_each(|f| pixels.iter_mut().for_each(|pixel| f(pixel))) +} + +/// Runs conversion functions to convert `pixel` from one `Space` to another +/// in the least possible moves. Caches conversion graph for faster iteration. +/// Ignores remainder values in slice +pub fn convert_space_sliced(from: Space, to: Space, pixels: &mut [f32]) { + graph(from, to).into_iter().for_each(|f| pixels.chunks_exact_mut(3).for_each(|pixel| f(pixel.try_into().unwrap()))) } /// Same as `convert_space`, ignores the 4th value in `pixel`. @@ -286,7 +297,7 @@ pub extern "C" fn lrgb_to_xyz(pixel: &mut [f32; 3]) { /// #[no_mangle] pub extern "C" fn xyz_to_lab(pixel: &mut [f32; 3]) { - pixel.iter_mut().zip(ILLUMINANT).for_each(|(c, d)| *c /= d); + pixel.iter_mut().zip(D65).for_each(|(c, d)| *c /= d); pixel.iter_mut().for_each(|c| { if *c > LAB_DELTA.powi(3) { @@ -429,7 +440,7 @@ pub extern "C" fn lab_to_xyz(pixel: &mut [f32; 3]) { } }); - pixel.iter_mut().zip(ILLUMINANT).for_each(|(c, d)| *c *= d); + pixel.iter_mut().zip(D65).for_each(|(c, d)| *c *= d); } /// Convert from CIE LCH to CIE LAB. @@ -580,6 +591,22 @@ mod tests { pixcmp(pixel, SRGB) } + #[test] + fn sweep_chunk() { + let mut pixel = [SRGB]; + convert_space_chunked(Space::SRGB, Space::LCH, &mut pixel); + convert_space_chunked(Space::LCH, Space::SRGB, &mut pixel); + pixcmp(pixel[0], SRGB) + } + + #[test] + fn sweep_slice() { + let pixel: &mut [f32] = &mut SRGB.clone(); + convert_space_sliced(Space::SRGB, Space::LCH, pixel); + convert_space_sliced(Space::LCH, Space::SRGB, pixel); + pixcmp(pixel.try_into().unwrap(), SRGB) + } + #[test] fn irgb_to() { assert_eq!(IRGB, srgb_to_irgb(SRGB))