lab-0.11.0/.cargo_vcs_info.json0000644000000001120000000000000116420ustar { "git": { "sha1": "8a73a7cbdd44340f18422c50912676811675a360" } } lab-0.11.0/Cargo.toml0000644000000026620000000000000076540ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] name = "lab" version = "0.11.0" authors = ["Jesse Bees "] include = ["src/**/*", "benches/**/*", "Cargo.toml", "README.md", "LICENSE"] description = "Tools for converting RGB colors to the CIE-L*a*b* color space, and\ncomparing differences in color.\n" homepage = "https://github.com/TooManyBees/lab" documentation = "https://docs.rs/lab" readme = "README.md" keywords = ["lab", "color", "pixel", "rgb"] categories = ["multimedia::images", "multimedia::video"] license = "MIT" repository = "https://github.com/TooManyBees/lab" [lib] bench = false [[bench]] name = "rgb_to_lab" harness = false [[bench]] name = "lab_to_rgb" harness = false [dev-dependencies.approx] version = "0.5" [dev-dependencies.criterion] version = "0.3" default-features = false [dev-dependencies.lazy_static] version = "1.3.0" [dev-dependencies.pretty_assertions] version = "0.7" [dev-dependencies.rand] version = "0.8" lab-0.11.0/Cargo.toml.orig000064400000000000000000000015000000000000000133010ustar 00000000000000[package] name = "lab" version = "0.11.0" authors = ["Jesse Bees "] description = """ Tools for converting RGB colors to the CIE-L*a*b* color space, and comparing differences in color. """ homepage = "https://github.com/TooManyBees/lab" repository = "https://github.com/TooManyBees/lab" documentation = "https://docs.rs/lab" readme = "README.md" keywords = ["lab", "color", "pixel", "rgb"] categories = ["multimedia::images", "multimedia::video"] license = "MIT" include = ["src/**/*", "benches/**/*", "Cargo.toml", "README.md", "LICENSE"] [lib] bench = false [dev-dependencies] rand = "0.8" criterion = { version = "0.3", default-features = false } lazy_static = "1.3.0" pretty_assertions = "0.7" approx = "0.5" [[bench]] name = "rgb_to_lab" harness = false [[bench]] name = "lab_to_rgb" harness = false lab-0.11.0/LICENSE000064400000000000000000000020550000000000000114250ustar 00000000000000MIT License Copyright (c) 2020 🐝🐝🐝 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lab-0.11.0/README.md000064400000000000000000000045440000000000000117040ustar 00000000000000# Lab [![Lab crate](https://img.shields.io/crates/v/lab.svg)](https://crates.io/crates/lab) [![Lab documentation](https://docs.rs/lab/badge.svg)](https://docs.rs/lab) ![minimum rustc 1.31](https://img.shields.io/badge/rustc-1.36+-red.svg) [![TravisCI Build Status](https://travis-ci.com/TooManyBees/lab.svg)](https://travis-ci.com/github/TooManyBees/lab) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/TooManyBees/lab?branch=master&svg=true)](https://ci.appveyor.com/project/TooManyBees/lab) Tools for converting RGB colors to L\*a\*b\* measurements. RGB colors, for this crate at least, are considered to be composed of `u8` values from 0 to 255, while L\*a\*b\* colors are represented by its own struct that uses `f32` values. # Usage ## Converting single values To convert a single value, use one of the functions * `lab::Lab::from_rgb(rgb: &[u8; 3]) -> Lab` * `lab::Lab::from_rgba(rgba: &[u8; 4]) -> Lab` (drops the fourth alpha byte) * `lab::Lab::to_rgb(&self) -> [u8; 3]` ```rust extern crate lab; use lab::Lab; let pink_in_lab = Lab::from_rgb(&[253, 120, 138]); // Lab { l: 66.639084, a: 52.251457, b: 14.860654 } ``` ## Converting multiple values To convert slices of values * `lab::rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec` * `lab::labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]>` * `lab::rgb_bytes_to_labs(bytes: &[u8]) -> Vec` * `lab::labs_to_rgb_bytes(labs: &[Lab]) -> Vec` ```rust extern crate lab; use lab::rgbs_to_labs; let rgbs = vec![ [0xFF, 0x69, 0xB6], [0xE7, 0x00, 0x00], [0xFF, 0x8C, 0x00], [0xFF, 0xEF, 0x00], [0x00, 0x81, 0x1F], [0x00, 0xC1, 0xC1], [0x00, 0x44, 0xFF], [0x76, 0x00, 0x89], ]; let labs = rgbs_to_labs(&rgbs); ``` ```rust extern crate lab; use lab::rgb_bytes_to_labs; let rgbs = vec![ 0xFF, 0x69, 0xB6, 0xE7, 0x00, 0x00, 0xFF, 0x8C, 0x00, 0xFF, 0xEF, 0x00, 0x00, 0x81, 0x1F, 0x00, 0xC1, 0xC1, 0x00, 0x44, 0xFF, 0x76, 0x00, 0x89, ]; let labs = rgb_bytes_to_labs(&rgbs); ``` These functions will use x86_64 AVX2 instructions if compiled to a supported target. ## Minimum Rust version Lab 0.8.0 requires Rust >= 1.36.0 for the [MaybeUninit](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html) struct Lab 0.7.0 requires Rust >= 1.31.0 for the [chunks_exact](https://doc.rust-lang.org/std/primitive.slice.html#method.chunks_exact) slice method lab-0.11.0/benches/lab_to_rgb.rs000064400000000000000000000025750000000000000144760ustar 00000000000000#[macro_use] extern crate criterion; #[macro_use] extern crate lazy_static; extern crate lab; extern crate rand; use criterion::Criterion; use rand::distributions::Standard; use rand::Rng; lazy_static! { static ref LABS: Vec = { let rand_seed = [0u8; 32]; let rng: rand::rngs::StdRng = rand::SeedableRng::from_seed(rand_seed); let labs: Vec<[f32; 8]> = rng.sample_iter(&Standard).take(512).collect(); labs.iter() .map(|lab| lab::Lab { l: lab[0], a: lab[1], b: lab[2], }) .collect() }; } fn labs_to_rgbs(c: &mut Criterion) { c.bench_function("[Lab] -> [RGB]", move |b| { b.iter(|| lab::__scalar::labs_to_rgbs(&LABS)) }); } fn labs_to_rgb_bytes(c: &mut Criterion) { c.bench_function("[Lab] -> [u8]", move |b| { b.iter(|| lab::__scalar::labs_to_rgb_bytes(&LABS)) }); } fn labs_to_rgbs_simd(c: &mut Criterion) { c.bench_function("[Lab] -> [RGB] (simd)", move |b| { b.iter(|| lab::labs_to_rgbs(&LABS)) }); } fn labs_to_rgb_bytes_simd(c: &mut Criterion) { c.bench_function("[Lab] -> [u8] (simd)", move |b| { b.iter(|| lab::labs_to_rgb_bytes(&LABS)) }); } criterion_group!( benches, labs_to_rgbs, labs_to_rgb_bytes, labs_to_rgbs_simd, labs_to_rgb_bytes_simd ); criterion_main!(benches); lab-0.11.0/benches/rgb_to_lab.rs000064400000000000000000000026150000000000000144710ustar 00000000000000#[macro_use] extern crate criterion; #[macro_use] extern crate lazy_static; extern crate lab; extern crate rand; use criterion::Criterion; use rand::distributions::Standard; use rand::Rng; lazy_static! { static ref RGBS: Vec<[u8; 3]> = { let rand_seed = [0u8; 32]; let rng: rand::rngs::StdRng = rand::SeedableRng::from_seed(rand_seed); rng.sample_iter(&Standard).take(512).collect() }; static ref RGBS_FLAT: Vec = RGBS.iter() .fold(Vec::with_capacity(RGBS.len() * 3), |mut acc, rgb| { acc.extend_from_slice(rgb); acc }); } fn rgbs_to_labs(c: &mut Criterion) { c.bench_function("[RGB] -> [Lab]", move |b| { b.iter(|| lab::__scalar::rgbs_to_labs(&RGBS)) }); } fn rgb_bytes_to_labs(c: &mut Criterion) { c.bench_function("[u8] -> [Lab]", move |b| { b.iter(|| lab::__scalar::rgb_bytes_to_labs(&RGBS_FLAT)) }); } fn rgbs_to_labs_simd(c: &mut Criterion) { c.bench_function("[RGB] -> [Lab] (simd)", move |b| { b.iter(|| lab::rgbs_to_labs(&RGBS)) }); } fn rgb_bytes_to_labs_simd(c: &mut Criterion) { c.bench_function("[u8] -> [Lab] (simd)", move |b| { b.iter(|| lab::rgb_bytes_to_labs(&RGBS_FLAT)) }); } criterion_group!( benches, rgbs_to_labs, rgb_bytes_to_labs, rgbs_to_labs_simd, rgb_bytes_to_labs_simd ); criterion_main!(benches); lab-0.11.0/src/approx_impl.rs000064400000000000000000000035140000000000000141100ustar 00000000000000use crate::{LCh, Lab}; use approx::{AbsDiffEq, RelativeEq}; impl AbsDiffEq for Lab { type Epsilon = f32; fn default_epsilon() -> Self::Epsilon { std::f32::EPSILON } fn abs_diff_eq(&self, other: &Lab, epsilon: Self::Epsilon) -> bool { AbsDiffEq::abs_diff_eq(&self.l, &other.l, epsilon) && AbsDiffEq::abs_diff_eq(&self.a, &other.a, epsilon) && AbsDiffEq::abs_diff_eq(&self.b, &other.b, epsilon) } } impl RelativeEq for Lab { fn default_max_relative() -> Self::Epsilon { std::f32::EPSILON } fn relative_eq( &self, other: &Lab, epsilon: Self::Epsilon, max_relative: Self::Epsilon, ) -> bool { RelativeEq::relative_eq(&self.l, &other.l, epsilon, max_relative) && RelativeEq::relative_eq(&self.a, &other.a, epsilon, max_relative) && RelativeEq::relative_eq(&self.b, &other.b, epsilon, max_relative) } } impl AbsDiffEq for LCh { type Epsilon = f32; fn default_epsilon() -> Self::Epsilon { std::f32::EPSILON } fn abs_diff_eq(&self, other: &LCh, epsilon: Self::Epsilon) -> bool { AbsDiffEq::abs_diff_eq(&self.l, &other.l, epsilon) && AbsDiffEq::abs_diff_eq(&self.c, &other.c, epsilon) && AbsDiffEq::abs_diff_eq(&self.h, &other.h, epsilon) } } impl RelativeEq for LCh { fn default_max_relative() -> Self::Epsilon { std::f32::EPSILON } fn relative_eq( &self, other: &LCh, epsilon: Self::Epsilon, max_relative: Self::Epsilon, ) -> bool { RelativeEq::relative_eq(&self.l, &other.l, epsilon, max_relative) && RelativeEq::relative_eq(&self.c, &other.c, epsilon, max_relative) && RelativeEq::relative_eq(&self.h, &other.h, epsilon, max_relative) } } lab-0.11.0/src/lib.rs000064400000000000000000000547360000000000000123400ustar 00000000000000#![doc(html_root_url = "https://docs.rs/lab")] /*! # Lab Tools for converting RGB colors to L\*a\*b\* measurements. RGB colors, for this crate at least, are considered to be composed of `u8` values from 0 to 255, while L\*a\*b\* colors are represented by its own struct that uses `f32` values. # Usage ## Converting single values To convert a single value, use one of the functions * `lab::Lab::from_rgb(rgb: &[u8; 3]) -> Lab` * `lab::Lab::from_rgba(rgba: &[u8; 4]) -> Lab` (drops the fourth alpha byte) * `lab::Lab::to_rgb(&self) -> [u8; 3]` ```rust extern crate lab; use lab::Lab; let pink_in_lab = Lab::from_rgb(&[253, 120, 138]); // Lab { l: 66.639084, a: 52.251457, b: 14.860654 } ``` ## Converting multiple values To convert slices of values * `lab::rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec` * `lab::labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]>` * `lab::rgb_bytes_to_labs(bytes: &[u8]) -> Vec` * `lab::labs_to_rgb_bytes(labs: &[Lab]) -> Vec` ```rust extern crate lab; use lab::rgbs_to_labs; let rgbs = vec![ [0xFF, 0x69, 0xB6], [0xE7, 0x00, 0x00], [0xFF, 0x8C, 0x00], [0xFF, 0xEF, 0x00], [0x00, 0x81, 0x1F], [0x00, 0xC1, 0xC1], [0x00, 0x44, 0xFF], [0x76, 0x00, 0x89], ]; let labs = rgbs_to_labs(&rgbs); ``` ```rust extern crate lab; use lab::rgb_bytes_to_labs; let rgbs = vec![ 0xFF, 0x69, 0xB6, 0xE7, 0x00, 0x00, 0xFF, 0x8C, 0x00, 0xFF, 0xEF, 0x00, 0x00, 0x81, 0x1F, 0x00, 0xC1, 0xC1, 0x00, 0x44, 0xFF, 0x76, 0x00, 0x89, ]; let labs = rgb_bytes_to_labs(&rgbs); ``` These functions will use x86_64 AVX2 instructions if compiled to a supported target. ## Minimum Rust version Lab 0.7.0 requires Rust >= 1.31.0 for the [chunks_exact](https://doc.rust-lang.org/std/primitive.slice.html#method.chunks_exact) slice method */ #[cfg(test)] #[macro_use] extern crate pretty_assertions; #[cfg(test)] extern crate approx; #[cfg(test)] extern crate lazy_static; #[cfg(test)] extern crate rand; #[cfg(test)] mod approx_impl; #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))] mod simd; /// Struct representing a color in CIALab, a.k.a. L\*a\*b\*, color space #[derive(Debug, PartialEq, Copy, Clone, Default)] pub struct Lab { pub l: f32, pub a: f32, pub b: f32, } /// Struct representing a color in cylindrical CIELCh color space #[derive(Debug, PartialEq, Copy, Clone, Default)] pub struct LCh { pub l: f32, pub c: f32, pub h: f32, } // κ and ε parameters used in conversion between XYZ and La*b*. See // http://www.brucelindbloom.com/LContinuity.html for explanation as to why // those are different values than those provided by CIE standard. pub(crate) const KAPPA: f32 = 24389.0 / 27.0; pub(crate) const EPSILON: f32 = 216.0 / 24389.0; pub(crate) const CBRT_EPSILON: f32 = 6.0 / 29.0; // S₀ and E₀ thresholds used in sRGB gamma. The latter is scaled for encoded // value in the range 0..255. pub(crate) const S_0: f32 = 0.003130668442500564; pub(crate) const E_0_255: f32 = 3294.6 * S_0; // Generated by srgb-matrices.py const WHITE_X: f32 = 0.9504492182750991; const WHITE_Z: f32 = 1.0889166484304715; fn rgb_to_lab(r: u8, g: u8, b: u8) -> Lab { xyz_to_lab(rgb_to_xyz(r, g, b)) } fn rgb_to_xyz(r: u8, g: u8, b: u8) -> [f32; 3] { rgb_to_xyz_inner(r as f32, g as f32, b as f32) } fn rgb_to_xyz_normalized(rgb: &[f32; 3]) -> [f32; 3] { rgb_to_xyz_inner(rgb[0] * 255.0, rgb[1] * 255.0, rgb[2] * 255.0) } #[inline(always)] #[cfg(any(target_feature = "fma", test))] fn mul3(a0: f32, a1: f32, a2: f32, b0: f32, b1: f32, b2: f32) -> f32 { a2.mul_add(b2, a1.mul_add(b1, a0 * b0)) } #[inline(always)] #[cfg(not(any(target_feature = "fma", test)))] fn mul3(a0: f32, a1: f32, a2: f32, b0: f32, b1: f32, b2: f32) -> f32 { a0 * b0 + a1 * b1 + a2 * b2 } #[inline] fn rgb_to_xyz_inner(r: f32, g: f32, b: f32) -> [f32; 3] { #[inline] fn rgb_to_xyz_map(c: f32) -> f32 { if c > E_0_255 { const A: f32 = 0.055 * 255.0; const D: f32 = 1.055 * 255.0; ((c + A) / D).powf(2.4) } else { const D: f32 = 12.92 * 255.0; c / D } } let r = rgb_to_xyz_map(r); let g = rgb_to_xyz_map(g); let b = rgb_to_xyz_map(b); // Generated by srgb-matrices.py let x = mul3( r, g, b, 0.4124108464885388, 0.3575845678529519, 0.18045380393360833, ); let y = mul3( r, g, b, 0.21264934272065283, 0.7151691357059038, 0.07218152157344333, ); let z = mul3( r, g, b, 0.019331758429150258, 0.11919485595098397, 0.9503900340503373, ); [x, y, z] } fn xyz_to_lab(xyz: [f32; 3]) -> Lab { #[inline] fn xyz_to_lab_map(c: f32) -> f32 { if c > EPSILON { c.powf(1.0 / 3.0) } else { (KAPPA * c + 16.0) / 116.0 } } // It’s tempting to replace the division with a multiplication by inverse, // however that results in slightly worse test_grey_error benchmark. let x = xyz_to_lab_map(xyz[0] / WHITE_X); let y = xyz_to_lab_map(xyz[1]); let z = xyz_to_lab_map(xyz[2] / WHITE_Z); Lab { l: (116.0 * y) - 16.0, a: 500.0 * (x - y), b: 200.0 * (y - z), } } fn lab_to_xyz(lab: &Lab) -> [f32; 3] { let fy = (lab.l + 16.0) / 116.0; let fx = (lab.a / 500.0) + fy; let fz = fy - (lab.b / 200.0); let xr = if fx > CBRT_EPSILON { fx.powi(3) } else { ((fx * 116.0) - 16.0) / KAPPA }; let yr = if lab.l > EPSILON * KAPPA { fy.powi(3) } else { lab.l / KAPPA }; let zr = if fz > CBRT_EPSILON { fz.powi(3) } else { ((fz * 116.0) - 16.0) / KAPPA }; [xr * WHITE_X, yr, zr * WHITE_Z] } fn xyz_to_rgb(xyz: [f32; 3]) -> [u8; 3] { let rgb = xyz_to_rgb_normalized(xyz); [ (rgb[0] * 255.0).round() as u8, (rgb[1] * 255.0).round() as u8, (rgb[2] * 255.0).round() as u8, ] } fn xyz_to_rgb_normalized(xyz: [f32; 3]) -> [f32; 3] { let x = xyz[0]; let y = xyz[1]; let z = xyz[2]; // Generated by srgb-matrices.py let r = mul3( x, y, z, 3.240812398895283, -1.5373084456298136, -0.4985865229069666, ); let g = mul3( x, y, z, -0.9692430170086407, 1.8759663029085742, 0.04155503085668564, ); let b = mul3( x, y, z, 0.055638398436112804, -0.20400746093241362, 1.0571295702861434, ); #[inline] fn xyz_to_rgb_map(c: f32) -> f32 { (if c > S_0 { 1.055 * c.powf(1.0 / 2.4) - 0.055 } else { 12.92 * c }) .min(1.0) .max(0.0) } [xyz_to_rgb_map(r), xyz_to_rgb_map(g), xyz_to_rgb_map(b)] } /// Convenience function to map a slice of RGB values to Lab values in serial /// /// # Example /// ``` /// # extern crate lab; /// # use lab::{Lab, rgbs_to_labs}; /// let rgbs = &[[255u8, 0, 0], [255, 0, 255], [0, 255, 255]]; /// let labs = lab::rgbs_to_labs(rgbs); /// assert_eq!(labs, vec![ /// Lab { l: 53.238235, a: 80.09231, b: 67.202095 }, /// Lab { l: 60.322693, a: 98.23698, b: -60.827957 }, /// Lab { l: 91.11428, a: -48.08274, b: -14.12958 } /// ]); /// ``` #[inline] pub fn rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec { #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))] let labs = simd::rgbs_to_labs(rgbs); #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))] let labs = __scalar::rgbs_to_labs(rgbs); labs } /// RGB to Lab conversion that operates on a flat `&[u8]` of consecutive RGB triples. /// /// # Example /// ``` /// # extern crate lab; /// # use lab::{Lab, rgb_bytes_to_labs}; /// let rgbs = &[255u8, 0, 0, 255, 0, 255, 0, 255, 255]; /// let labs = lab::rgb_bytes_to_labs(rgbs); /// assert_eq!(labs, vec![ /// Lab { l: 53.238235, a: 80.09231, b: 67.202095 }, /// Lab { l: 60.322693, a: 98.23698, b: -60.827957 }, /// Lab { l: 91.11428, a: -48.08274, b: -14.12958 } /// ]); /// ``` pub fn rgb_bytes_to_labs(bytes: &[u8]) -> Vec { #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))] let labs = simd::rgb_bytes_to_labs(bytes); #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))] let labs = __scalar::rgb_bytes_to_labs(bytes); labs } /// Convenience function to map a slice of Lab values to RGB values in serial /// /// # Example /// ``` /// # extern crate lab; /// # use lab::{Lab, labs_to_rgbs}; /// let labs = &[ /// Lab { l: 91.11321, a: -48.08751, b: -14.131201 }, /// Lab { l: 60.32421, a: 98.23433, b: -60.824894 }, /// Lab { l: 97.13926, a: -21.553724, b: 94.47797 }, /// ]; /// let rgbs = lab::labs_to_rgbs(labs); /// assert_eq!(rgbs, vec![[0u8, 255, 255], [255, 0, 255], [255, 255, 0]]); /// ``` #[inline] pub fn labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]> { #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))] let rgbs = simd::labs_to_rgbs(labs); #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))] let rgbs = __scalar::labs_to_rgbs(labs); rgbs } /// Lab to RGB conversion that returns RGB triples flattened into a `Vec` /// /// # Example /// ``` /// # extern crate lab; /// # use lab::{Lab, labs_to_rgb_bytes}; /// let labs = &[ /// Lab { l: 91.11321, a: -48.08751, b: -14.131201 }, /// Lab { l: 60.32421, a: 98.23433, b: -60.824894 }, /// Lab { l: 97.13926, a: -21.553724, b: 94.47797 }, /// ]; /// let rgb_bytes = lab::labs_to_rgb_bytes(labs); /// assert_eq!(rgb_bytes, vec![0, 255, 255, 255, 0, 255, 255, 255, 0]); /// ``` #[inline] pub fn labs_to_rgb_bytes(labs: &[Lab]) -> Vec { #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))] let bytes = simd::labs_to_rgb_bytes(labs); #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))] let bytes = __scalar::labs_to_rgb_bytes(labs); bytes } #[doc(hidden)] pub mod __scalar { use rgb_to_lab; use Lab; #[inline] pub fn labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]> { labs.iter().map(Lab::to_rgb).collect() } #[inline] pub fn labs_to_rgb_bytes(labs: &[Lab]) -> Vec { labs.iter() .map(Lab::to_rgb) .fold(Vec::with_capacity(labs.len() * 3), |mut acc, rgb| { acc.extend_from_slice(&rgb); acc }) } #[inline] pub fn rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec { rgbs.iter().map(Lab::from_rgb).collect() } #[inline] pub fn rgb_bytes_to_labs(bytes: &[u8]) -> Vec { bytes .chunks_exact(3) .map(|rgb| rgb_to_lab(rgb[0], rgb[1], rgb[2])) .collect() } } impl Lab { /// Constructs a new `Lab` from a three-element array of `u8`s /// /// # Examples /// /// ``` /// let lab = lab::Lab::from_rgb(&[240, 33, 95]); /// assert_eq!(lab::Lab { l: 52.334686, a: 75.55157, b: 19.995684 }, lab); /// ``` pub fn from_rgb(rgb: &[u8; 3]) -> Self { rgb_to_lab(rgb[0], rgb[1], rgb[2]) } #[doc(hidden)] pub fn from_rgb_normalized(rgb: &[f32; 3]) -> Self { xyz_to_lab(rgb_to_xyz_normalized(rgb)) } /// Constructs a new `Lab` from a four-element array of `u8`s /// /// The `Lab` struct does not store alpha channel information, so the last /// `u8` representing alpha is discarded. This convenience method exists /// in order to easily measure colors already stored in an RGBA array. /// /// # Examples /// /// ``` /// let lab = lab::Lab::from_rgba(&[240, 33, 95, 255]); /// assert_eq!(lab::Lab { l: 52.334686, a: 75.55157, b: 19.995684 }, lab); /// ``` pub fn from_rgba(rgba: &[u8; 4]) -> Self { Lab::from_rgb(&[rgba[0], rgba[1], rgba[2]]) } #[doc(hidden)] pub fn from_rgba_normalized(rgba: &[f32; 4]) -> Self { Lab::from_rgb_normalized(&[rgba[0], rgba[1], rgba[2]]) } /// Returns the `Lab`'s color in RGB, in a 3-element array. /// /// # Examples /// /// ``` /// let lab = lab::Lab { l: 52.330193, a: 75.56704, b: 19.989174 }; /// let rgb = lab.to_rgb(); /// assert_eq!([240, 33, 95], rgb); /// ``` pub fn to_rgb(&self) -> [u8; 3] { xyz_to_rgb(lab_to_xyz(self)) } #[doc(hidden)] pub fn to_rgb_normalized(&self) -> [f32; 3] { xyz_to_rgb_normalized(lab_to_xyz(self)) } /// Measures the perceptual distance between the colors of one `Lab` /// and an `other`. /// /// # Examples /// /// ``` /// # use lab::Lab; /// let pink = Lab { l: 66.6348, a: 52.260696, b: 14.850557 }; /// let websafe_pink = Lab { l: 64.2116, a: 62.519463, b: 2.8871894 }; /// assert_eq!(254.23636, pink.squared_distance(&websafe_pink)); /// ``` pub fn squared_distance(&self, other: &Lab) -> f32 { (self.l - other.l).powi(2) + (self.a - other.a).powi(2) + (self.b - other.b).powi(2) } } impl LCh { /// Constructs a new `LCh` from a three-element array of `u8`s /// /// # Examples /// /// ``` /// let lch = lab::LCh::from_rgb(&[240, 33, 95]); /// assert_eq!(lab::LCh { l: 52.334686, c: 78.15284, h: 0.25873056 }, lch); /// ``` pub fn from_rgb(rgb: &[u8; 3]) -> Self { LCh::from_lab(Lab::from_rgb(rgb)) } /// Constructs a new `LCh` from a four-element array of `u8`s /// /// The `LCh` struct does not store alpha channel information, so the last /// `u8` representing alpha is discarded. This convenience method exists /// in order to easily measure colors already stored in an RGBA array. /// /// # Examples /// /// ``` /// let lch = lab::LCh::from_rgba(&[240, 33, 95, 255]); /// assert_eq!(lab::LCh { l: 52.334686, c: 78.15284, h: 0.25873056 }, lch); /// ``` pub fn from_rgba(rgba: &[u8; 4]) -> Self { LCh::from_lab(Lab::from_rgba(rgba)) } /// Constructs a new `LCh` from a `Lab` /// /// # Examples /// /// ``` /// let lab = lab::Lab { l: 52.33686, a: 75.5516, b: 19.998878 }; /// let lch = lab::LCh::from_lab(lab); /// assert_eq!(lab::LCh { l: 52.33686, c: 78.15369, h: 0.25877 }, lch); /// /// let lab = lab::Lab { l: 52.33686, a: 0.0, b: 0.0 }; /// let lch = lab::LCh::from_lab(lab); /// assert_eq!(lab::LCh { l: 52.33686, c: 0.0, h: 0.0 }, lch); /// ``` pub fn from_lab(lab: Lab) -> Self { LCh { l: lab.l, c: lab.a.hypot(lab.b), h: lab.b.atan2(lab.a), } } /// Returns the `LCh`'s color in RGB, in a 3-element array /// /// # Examples /// /// ``` /// let mut lch = lab::LCh { l: 52.33686, c: 78.15369, h: 0.25877 }; /// assert_eq!([240, 33, 95], lch.to_rgb()); /// /// lch.h += std::f32::consts::TAU; /// assert_eq!([240, 33, 95], lch.to_rgb()); /// ``` pub fn to_rgb(&self) -> [u8; 3] { self.to_lab().to_rgb() } /// Returns the `LCh`'s color in `Lab` /// /// Note that due to imprecision of floating point arithmetic, conversions /// between Lab and LCh are not stable. A chain of Lab→LCh→Lab or /// LCh→Lab→LCh operations isn’t guaranteed to give back the source colour. /// /// # Examples /// /// ``` /// let lch = lab::LCh { l: 52.33686, c: 78.15369, h: 0.25877 }; /// let lab = lch.to_lab(); /// assert_eq!(lab::Lab { l: 52.33686, a: 75.5516, b: 19.998878 }, lab); /// /// let lch = lab::LCh { l: 52.33686, c: 0.0, h: 0.25877 }; /// let lab = lch.to_lab(); /// assert_eq!(lab::Lab { l: 52.33686, a: 0.0, b: 0.0 }, lab); /// /// let inp = lab::Lab { l: 29.52658, a: 58.595745, b: -36.281406 }; /// let lch = lab::LCh { l: 29.52658, c: 68.91881, h: -0.5544043 }; /// let out = lab::Lab { l: 29.52658, a: 58.59575, b: -36.281406 }; /// assert_eq!(lch, lab::LCh::from_lab(inp)); /// assert_eq!(out, lch.to_lab()); /// ``` pub fn to_lab(&self) -> Lab { Lab { l: self.l, a: self.c * self.h.cos(), b: self.c * self.h.sin(), } } } #[cfg(test)] mod tests { use super::{labs_to_rgbs, rgbs_to_labs, LCh, Lab}; use approx::assert_relative_eq; use rand::Rng; const PINK: Lab = Lab { l: 66.637695, a: 52.250145, b: 14.858591, }; #[rustfmt::skip] static COLOURS: [([u8; 3], Lab, LCh); 17] = [ ([253, 120, 138], PINK, LCh { l: 66.637695, c: 54.321777, h: 0.2770602 }), ([127, 0, 0], Lab { l: 25.299877, a: 47.77421, b: 37.752514 }, LCh { l: 25.299877, c: 60.890293, h: 0.66875386 }), ([ 0, 127, 0], Lab { l: 45.87715, a: -51.405922, b: 49.61748 }, LCh { l: 45.87715, c: 71.445526, h: 2.373896 }), ([ 0, 0, 127], Lab { l: 12.809523, a: 47.237186, b: -64.33636 }, LCh { l: 12.809523, c: 79.81553, h: -0.93746966 }), ([ 0, 127, 127], Lab { l: 47.892532, a: -28.680845, b: -8.428156 }, LCh { l: 47.892532, c: 29.893557, h: -2.8557782 }), ([127, 0, 127], Lab { l: 29.525677, a: 58.597298, b: -36.28323 }, LCh { l: 29.525677, c: 68.92109, h: -0.554415 }), ([255, 0, 0], Lab { l: 53.238235, a: 80.09231, b: 67.202095 }, LCh { l: 53.238235, c: 104.55094, h: 0.6981073 }), ([ 0, 255, 0], Lab { l: 87.73554, a: -86.18078, b: 83.18251 }, LCh { l: 87.73554, c: 119.776695, h: 2.373896 }), ([ 0, 0, 255], Lab { l: 32.298466, a: 79.192, b: -107.858345 }, LCh { l: 32.298466, c: 133.8088, h: -0.93746966 }), ([ 0, 255, 255], Lab { l: 91.11428, a: -48.08274, b: -14.12958 }, LCh { l: 91.11428, c: 50.115814, h: -2.8557787 }), ([255, 0, 255], Lab { l: 60.322693, a: 98.23698, b: -60.827957 }, LCh { l: 60.322693, c: 115.544556, h: -0.55441487 }), ([255, 255, 0], Lab { l: 97.139, a: -21.556675, b: 94.48001 }, LCh { l: 97.139, c: 96.90801, h: 1.7951176 }), ([ 0, 0, 0], Lab { l: 0.0, a: 0.0, b: 0.0 }, LCh { l: 0.0, c: 0.0, h: 0.0 }), ([ 64, 64, 64], Lab { l: 27.09341, a: 0.0, b: 0.0 }, LCh { l: 27.09341, c: 0.0, h: 0.0 }), ([127, 127, 127], Lab { l: 53.192772, a: 0.0, b: 0.0 }, LCh { l: 53.192772, c: 0.0, h: 0.0 }), ([196, 196, 196], Lab { l: 79.15698, a: 0.0, b: 0.0 }, LCh { l: 79.15698, c: 0.0, h: 0.0 }), ([255, 255, 255], Lab { l: 100.0, a: 0.0, b: 0.0 }, LCh { l: 100.0, c: 0.0, h: 0.0 }), ]; #[test] fn test_lab_from_rgb() { let expected: Vec<_> = COLOURS.iter().map(|(_, lab, _)| *lab).collect(); let actual: Vec<_> = COLOURS .iter() .map(|(rgb, _, _)| Lab::from_rgb(rgb)) .collect(); assert_eq!(expected, actual); } #[test] fn test_lab_to_rgb() { let expected: Vec<_> = COLOURS.iter().map(|(rgb, _, _)| *rgb).collect(); let actual: Vec<_> = COLOURS.iter().map(|(_, lab, _)| lab.to_rgb()).collect(); assert_eq!(expected, actual); } #[test] fn test_lch_from_rgb() { let expected: Vec<_> = COLOURS.iter().map(|(_, _, lch)| *lch).collect(); let actual: Vec<_> = COLOURS .iter() .map(|(rgb, _, _)| LCh::from_rgb(rgb)) .collect(); assert_relative_eq!(expected.as_slice(), actual.as_slice()); } #[test] fn test_lch_to_rgb() { let expected: Vec<_> = COLOURS.iter().map(|(rgb, _, _)| *rgb).collect(); let actual: Vec<_> = COLOURS.iter().map(|(_, _, lch)| lch.to_rgb()).collect(); assert_eq!(expected, actual); } #[test] fn test_lch_from_lab() { let expected: Vec<_> = COLOURS.iter().map(|(_, _, lch)| *lch).collect(); let actual: Vec<_> = COLOURS .iter() .map(|(_, lab, _)| LCh::from_lab(*lab)) .collect(); assert_relative_eq!(expected.as_slice(), actual.as_slice()); } #[test] fn test_lch_to_lab() { let mut expected: Vec<_> = COLOURS.iter().map(|(_, lab, _)| *lab).collect(); let mut actual: Vec<_> = COLOURS.iter().map(|(_, _, lch)| lch.to_lab()).collect(); // Floating point arithmetic is hard. Due to accumulation of errors (or // perhaps imprecision of trig functions) the Lab→LCh→Lab conversion // produces slightly different colour than what the source. Round a* // and b* to four decimal places to work around this. fn round(vec: &mut Vec) { for lab in vec.iter_mut() { lab.a = (lab.a * 100000.0).round() / 100000.0; lab.b = (lab.b * 100000.0).round() / 100000.0; } } round(&mut expected); round(&mut actual); assert_eq!(expected, actual); } #[test] fn test_distance() { let ugly_websafe_pink = Lab { l: 64.2116, a: 62.519463, b: 2.8871894, }; assert_eq!(254.65927, PINK.squared_distance(&ugly_websafe_pink)); } #[test] fn test_send() { fn assert_send() {} assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); } #[test] fn test_rgb_to_lab_to_rgb() { let rgbs: Vec<[u8; 3]> = { let rand_seed = [1u8; 32]; let rng: rand::rngs::StdRng = rand::SeedableRng::from_seed(rand_seed); rng.sample_iter(&rand::distributions::Standard) .take(2048) .collect() }; let labs = rgbs_to_labs(&rgbs); let rgbs2 = labs_to_rgbs(&labs); assert_eq!(rgbs2, rgbs); } #[test] fn test_grey_error() { // Grey colours have a* and b* components equal to zero. This test goes // through all 8-bit greys and calculates squared error. If it goes up, // a change might have worsen the precision of the calculations. If it // goes down, calculations got better. let mut error: f64 = 0.0; let mut count: usize = 0; for i in 0..=255_u32 { let lab = Lab::from_rgb(&[i as u8, i as u8, i as u8]); if lab.a != 0.0 || lab.b != 0.0 { error = (lab.a as f64).mul_add(lab.a as f64, error); error = (lab.b as f64).mul_add(lab.b as f64, error); count += 1; } } assert_eq!((23, 10.627054791711998), (count, error * 1e9)); } } lab-0.11.0/src/simd/labs_to_rgbs.rs000064400000000000000000000244020000000000000151510ustar 00000000000000use crate::simd::math::powf256_ps; use crate::{Lab, CBRT_EPSILON, EPSILON, KAPPA, S_0}; use std::arch::x86_64::*; use std::{f32, iter, mem}; static BLANK_LAB: Lab = Lab { l: f32::NAN, a: f32::NAN, b: f32::NAN, }; pub fn labs_to_rgbs(labs: &[Lab]) -> Vec<[u8; 3]> { let chunks = labs.chunks_exact(8); let remainder = chunks.remainder(); let mut vs = chunks.fold(Vec::with_capacity(labs.len()), |mut v, labs| { let rgbs = unsafe { slice_labs_to_slice_rgbs(labs) }; v.extend_from_slice(&rgbs); v }); // While we could simplify this block by just calling the scalar version // of the code on the remainder, there are some variations between scalar // and SIMD floating point math (especially on TravisCI for some reason?) // and I don't want the trailing N items to be computed by a different // algorithm. if remainder.len() > 0 { let labs: Vec = remainder .iter() .cloned() .chain(iter::repeat(BLANK_LAB)) .take(8) .collect(); let rgbs = unsafe { slice_labs_to_slice_rgbs(&labs) }; vs.extend_from_slice(&rgbs[..remainder.len()]); } vs } pub fn labs_to_rgb_bytes(labs: &[Lab]) -> Vec { let chunks = labs.chunks_exact(8); let remainder = chunks.remainder(); let mut vs = chunks.fold(Vec::with_capacity(labs.len()), |mut v, labs| { let bytes = unsafe { slice_labs_to_rgb_bytes(labs) }; v.extend_from_slice(&bytes); v }); if remainder.len() > 0 { let labs: Vec = remainder .iter() .cloned() .chain(iter::repeat(BLANK_LAB)) .take(8) .collect(); let bytes = unsafe { slice_labs_to_rgb_bytes(&labs) }; vs.extend_from_slice(&bytes[..remainder.len() * 3]); } vs } #[allow(dead_code)] pub fn labs_to_rgbs_chunk(labs: &[Lab]) -> [[u8; 3]; 8] { unsafe { slice_labs_to_slice_rgbs(labs) } } #[inline] unsafe fn slice_labs_to_slice_rgbs(labs: &[Lab]) -> [[u8; 3]; 8] { let (l, a, b) = lab_slice_to_simd(labs); let (x, y, z) = labs_to_xyzs(l, a, b); let (r, g, b) = xyzs_to_rgbs(x, y, z); simd_to_rgb_array(r, g, b) } #[inline] unsafe fn slice_labs_to_rgb_bytes(labs: &[Lab]) -> [u8; 8 * 3] { let (l, a, b) = lab_slice_to_simd(labs); let (x, y, z) = labs_to_xyzs(l, a, b); let (r, g, b) = xyzs_to_rgbs(x, y, z); simd_to_rgb_bytes(r, g, b) } #[inline] unsafe fn lab_slice_to_simd(labs: &[Lab]) -> (__m256, __m256, __m256) { let labs = &labs[..8]; let l = _mm256_set_ps( labs[0].l, labs[1].l, labs[2].l, labs[3].l, labs[4].l, labs[5].l, labs[6].l, labs[7].l, ); let a = _mm256_set_ps( labs[0].a, labs[1].a, labs[2].a, labs[3].a, labs[4].a, labs[5].a, labs[6].a, labs[7].a, ); let b = _mm256_set_ps( labs[0].b, labs[1].b, labs[2].b, labs[3].b, labs[4].b, labs[5].b, labs[6].b, labs[7].b, ); (l, a, b) } #[inline] unsafe fn labs_to_xyzs(l: __m256, a: __m256, b: __m256) -> (__m256, __m256, __m256) { let fy = _mm256_div_ps( _mm256_add_ps(l, _mm256_set1_ps(16.0)), _mm256_set1_ps(116.0), ); let fx = _mm256_add_ps(_mm256_div_ps(a, _mm256_set1_ps(500.0)), fy); let fz = _mm256_sub_ps(fy, _mm256_div_ps(b, _mm256_set1_ps(200.0))); let xr = { let false_branch = { let temp1 = _mm256_mul_ps(fx, _mm256_set1_ps(116.0)); let temp2 = _mm256_sub_ps(temp1, _mm256_set1_ps(16.0)); _mm256_div_ps(temp2, _mm256_set1_ps(KAPPA)) }; let unpacked_false_branch: [f32; 8] = mem::transmute(false_branch); let mut unpacked: [f32; 8] = mem::transmute(fx); for (i, el) in unpacked.iter_mut().enumerate() { if *el > CBRT_EPSILON { *el = el.powi(3); } else { *el = unpacked_false_branch[i]; } } mem::transmute(unpacked) }; let yr = { let false_branch = _mm256_div_ps(l, _mm256_set1_ps(KAPPA)); let unpacked_false_branch: [f32; 8] = mem::transmute(false_branch); let unpacked_fy: [f32; 8] = mem::transmute(fy); let mut unpacked: [f32; 8] = mem::transmute(l); for (i, el) in unpacked.iter_mut().enumerate() { if *el > EPSILON * KAPPA { *el = unpacked_fy[i].powi(3); } else { *el = unpacked_false_branch[i]; } } mem::transmute(unpacked) }; let zr = { let false_branch = { let temp1 = _mm256_mul_ps(fz, _mm256_set1_ps(116.0)); let temp2 = _mm256_sub_ps(temp1, _mm256_set1_ps(16.0)); _mm256_div_ps(temp2, _mm256_set1_ps(KAPPA)) }; let unpacked_false_branch: [f32; 8] = mem::transmute(false_branch); let mut unpacked: [f32; 8] = mem::transmute(fz); for (i, el) in unpacked.iter_mut().enumerate() { if *el > CBRT_EPSILON { *el = el.powi(3); } else { *el = unpacked_false_branch[i]; } } mem::transmute(unpacked) }; ( _mm256_mul_ps(xr, _mm256_set1_ps(0.9504492182750991)), yr, _mm256_mul_ps(zr, _mm256_set1_ps(1.0889166484304715)), ) } #[inline] unsafe fn xyzs_to_rgbs(x: __m256, y: __m256, z: __m256) -> (__m256, __m256, __m256) { let r = { let prod_x = _mm256_mul_ps(x, _mm256_set1_ps(3.240812398895283)); let prod_y = _mm256_mul_ps(y, _mm256_set1_ps(-1.5373084456298136)); let prod_z = _mm256_mul_ps(z, _mm256_set1_ps(-0.4985865229069666)); let sum = _mm256_add_ps(_mm256_add_ps(prod_x, prod_y), prod_z); xyzs_to_rgbs_map(sum) }; let g = { let prod_x = _mm256_mul_ps(x, _mm256_set1_ps(-0.9692430170086407)); let prod_y = _mm256_mul_ps(y, _mm256_set1_ps(1.8759663029085742)); let prod_z = _mm256_mul_ps(z, _mm256_set1_ps(0.04155503085668564)); let sum = _mm256_add_ps(_mm256_add_ps(prod_x, prod_y), prod_z); xyzs_to_rgbs_map(sum) }; let b = { let prod_x = _mm256_mul_ps(x, _mm256_set1_ps(0.055638398436112804)); let prod_y = _mm256_mul_ps(y, _mm256_set1_ps(-0.20400746093241362)); let prod_z = _mm256_mul_ps(z, _mm256_set1_ps(1.0571295702861434)); let sum = _mm256_add_ps(_mm256_add_ps(prod_x, prod_y), prod_z); xyzs_to_rgbs_map(sum) }; (r, g, b) } #[inline] unsafe fn xyzs_to_rgbs_map(c: __m256) -> __m256 { let mask = _mm256_cmp_ps(c, _mm256_set1_ps(S_0), _CMP_GT_OQ); let false_branch = _mm256_mul_ps(c, _mm256_set1_ps(12.92)); let true_branch = { let raised = powf256_ps(c, _mm256_set1_ps(1.0 / 2.4)); let temp2 = _mm256_mul_ps(raised, _mm256_set1_ps(1.055)); _mm256_sub_ps(temp2, _mm256_set1_ps(0.055)) }; let blended = _mm256_blendv_ps(false_branch, true_branch, mask); _mm256_mul_ps(blended, _mm256_set1_ps(255.0)) } #[inline] unsafe fn simd_to_rgb_array(r: __m256, g: __m256, b: __m256) -> [[u8; 3]; 8] { let r: [f32; 8] = mem::transmute(_mm256_round_ps(r, _MM_FROUND_TO_NEAREST_INT)); let g: [f32; 8] = mem::transmute(_mm256_round_ps(g, _MM_FROUND_TO_NEAREST_INT)); let b: [f32; 8] = mem::transmute(_mm256_round_ps(b, _MM_FROUND_TO_NEAREST_INT)); let mut rgbs: [mem::MaybeUninit<[u8; 3]>; 8] = mem::MaybeUninit::uninit().assume_init(); for (((&r, &g), &b), rgb) in r .iter() .zip(g.iter()) .zip(b.iter()) .rev() .zip(rgbs.iter_mut()) { *rgb = mem::MaybeUninit::new([r as u8, g as u8, b as u8]); } mem::transmute(rgbs) } #[inline] unsafe fn simd_to_rgb_bytes(r: __m256, g: __m256, b: __m256) -> [u8; 8 * 3] { let r: [f32; 8] = mem::transmute(_mm256_round_ps(r, _MM_FROUND_TO_NEAREST_INT)); let g: [f32; 8] = mem::transmute(_mm256_round_ps(g, _MM_FROUND_TO_NEAREST_INT)); let b: [f32; 8] = mem::transmute(_mm256_round_ps(b, _MM_FROUND_TO_NEAREST_INT)); let mut bytes: [mem::MaybeUninit; 8 * 3] = mem::MaybeUninit::uninit().assume_init(); for (((&r, &g), &b), rgb) in r .iter() .zip(g.iter()) .zip(b.iter()) .rev() .zip(bytes.chunks_exact_mut(3)) { rgb[0] = mem::MaybeUninit::new(r as u8); rgb[1] = mem::MaybeUninit::new(g as u8); rgb[2] = mem::MaybeUninit::new(b as u8); } mem::transmute(bytes) } #[cfg(test)] mod test { use crate::{labs_to_rgbs, simd, Lab}; use lazy_static::lazy_static; use rand; use rand::distributions::Standard; use rand::Rng; lazy_static! { static ref RGBS: Vec<[u8; 3]> = { let rand_seed = [0u8; 32]; let rng: rand::rngs::StdRng = rand::SeedableRng::from_seed(rand_seed); rng.sample_iter(&Standard).take(512).collect() }; } #[test] fn test_simd_labs_to_rgbs() { let labs = simd::rgbs_to_labs(&RGBS); let rgbs = simd::labs_to_rgbs(&labs); assert_eq!(rgbs.as_slice(), RGBS.as_slice()); } #[test] fn test_simd_labs_to_rgb_bytes() { // Assert that returning a single slice of bytes returns the same values as // returning them in rgb triples. #[rustfmt::skip] let labs = vec![ Lab { l: 65.55042, a: 64.48197, b: -11.685503 }, Lab { l: 48.25341, a: 74.3235, b: 62.362576 }, Lab { l: 69.485344, a: 36.825745, b: 75.4871 }, Lab { l: 93.01275, a: -13.856977, b: 91.47719 }, Lab { l: 46.70882, a: -50.29225, b: 42.086266 }, Lab { l: 70.86147, a: -38.99568, b: -11.459422 }, Lab { l: 40.166237, a: 55.847153, b: -94.75334 }, Lab { l: 28.53371, a: 58.779716, b: -44.23661 }, ]; let rgbs = simd::labs_to_rgbs(&labs).iter().fold( Vec::with_capacity(labs.len() * 3), |mut acc, rgb| { acc.extend_from_slice(rgb); acc }, ); let bytes = simd::labs_to_rgb_bytes(&labs); assert_eq!(rgbs, bytes); } #[test] fn test_simd_labs_to_rgbs_unsaturated() { let labs = vec![Lab { l: 66.6348, a: 52.260696, b: 14.850557, }]; let rgbs_non_simd = labs_to_rgbs(&labs); let rgbs_simd = simd::labs_to_rgbs(&labs); assert_eq!(rgbs_simd, rgbs_non_simd); } } lab-0.11.0/src/simd/math.rs000064400000000000000000000220300000000000000134350ustar 00000000000000/* An almost-literal transliteration of AVX-optimized sin(), cos(), exp() and log() functions by Giovanni Garberoglio, available at http://software-lisc.fbk.eu/avx_mathfun/ Copyright (C) 2012 Giovanni Garberoglio Interdisciplinary Laboratory for Computational Science (LISC) Fondazione Bruno Kessler and University of Trento via Sommarive, 18 I-38123 Trento (Italy) which was itself a translation of Simple SSE and SSE2 optimized sin, cos, log, and exp by Julien Pommier, available at http://gruntthepeon.free.fr/ssemath/ Copyright (C) 2007 Julien Pommier Both are provided under the zlib license: This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */ use std::arch::x86_64::*; use std::mem; static X7F: i32 = 0x7f; static SQRTHF: f32 = 0.70710678118654752; static LOG_P0: f32 = 7.0376836292E-2; static LOG_P1: f32 = -1.1514610310E-1; static LOG_P2: f32 = 1.1676998740E-1; static LOG_P3: f32 = -1.2420140846E-1; static LOG_P4: f32 = 1.4249322787E-1; static LOG_P5: f32 = -1.6668057665E-1; static LOG_P6: f32 = 2.0000714765E-1; static LOG_P7: f32 = -2.4999993993E-1; static LOG_P8: f32 = 3.3333331174E-1; static LOG_Q1: f32 = -2.12194440e-4; static LOG_Q2: f32 = 0.693359375; pub unsafe fn log256_ps(x: __m256) -> __m256 { let one = _mm256_set1_ps(1.0); let p5 = _mm256_set1_ps(0.5); let invalid_mask = _mm256_cmp_ps(x, _mm256_setzero_ps(), _CMP_LE_OS); let _ps256_min_norm_pos: __m256 = mem::transmute(_mm256_set1_epi32(0x00800000)); let mut x = _mm256_max_ps(x, _ps256_min_norm_pos); /* cut off denormalized stuff */ let mut imm0 = _mm256_srli_epi32(_mm256_castps_si256(x), 23); /* keep only the fractional part */ let _ps256_inv_mant_mask: __m256 = mem::transmute(_mm256_set1_epi32(!0x7f800000)); x = _mm256_and_ps(x, _ps256_inv_mant_mask); x = _mm256_or_ps(x, p5); // this is again another AVX2 instruction imm0 = _mm256_sub_epi32(imm0, _mm256_set1_epi32(X7F)); let mut e = _mm256_cvtepi32_ps(imm0); e = _mm256_add_ps(e, one); let mask = _mm256_cmp_ps(x, _mm256_set1_ps(SQRTHF), _CMP_LT_OS); let mut tmp = _mm256_and_ps(x, mask); x = _mm256_sub_ps(x, one); e = _mm256_sub_ps(e, _mm256_and_ps(one, mask)); x = _mm256_add_ps(x, tmp); let z = _mm256_mul_ps(x, x); let mut y = _mm256_set1_ps(LOG_P0); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(LOG_P1)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(LOG_P2)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(LOG_P3)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(LOG_P4)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(LOG_P5)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(LOG_P6)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(LOG_P7)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(LOG_P8)); y = _mm256_mul_ps(y, x); y = _mm256_mul_ps(y, z); tmp = _mm256_mul_ps(e, _mm256_set1_ps(LOG_Q1)); y = _mm256_add_ps(y, tmp); tmp = _mm256_mul_ps(z, p5); y = _mm256_sub_ps(y, tmp); tmp = _mm256_mul_ps(e, _mm256_set1_ps(LOG_Q2)); x = _mm256_add_ps(x, y); x = _mm256_add_ps(x, tmp); x = _mm256_or_ps(x, invalid_mask); // negative arg will be NAN return x; } static EXP_HI: f32 = 88.3762626647949; static EXP_LO: f32 = -88.3762626647949; static LOG2EF: f32 = 1.44269504088896341; static EXP_C1: f32 = 0.693359375; static EXP_C2: f32 = -2.12194440e-4; static EXP_P0: f32 = 1.9875691500E-4; static EXP_P1: f32 = 1.3981999507E-3; static EXP_P2: f32 = 8.3334519073E-3; static EXP_P3: f32 = 4.1665795894E-2; static EXP_P4: f32 = 1.6666665459E-1; static EXP_P5: f32 = 5.0000001201E-1; pub unsafe fn exp256_ps(x: __m256) -> __m256 { let one = _mm256_set1_ps(1.0); let mut x = _mm256_min_ps(x, _mm256_set1_ps(EXP_HI)); x = _mm256_max_ps(x, _mm256_set1_ps(EXP_LO)); /* express exp(x) as exp(g + n*log(2)) */ let mut fx = _mm256_mul_ps(x, _mm256_set1_ps(LOG2EF)); fx = _mm256_add_ps(fx, _mm256_set1_ps(0.5)); /* how to perform a floorf with SSE: just below */ //imm0 = _mm256_cvttps_epi32(fx); //tmp = _mm256_cvtepi32_ps(imm0); let mut tmp = _mm256_floor_ps(fx); /* if greater, substract 1 */ //v8sf mask = _mm256_cmpgt_ps(tmp, fx); let mut mask = _mm256_cmp_ps(tmp, fx, _CMP_GT_OS); mask = _mm256_and_ps(mask, one); fx = _mm256_sub_ps(tmp, mask); tmp = _mm256_mul_ps(fx, _mm256_set1_ps(EXP_C1)); let mut z = _mm256_mul_ps(fx, _mm256_set1_ps(EXP_C2)); x = _mm256_sub_ps(x, tmp); x = _mm256_sub_ps(x, z); z = _mm256_mul_ps(x, x); let mut y = _mm256_set1_ps(EXP_P0); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(EXP_P1)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(EXP_P2)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(EXP_P3)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(EXP_P4)); y = _mm256_mul_ps(y, x); y = _mm256_add_ps(y, _mm256_set1_ps(EXP_P5)); y = _mm256_mul_ps(y, z); y = _mm256_add_ps(y, x); y = _mm256_add_ps(y, one); /* build 2^n */ let mut imm0 = _mm256_cvttps_epi32(fx); // another two AVX2 instructions imm0 = _mm256_add_epi32(imm0, _mm256_set1_epi32(X7F)); imm0 = _mm256_slli_epi32(imm0, 23); let pow2n = _mm256_castsi256_ps(imm0); y = _mm256_mul_ps(y, pow2n); return y; } pub unsafe fn powf256_ps(x: __m256, y: __m256) -> __m256 { let invalid_mask = _mm256_cmp_ps(x, _mm256_setzero_ps(), _CMP_LE_OS); let result = exp256_ps(_mm256_mul_ps(y, log256_ps(x))); _mm256_or_ps(result, invalid_mask) } #[cfg(test)] mod test { use super::{exp256_ps, log256_ps, powf256_ps}; use approx::assert_relative_eq; use std::arch::x86_64::*; use std::{f32, mem}; #[test] fn test_log256_ps() { let scalar_result: Vec<_> = { let vals: [f32; 8] = [0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]; vals.iter().copied().map(f32::ln).collect() }; let avx_result: Vec<_> = unsafe { let vals = _mm256_set_ps(0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0); let result = log256_ps(vals); let result: [f32; 8] = mem::transmute(result); result.iter().rev().copied().collect() }; assert_relative_eq!(scalar_result.as_slice(), avx_result.as_slice()) } #[test] fn test_negative_log_returns_nan() { let avx_result: Vec<_> = unsafe { let vals = _mm256_set_ps(-0.5, 1.0, -2.0, 3.0, -4.0, 5.0, -6.0, 7.0); let result = log256_ps(vals); let result: [f32; 8] = mem::transmute(result); result.iter().rev().copied().collect() }; assert!(f32::is_nan(avx_result[0])); assert!(!f32::is_nan(avx_result[1])); assert!(f32::is_nan(avx_result[2])); assert!(!f32::is_nan(avx_result[3])); assert!(f32::is_nan(avx_result[4])); assert!(!f32::is_nan(avx_result[5])); assert!(f32::is_nan(avx_result[6])); assert!(!f32::is_nan(avx_result[7])); } #[test] fn test_exp256_ps() { let scalar_result: Vec<_> = { let vals: [f32; 8] = [-1.5, 0.5, 1.0, 2.0, 4.0, 5.0, 6.0, 10.0]; vals.iter().copied().map(f32::exp).collect() }; let avx_result: Vec<_> = unsafe { let vals = _mm256_set_ps(-1.5, 0.5, 1.0, 2.0, 4.0, 5.0, 6.0, 10.0); let result = exp256_ps(vals); let result: [f32; 8] = mem::transmute(result); result.iter().rev().copied().collect() }; assert_relative_eq!(scalar_result.as_slice(), avx_result.as_slice()) } #[test] fn test_powf256_ps() { let exponent = 4.0; let scalar_result: Vec<_> = { let vals: [f32; 8] = [0.25, 0.5, 1.0, 2.0, 4.0, 5.0, 6.0, 10.0]; vals.iter().map(|&n| n.powf(exponent)).collect() }; let avx_result: Vec<_> = unsafe { let vals = _mm256_set_ps(0.25, 0.5, 1.0, 2.0, 4.0, 5.0, 6.0, 10.0); let three = _mm256_set1_ps(exponent); let result = powf256_ps(vals, three); let result: [f32; 8] = mem::transmute(result); result.iter().rev().copied().collect() }; assert_relative_eq!(scalar_result.as_slice(), avx_result.as_slice()) } } lab-0.11.0/src/simd/mod.rs000064400000000000000000000006010000000000000132630ustar 00000000000000//! Experimental functions for parallel conversion using AVX and SSE4.1 //! //! This module is conditionally compiled by the cfg gate //! `#[cfg(target_arch = "x86_64")]` mod labs_to_rgbs; mod math; mod rgbs_to_labs; pub use self::labs_to_rgbs::{labs_to_rgb_bytes, labs_to_rgbs, labs_to_rgbs_chunk}; pub use self::rgbs_to_labs::{rgb_bytes_to_labs, rgbs_to_labs, rgbs_to_labs_chunk}; lab-0.11.0/src/simd/rgbs_to_labs.rs000064400000000000000000000276340000000000000151630ustar 00000000000000use crate::simd::math::powf256_ps; use crate::{Lab, EPSILON, E_0_255, KAPPA}; use std::arch::x86_64::*; use std::{iter, mem}; static BLANK_RGB: [u8; 3] = [0u8; 3]; pub fn rgbs_to_labs(rgbs: &[[u8; 3]]) -> Vec { let chunks = rgbs.chunks_exact(8); let remainder = chunks.remainder(); let mut vs = chunks.fold(Vec::with_capacity(rgbs.len()), |mut v, rgbs| { let labs = unsafe { slice_rgbs_to_slice_labs(rgbs) }; v.extend_from_slice(&labs); v }); // While we could simplify this block by just calling the scalar version // of the code on the remainder, there are some variations between scalar // and SIMD floating point math (especially on TravisCI for some reason?) // and I don't want the trailing N items to be computed by a different // algorithm. if remainder.len() > 0 { let rgbs: Vec<[u8; 3]> = remainder .iter() .cloned() .chain(iter::repeat(BLANK_RGB)) .take(8) .collect(); let labs = unsafe { slice_rgbs_to_slice_labs(&rgbs) }; vs.extend_from_slice(&labs[..remainder.len()]); } vs } pub fn rgb_bytes_to_labs(bytes: &[u8]) -> Vec { let chunks = bytes.chunks_exact(8 * 3); let remainder = chunks.remainder(); let mut vs = chunks.fold(Vec::with_capacity(bytes.len() / 3), |mut v, bytes| { let labs = unsafe { slice_bytes_to_slice_labs(bytes) }; v.extend_from_slice(&labs); v }); if remainder.len() > 0 { let bytes: Vec = remainder .iter() .cloned() .chain(iter::repeat(0u8)) .take(8 * 3) .collect(); let labs = unsafe { slice_bytes_to_slice_labs(&bytes) }; vs.extend_from_slice(&labs[..remainder.len() / 3]); } vs } #[allow(dead_code)] pub fn rgbs_to_labs_chunk(rgbs: &[[u8; 3]]) -> [Lab; 8] { unsafe { slice_rgbs_to_slice_labs(rgbs) } } unsafe fn slice_rgbs_to_slice_labs(rgbs: &[[u8; 3]]) -> [Lab; 8] { let (r, g, b) = rgb_bytes_to_simd(rgbs); let (x, y, z) = rgbs_to_xyzs(r, g, b); let (l, a, b) = xyzs_to_labs(x, y, z); simd_to_lab_array(l, a, b) } #[inline] unsafe fn slice_bytes_to_slice_labs(bytes: &[u8]) -> [Lab; 8] { let (r, g, b) = byte_slice_to_simd(bytes); let (x, y, z) = rgbs_to_xyzs(r, g, b); let (l, a, b) = xyzs_to_labs(x, y, z); simd_to_lab_array(l, a, b) } #[inline] unsafe fn rgb_bytes_to_simd(rgbs: &[[u8; 3]]) -> (__m256, __m256, __m256) { let r = _mm256_set_ps( rgbs[0][0] as f32, rgbs[1][0] as f32, rgbs[2][0] as f32, rgbs[3][0] as f32, rgbs[4][0] as f32, rgbs[5][0] as f32, rgbs[6][0] as f32, rgbs[7][0] as f32, ); let g = _mm256_set_ps( rgbs[0][1] as f32, rgbs[1][1] as f32, rgbs[2][1] as f32, rgbs[3][1] as f32, rgbs[4][1] as f32, rgbs[5][1] as f32, rgbs[6][1] as f32, rgbs[7][1] as f32, ); let b = _mm256_set_ps( rgbs[0][2] as f32, rgbs[1][2] as f32, rgbs[2][2] as f32, rgbs[3][2] as f32, rgbs[4][2] as f32, rgbs[5][2] as f32, rgbs[6][2] as f32, rgbs[7][2] as f32, ); (r, g, b) } #[inline] unsafe fn byte_slice_to_simd(bytes: &[u8]) -> (__m256, __m256, __m256) { let r = _mm256_set_ps( bytes[3 * 0] as f32, bytes[3 * 1] as f32, bytes[3 * 2] as f32, bytes[3 * 3] as f32, bytes[3 * 4] as f32, bytes[3 * 5] as f32, bytes[3 * 6] as f32, bytes[3 * 7] as f32, ); let g = _mm256_set_ps( bytes[3 * 0 + 1] as f32, bytes[3 * 1 + 1] as f32, bytes[3 * 2 + 1] as f32, bytes[3 * 3 + 1] as f32, bytes[3 * 4 + 1] as f32, bytes[3 * 5 + 1] as f32, bytes[3 * 6 + 1] as f32, bytes[3 * 7 + 1] as f32, ); let b = _mm256_set_ps( bytes[3 * 0 + 2] as f32, bytes[3 * 1 + 2] as f32, bytes[3 * 2 + 2] as f32, bytes[3 * 3 + 2] as f32, bytes[3 * 4 + 2] as f32, bytes[3 * 5 + 2] as f32, bytes[3 * 6 + 2] as f32, bytes[3 * 7 + 2] as f32, ); (r, g, b) } unsafe fn rgbs_to_xyzs(r: __m256, g: __m256, b: __m256) -> (__m256, __m256, __m256) { let r = rgbs_to_xyzs_map(r); let g = rgbs_to_xyzs_map(g); let b = rgbs_to_xyzs_map(b); let x = { let prod_r = _mm256_mul_ps(r, _mm256_set1_ps(0.4124108464885388)); let prod_g = _mm256_mul_ps(g, _mm256_set1_ps(0.3575845678529519)); let prod_b = _mm256_mul_ps(b, _mm256_set1_ps(0.18045380393360833)); _mm256_add_ps(_mm256_add_ps(prod_r, prod_g), prod_b) }; let y = { let prod_r = _mm256_mul_ps(r, _mm256_set1_ps(0.21264934272065283)); let prod_g = _mm256_mul_ps(g, _mm256_set1_ps(0.7151691357059038)); let prod_b = _mm256_mul_ps(b, _mm256_set1_ps(0.07218152157344333)); _mm256_add_ps(_mm256_add_ps(prod_r, prod_g), prod_b) }; let z = { let prod_r = _mm256_mul_ps(r, _mm256_set1_ps(0.019331758429150258)); let prod_g = _mm256_mul_ps(g, _mm256_set1_ps(0.11919485595098397)); let prod_b = _mm256_mul_ps(b, _mm256_set1_ps(0.9503900340503373)); _mm256_add_ps(_mm256_add_ps(prod_r, prod_g), prod_b) }; (x, y, z) } #[inline] unsafe fn rgbs_to_xyzs_map(c: __m256) -> __m256 { let mask = _mm256_cmp_ps(c, _mm256_set1_ps(E_0_255), _CMP_GT_OQ); let true_branch = { const A: f32 = 0.055 * 255.0; const D: f32 = 1.055 * 255.0; let t0 = _mm256_div_ps(_mm256_add_ps(c, _mm256_set1_ps(A)), _mm256_set1_ps(D)); powf256_ps(t0, _mm256_set1_ps(2.4)) }; let false_branch = { const D: f32 = 12.92 * 255.0; _mm256_div_ps(c, _mm256_set1_ps(D)) }; _mm256_blendv_ps(false_branch, true_branch, mask) } unsafe fn xyzs_to_labs(x: __m256, y: __m256, z: __m256) -> (__m256, __m256, __m256) { let x = xyzs_to_labs_map(_mm256_div_ps(x, _mm256_set1_ps(0.9504492182750991))); let y = xyzs_to_labs_map(y); let z = xyzs_to_labs_map(_mm256_div_ps(z, _mm256_set1_ps(1.0889166484304715))); let l = _mm256_add_ps( _mm256_mul_ps(y, _mm256_set1_ps(116.0)), _mm256_set1_ps(-16.0), ); let a = _mm256_mul_ps(_mm256_sub_ps(x, y), _mm256_set1_ps(500.0)); let b = _mm256_mul_ps(_mm256_sub_ps(y, z), _mm256_set1_ps(200.0)); (l, a, b) } #[inline] unsafe fn xyzs_to_labs_map(c: __m256) -> __m256 { let mask = _mm256_cmp_ps(c, _mm256_set1_ps(EPSILON), _CMP_GT_OQ); // do false branch first let false_branch = _mm256_div_ps( _mm256_add_ps( _mm256_mul_ps(c, _mm256_set1_ps(KAPPA)), _mm256_set1_ps(16.0), ), _mm256_set1_ps(116.0), ); let true_branch = powf256_ps(c, _mm256_set1_ps(1.0 / 3.0)); _mm256_blendv_ps(false_branch, true_branch, mask) } unsafe fn simd_to_lab_array(l: __m256, a: __m256, b: __m256) -> [Lab; 8] { let l: [f32; 8] = mem::transmute(l); let a: [f32; 8] = mem::transmute(a); let b: [f32; 8] = mem::transmute(b); // Per `https://doc.rust-lang.org/std/mem/union.MaybeUninit.html` this `assume_init` // is safe because the MaybeUninits inside the array don't require initialization? let mut labs: [mem::MaybeUninit; 8] = mem::MaybeUninit::uninit().assume_init(); for (((&l, &a), &b), lab) in l .iter() .zip(a.iter()) .zip(b.iter()) .rev() .zip(labs.iter_mut()) { *lab = mem::MaybeUninit::new(Lab { l, a, b }); } mem::transmute(labs) } // #[inline] // unsafe fn clamp(r: __m256, g: __m256, b: __m256) -> (__m256, __m256, __m256) { // let max = _mm256_set1_ps(1.0); // let min = _mm256_set1_ps(0.0); // let r = _mm256_max_ps(_mm256_min_ps(r, max), min); // let g = _mm256_max_ps(_mm256_min_ps(g, max), min); // let b = _mm256_max_ps(_mm256_min_ps(b, max), min); // (r, g, b) // } // #[inline] // unsafe fn normalize_short_to_unit(r: __m256i, g: __m256i, b: __m256i) -> (__m256, __m256, __m256) { // let normalizer = _mm256_set1_ps(255.0); // let r = _mm256_div_ps(r, normalizer); // let g = _mm256_div_ps(g, normalizer); // let b = _mm256_div_ps(b, normalizer); // (r, g, b) // } #[cfg(test)] mod test { use crate::{rgbs_to_labs, simd}; use approx::assert_relative_eq; use lazy_static::lazy_static; use rand; use rand::distributions::Standard; use rand::Rng; lazy_static! { static ref RGBS: Vec<[u8; 3]> = { let rand_seed = [0u8; 32]; let rng: rand::rngs::StdRng = rand::SeedableRng::from_seed(rand_seed); rng.sample_iter(&Standard).take(512).collect() }; } #[test] fn test_simd_rgbs_to_labs() { let rgbs = vec![ [253, 120, 138], // Lab { l: 66.6348, a: 52.260696, b: 14.850557 } [25, 20, 22], // Lab { l: 6.9093895, a: 2.8204322, b: -0.45616925 } [63, 81, 181], // Lab { l: 38.336494, a: 25.586218, b: -55.288517 } [21, 132, 102], // Lab { l: 49.033485, a: -36.959187, b: 7.9363704 } [255, 193, 7], // Lab { l: 81.519325, a: 9.4045105, b: 82.69791 } [233, 30, 99], // Lab { l: 50.865776, a: 74.61989, b: 15.343171 } [155, 96, 132], // Lab { l: 48.260345, a: 29.383003, b: -9.950054 } [249, 165, 33], // Lab { l: 74.29188, a: 21.827251, b: 72.75864 } ]; let labs_non_simd = rgbs_to_labs(&rgbs); let labs_simd = simd::rgbs_to_labs(&rgbs); assert_relative_eq!( labs_simd.as_slice(), labs_non_simd.as_slice(), max_relative = 0.00002 ); } #[test] fn test_simd_rgb_bytes_to_labs() { // Assert that converting a slice of bytes and a slice of rgb triples // returns the same Lab values. let rgbs = vec![ [253, 120, 138], // Lab { l: 66.6348, a: 52.260696, b: 14.850557 } [25, 20, 22], // Lab { l: 6.9093895, a: 2.8204322, b: -0.45616925 } [63, 81, 181], // Lab { l: 38.336494, a: 25.586218, b: -55.288517 } [21, 132, 102], // Lab { l: 49.033485, a: -36.959187, b: 7.9363704 } [255, 193, 7], // Lab { l: 81.519325, a: 9.4045105, b: 82.69791 } [233, 30, 99], // Lab { l: 50.865776, a: 74.61989, b: 15.343171 } [155, 96, 132], // Lab { l: 48.260345, a: 29.383003, b: -9.950054 } [249, 165, 33], // Lab { l: 74.29188, a: 21.827251, b: 72.75864 } ]; #[rustfmt::skip] let bytes = vec![ 253, 120, 138, // Lab { l: 66.6348, a: 52.260696, b: 14.850557 } 25, 20, 22, // Lab { l: 6.9093895, a: 2.8204322, b: -0.45616925 } 63, 81, 181, // Lab { l: 38.336494, a: 25.586218, b: -55.288517 } 21, 132, 102, // Lab { l: 49.033485, a: -36.959187, b: 7.9363704 } 255, 193, 7, // Lab { l: 81.519325, a: 9.4045105, b: 82.69791 } 233, 30, 99, // Lab { l: 50.865776, a: 74.61989, b: 15.343171 } 155, 96, 132, // Lab { l: 48.260345, a: 29.383003, b: -9.950054 } 249, 165, 33, // Lab { l: 74.29188, a: 21.827251, b: 72.75864 } ]; let labs_from_triples = simd::rgbs_to_labs(&rgbs); let labs_from_bytes = simd::rgb_bytes_to_labs(&bytes); assert_eq!(labs_from_triples, labs_from_bytes); } #[test] fn test_simd_rgbs_to_labs_many() { let labs_non_simd = rgbs_to_labs(&RGBS); let labs_simd = simd::rgbs_to_labs(&RGBS); assert_relative_eq!( labs_simd.as_slice(), labs_non_simd.as_slice(), max_relative = 0.00005 ); } #[test] fn test_simd_rgbs_to_labs_unsaturated() { let rgbs = vec![[253, 120, 138]]; let labs_non_simd = rgbs_to_labs(&rgbs); let labs_simd = simd::rgbs_to_labs(&rgbs); assert_relative_eq!( labs_simd.as_slice(), labs_non_simd.as_slice(), max_relative = 0.00002 ); } }