Skip to content

Commit

Permalink
Merge #308
Browse files Browse the repository at this point in the history
308: Allow RGB/XYZ conversion matrices to be pre-defined r=Ogeon a=Ogeon

As suggested by `@Kannen` in #199 (comment), make it possible to have RGB to XYZ and XYZ to RGB matrices pre-defined for an RGB space. This results in significantly faster conversions.

Co-authored-by: Erik Hedvall <erikwhedvall@gmail.com>
  • Loading branch information
bors[bot] and Ogeon committed Apr 10, 2023
2 parents 4365f7a + df9902f commit 4406b38
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Expand Up @@ -13,7 +13,7 @@ jobs:
toolchain: stable
override: true
profile: minimal
- uses: boa-dev/criterion-compare-action@v3.2.0
- uses: boa-dev/criterion-compare-action@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
branchName: ${{ github.base_ref }}
Expand Down
44 changes: 42 additions & 2 deletions palette/src/encoding/srgb.rs
Expand Up @@ -7,7 +7,7 @@ use crate::{
num::{Arithmetics, MulAdd, MulSub, PartialCmp, Powf, Real},
rgb::{Primaries, RgbSpace, RgbStandard},
white_point::{Any, D65},
Yxy,
Mat3, Yxy,
};

use lookup_tables::*;
Expand Down Expand Up @@ -59,6 +59,28 @@ impl<T: Real> Primaries<T> for Srgb {
impl RgbSpace for Srgb {
type Primaries = Srgb;
type WhitePoint = D65;

#[rustfmt::skip]
#[inline(always)]
fn rgb_to_xyz_matrix() -> Option<Mat3<f64>> {
// Matrix from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
Some([
0.4124564, 0.3575761, 0.1804375,
0.2126729, 0.7151522, 0.0721750,
0.0193339, 0.1191920, 0.9503041,
])
}

#[rustfmt::skip]
#[inline(always)]
fn xyz_to_rgb_matrix() -> Option<Mat3<f64>> {
// Matrix from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
Some([
3.2404542, -1.5371385, -0.4985314,
-0.9692660, 1.8760108, 0.0415560,
0.0556434, -0.2040259, 1.0572252,
])
}
}

impl RgbStandard for Srgb {
Expand Down Expand Up @@ -130,7 +152,25 @@ impl FromLinear<f64, u8> for Srgb {

#[cfg(test)]
mod test {
use crate::encoding::{FromLinear, IntoLinear, Srgb};
use crate::{
encoding::{FromLinear, IntoLinear, Srgb},
matrix::{matrix_inverse, rgb_to_xyz_matrix},
rgb::RgbSpace,
};

#[test]
fn rgb_to_xyz() {
let dynamic = rgb_to_xyz_matrix::<Srgb, f64>();
let constant = Srgb::rgb_to_xyz_matrix().unwrap();
assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001);
}

#[test]
fn xyz_to_rgb() {
let dynamic = matrix_inverse(rgb_to_xyz_matrix::<Srgb, f64>());
let constant = Srgb::xyz_to_rgb_matrix().unwrap();
assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001);
}

#[test]
fn u8_to_f32_to_u8() {
Expand Down
19 changes: 19 additions & 0 deletions palette/src/matrix.rs
Expand Up @@ -152,6 +152,25 @@ where
]
}

/// Maps a matrix from one item type to another.
///
/// This turned out to be easier for the compiler to optimize than `matrix.map(f)`.
#[inline(always)]
pub fn matrix_map<T, U>(matrix: Mat3<T>, mut f: impl FnMut(T) -> U) -> Mat3<U> {
let [m1, m2, m3, m4, m5, m6, m7, m8, m9] = matrix;
[
f(m1),
f(m2),
f(m3),
f(m4),
f(m5),
f(m6),
f(m7),
f(m8),
f(m9),
]
}

/// Generates the Srgb to Xyz transformation matrix for a given white point.
#[inline]
pub fn rgb_to_xyz_matrix<S, T>() -> Mat3<T>
Expand Down
22 changes: 21 additions & 1 deletion palette/src/rgb.rs
Expand Up @@ -64,7 +64,7 @@ use crate::{
encoding::{self, FromLinear, Gamma, IntoLinear, Linear},
stimulus::{FromStimulus, Stimulus},
white_point::Any,
Yxy,
Mat3, Yxy,
};

pub use self::rgb::{FromHexError, Rgb, Rgba};
Expand Down Expand Up @@ -121,6 +121,26 @@ pub trait RgbSpace {

/// The white point of the RGB color space.
type WhitePoint;

/// Get a pre-defined matrix for converting an RGB value with this standard
/// into an XYZ value.
///
/// Returning `None` (as in the default implementation) means that the
/// matrix will be computed dynamically, which is significantly slower.
#[inline(always)]
fn rgb_to_xyz_matrix() -> Option<Mat3<f64>> {
None
}

/// Get a pre-defined matrix for converting an XYZ value into an RGB value
/// with this standard.
///
/// Returning `None` (as in the default implementation) means that the
/// matrix will be computed dynamically, which is significantly slower.
#[inline(always)]
fn xyz_to_rgb_matrix() -> Option<Mat3<f64>> {
None
}
}

impl<P, W> RgbSpace for (P, W) {
Expand Down
17 changes: 13 additions & 4 deletions palette/src/rgb/rgb.rs
Expand Up @@ -29,7 +29,7 @@ use crate::{
convert::{FromColorUnclamped, IntoColorUnclamped},
encoding::{FromLinear, IntoLinear, Linear, Srgb},
luma::LumaStandard,
matrix::{matrix_inverse, multiply_xyz_to_rgb, rgb_to_xyz_matrix},
matrix::{matrix_inverse, matrix_map, multiply_xyz_to_rgb, rgb_to_xyz_matrix},
num::{
self, Abs, Arithmetics, FromScalar, FromScalarArray, IntoScalarArray, IsValidDivisor,
MinMax, One, PartialCmp, Real, Recip, Round, Trigonometry, Zero,
Expand Down Expand Up @@ -251,6 +251,7 @@ impl<S: RgbStandard, T> Rgb<S, T> {
///
/// See the transfer function types in the [`encoding`](crate::encoding)
/// module for details and performance characteristics.
#[inline(always)]
pub fn into_linear<U>(self) -> Rgb<Linear<S::Space>, U>
where
S::TransferFn: IntoLinear<U, T>,
Expand All @@ -276,6 +277,7 @@ impl<S: RgbStandard, T> Rgb<S, T> {
///
/// See the transfer function types in the [`encoding`](crate::encoding)
/// module for details and performance characteristics.
#[inline(always)]
pub fn from_linear<U>(color: Rgb<Linear<S::Space>, U>) -> Self
where
S::TransferFn: FromLinear<U, T>,
Expand Down Expand Up @@ -589,12 +591,19 @@ where
<S::Space as RgbSpace>::Primaries: Primaries<T::Scalar>,
<S::Space as RgbSpace>::WhitePoint: WhitePoint<T::Scalar>,
T: Arithmetics + FromScalar,
T::Scalar:
Recip + IsValidDivisor<Mask = bool> + Arithmetics + Clone + FromScalar<Scalar = T::Scalar>,
T::Scalar: Real
+ Recip
+ IsValidDivisor<Mask = bool>
+ Arithmetics
+ Clone
+ FromScalar<Scalar = T::Scalar>,
Yxy<Any, T::Scalar>: IntoColorUnclamped<Xyz<Any, T::Scalar>>,
{
fn from_color_unclamped(color: Xyz<<S::Space as RgbSpace>::WhitePoint, T>) -> Self {
let transform_matrix = matrix_inverse(rgb_to_xyz_matrix::<S::Space, T::Scalar>());
let transform_matrix = S::Space::xyz_to_rgb_matrix().map_or_else(
|| matrix_inverse(rgb_to_xyz_matrix::<S::Space, T::Scalar>()),
|matrix| matrix_map(matrix, T::Scalar::from_f64),
);
Self::from_linear(multiply_xyz_to_rgb(transform_matrix, color))
}
}
Expand Down
15 changes: 11 additions & 4 deletions palette/src/xyz.rs
Expand Up @@ -21,7 +21,7 @@ use crate::{
convert::{FromColorUnclamped, IntoColorUnclamped},
encoding::IntoLinear,
luma::LumaStandard,
matrix::{multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix},
matrix::{matrix_map, multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix},
num::{
self, Arithmetics, FromScalar, FromScalarArray, IntoScalarArray, IsValidDivisor, MinMax,
One, PartialCmp, Powi, Real, Recip, Zero,
Expand Down Expand Up @@ -209,8 +209,12 @@ impl<Wp, T> FromColorUnclamped<Xyz<Wp, T>> for Xyz<Wp, T> {
impl<Wp, T, S> FromColorUnclamped<Rgb<S, T>> for Xyz<Wp, T>
where
T: Arithmetics + FromScalar,
T::Scalar:
Recip + IsValidDivisor<Mask = bool> + Arithmetics + FromScalar<Scalar = T::Scalar> + Clone,
T::Scalar: Real
+ Recip
+ IsValidDivisor<Mask = bool>
+ Arithmetics
+ FromScalar<Scalar = T::Scalar>
+ Clone,
Wp: WhitePoint<T::Scalar>,
S: RgbStandard,
S::TransferFn: IntoLinear<T, T>,
Expand All @@ -219,7 +223,10 @@ where
Yxy<Any, T::Scalar>: IntoColorUnclamped<Xyz<Any, T::Scalar>>,
{
fn from_color_unclamped(color: Rgb<S, T>) -> Self {
let transform_matrix = rgb_to_xyz_matrix::<S::Space, T::Scalar>();
let transform_matrix = S::Space::rgb_to_xyz_matrix().map_or_else(
|| rgb_to_xyz_matrix::<S::Space, T::Scalar>(),
|matrix| matrix_map(matrix, T::Scalar::from_f64),
);
multiply_rgb_to_xyz(transform_matrix, color.into_linear())
}
}
Expand Down

0 comments on commit 4406b38

Please sign in to comment.