Skip to content

Commit

Permalink
Refactor around function graph
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Beinsezii committed Dec 14, 2023
1 parent 5f8b35e commit 21a79eb
Showing 3 changed files with 133 additions and 82 deletions.
5 changes: 1 addition & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
47 changes: 37 additions & 10 deletions benches/conversions.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<[f32; 3]>>().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::<Vec<[f32; 3]>>().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::<Vec<[f32; 3]>>().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);
163 changes: 95 additions & 68 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Ordering> {
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<extern "C" fn(&mut [f32; 3])> {
let mut result: Vec<extern "C" fn(&mut [f32; 3])> = 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]) {
/// <https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIEXYZ_to_CIELAB>
#[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))

0 comments on commit 21a79eb

Please sign in to comment.