diff --git a/src/Magnum/Math/Color.h b/src/Magnum/Math/Color.h index f4511cdcf..5408fae1f 100644 --- a/src/Magnum/Math/Color.h +++ b/src/Magnum/Math/Color.h @@ -170,6 +170,32 @@ template inline Vector4 toSrgbAlphaIntegral(c return denormalize>(toSrgbAlpha(rgba)); } +/* CIE XYZ -> RGB conversion */ +template typename std::enable_if::value, Color3>::type fromXyz(const Vector3& xyz) { + /* Taken from https://en.wikipedia.org/wiki/Talk:SRGB#Rounded_vs._Exact, + the rounded matrices from the main article don't round-trip perfectly */ + return Matrix3x3{ + Vector3{T(12831)/T(3959), T(-851781)/T(878810), T(705)/T(12673)}, + Vector3{T(-329)/T(214), T(1648619)/T(878810), T(-2585)/T(12673)}, + Vector3{T(-1974)/T(3959), T(36519)/T(878810), T(705)/T(667)}}*xyz; +} +template inline typename std::enable_if::value, Color3>::type fromXyz(const Vector3::FloatingPointType>& xyz) { + return denormalize>(fromXyz::FloatingPointType>(xyz)); +} + +/* RGB -> CIE XYZ conversion */ +template Vector3::FloatingPointType> toXyz(typename std::enable_if::value, const Color3&>::type rgb) { + /* Taken from https://en.wikipedia.org/wiki/Talk:SRGB#Rounded_vs._Exact, + the rounded matrices from the main article don't round-trip perfectly */ + return (Matrix3x3{ + Vector3{T(506752)/T(1228815), T(87098)/T(409605), T(7918)/T(409605)}, + Vector3{T(87881)/T(245763), T(175762)/T(245763), T(87881)/T(737289)}, + Vector3{T(12673)/T(70218), T(12673)/T(175545), T(1001167)/T(1053270)}})*rgb; +} +template inline Vector3::FloatingPointType> toXyz(typename std::enable_if::value, const Color3&>::type rgb) { + return toXyz::FloatingPointType>(normalize::FloatingPointType>>(rgb)); +} + /* Value for full channel (1.0f for floats, 255 for unsigned byte) */ template constexpr typename std::enable_if::value, T>::type fullChannel() { return T(1); @@ -366,6 +392,26 @@ template class Color3: public Vector3 { return Implementation::fromSrgbIntegral(srgb); } + /** + * @brief Create RGB color from CIE XYZ representation + * @param xyz Color in CIE XYZ color space + * + * Applies transformation matrix, returning the input in linear + * RGB color space with D65 illuminant and 2° standard colorimetric + * observer. @f[ + * \begin{bmatrix} R_\mathrm{linear} \\ G_\mathrm{linear} \\ B_\mathrm{linear} \end{bmatrix} = + * \begin{bmatrix} + * 3.2406 & -1.5372 & -0.4986 \\ + * -0.9689 & 1.8758 & 0.0415 \\ + * 0.0557 & -0.2040 & 1.0570 + * \end{bmatrix} \begin{bmatrix} X \\ Y \\ Z \end{bmatrix} + * @f] + * @see @ref toXyz(), @ref toSrgb() + */ + static Color3 fromXyz(const Vector3& xyz) { + return Implementation::fromXyz(xyz); + } + /** * @brief Default constructor * @@ -508,6 +554,30 @@ template class Color3: public Vector3 { return Implementation::toSrgbIntegral(*this); } + /** + * @brief Convert to CIE XYZ representation + * + * Assuming the color is in linear RGB with D65 illuminant and 2° + * standard colorimetric observer, applies transformation matrix, + * returning the color in CIE XYZ color space. @f[ + * \begin{bmatrix} X \\ Y \\ Z \end{bmatrix} = + * \begin{bmatrix} + * 0.4124 & 0.3576 & 0.1805 \\ + * 0.2126 & 0.7152 & 0.0722 \\ + * 0.0193 & 0.1192 & 0.9505 + * \end{bmatrix} + * \begin{bmatrix} R_\mathrm{linear} \\ G_\mathrm{linear} \\ B_\mathrm{linear} \end{bmatrix} + * @f] + * + * Please note that @ref x(), @ref y() and @ref z() *do not* correspond + * to primaries in CIE XYZ color space, but are rather aliases to + * @ref r(), @ref g() and @ref b(). + * @see @ref fromXyz(), @ref fromSrgb() + */ + Vector3 toXyz() const { + return Implementation::toXyz(*this); + } + MAGNUM_VECTOR_SUBCLASS_IMPLEMENTATION(3, Color3) }; @@ -708,6 +778,20 @@ class Color4: public Vector4 { return {Implementation::fromSrgbIntegral(srgb), a}; } + /** + * @brief Create RGBA color from CIE XYZ representation + * @param xyz Color in CIE XYZ color space + * @param a Alpha value, defaults to `1.0` for floating-point types + * and maximum positive value for integral types. + * + * Applies transformation matrix, returning the input in linear RGB + * color space. See @ref Color3::fromXyz() for more information. + * @see @ref toXyz(), @ref toSrgbAlpha() + */ + static Color4 fromXyz(const Vector3 xyz, T a = Implementation::fullChannel()) { + return {Implementation::fromXyz(xyz), a}; + } + /** * @brief Default constructor * @@ -849,6 +933,23 @@ class Color4: public Vector4 { return Implementation::toSrgbAlphaIntegral(*this); } + /** + * @brief Convert to CIE XYZ representation + * + * Assuming the color is in linear RGB, applies transformation matrix, + * returning the color in CIE XYZ color space. The alpha channel is not + * subject to any conversion, so it is ignored. See @ref Color3::toXyz() + * for more information. + * + * Please note that @ref xyz(), @ref x(), @ref y() and @ref z() *do not* + * correspond to primaries in CIE XYZ color space, but are rather + * aliases to @ref rgb(), @ref r(), @ref g() and @ref b(). + * @see @ref fromXyz() + */ + Vector3 toXyz() const { + return Implementation::toXyz(Vector4::rgb()); + } + MAGNUM_VECTOR_SUBCLASS_IMPLEMENTATION(4, Color4) }; diff --git a/src/Magnum/Math/Test/ColorTest.cpp b/src/Magnum/Math/Test/ColorTest.cpp index f370af9b3..d29fde1df 100644 --- a/src/Magnum/Math/Test/ColorTest.cpp +++ b/src/Magnum/Math/Test/ColorTest.cpp @@ -95,6 +95,9 @@ struct ColorTest: Corrade::TestSuite::Tester { void srgbMonotonic(); void srgbLiterals(); + void xyz(); + void fromXyzDefaultAlpha(); + void swizzleType(); void debug(); void debugUb(); @@ -140,6 +143,9 @@ ColorTest::ColorTest() { addTests({&ColorTest::fromSrgbDefaultAlpha, &ColorTest::srgbLiterals, + &ColorTest::xyz, + &ColorTest::fromXyzDefaultAlpha, + &ColorTest::swizzleType, &ColorTest::debug, &ColorTest::debugUb, @@ -644,6 +650,67 @@ void ColorTest::srgbLiterals() { CORRADE_COMPARE(0x33b27fcc_srgbaf, (Color4{0.0331048f, 0.445201f, 0.212231f, 0.8f})); } +void ColorTest::xyz() { + /* Verified using http://colormine.org/convert/rgb-to-xyz and + http://www.easyrgb.com/index.php?X=CALC. The results have slight + precision differences, because most of the code out there uses just the + rounded matrices from Wikipedia which don't round-trip perfectly. I'm + having Y in 0-1 instead of 0-100, thus the values are 100 times smaller. */ + CORRADE_COMPARE(Color3::fromSrgb({232, 157, 16}).toXyz(), + (Vector3{0.454279f, 0.413092f, 0.0607124f})); + CORRADE_COMPARE(Color3::fromXyz({0.454279f, 0.413092f, 0.0607124f}).toSrgb(), + (Math::Vector3{231, 156, 16})); + CORRADE_COMPARE(Color3::fromXyz({0.454279f, 0.413092f, 0.0607124f}), + (Color3{0.806952f, 0.337163f, 0.0051861f})); + + CORRADE_COMPARE(Color3::fromSrgb({96, 43, 193}).toXyz(), + (Vector3{0.153122f, 0.0806478f, 0.512037f})); + CORRADE_COMPARE(Color3::fromXyz({0.153122f, 0.0806478f, 0.512037f}).toSrgb(), + (Math::Vector3{95, 43, 192})); + CORRADE_COMPARE(Color3::fromXyz({0.153122f, 0.0806478f, 0.512037f}), + (Color3{0.11697f, 0.0241579f, 0.533276f})); + + /* Extremes -- for black it should be zeros, for white roughly X = 0.95, + Y = 1, Z = 1.09 corresponding to white point in D65 */ + CORRADE_COMPARE((Color3{0.0f, 0.0f, 0.0f}).toXyz(), + (Vector3{0.0f, 0.0f, 0.0f})); + CORRADE_COMPARE((Color3{1.0f, 1.0f, 1.0f}).toXyz(), + (Vector3{0.950456f, 1.0f, 1.08906f})); + + /* RGBA */ + CORRADE_COMPARE(Color4::fromXyz({0.454279f, 0.413092f, 0.0607124f}, 0.175f), + (Color4{0.806952f, 0.337163f, 0.0051861f, 0.175f})); + CORRADE_COMPARE(Color4::fromSrgb({232, 157, 16}, 0.175f).toXyz(), + (Vector3{0.454279f, 0.413092f, 0.0607124f})); + + /* Integral -- slight precision loss */ + CORRADE_COMPARE(Math::Color3::fromXyz({0.454279f, 0.413092f, 0.0607124f}), + (Math::Color3{52883, 22095, 339})); + CORRADE_COMPARE(Math::Color4::fromXyz({0.454279f, 0.413092f, 0.0607124f}, 15299), + (Math::Color4{52883, 22095, 339, 15299})); + CORRADE_COMPARE((Math::Color3{52883, 22095, 339}).toXyz(), + (Vector3{0.454268f, 0.413079f, 0.0607021f})); + CORRADE_COMPARE((Math::Color4{52883, 22095, 339, 15299}).toXyz(), + (Vector3{0.454268f, 0.413079f, 0.0607021f})); + + /* Round-trip */ + CORRADE_COMPARE(Color3::fromXyz({0.454279f, 0.413092f, 0.0607124f}).toXyz(), + (Vector3{0.454279f, 0.413092f, 0.0607124f})); + CORRADE_COMPARE(Color3::fromXyz({0.153122f, 0.0806478f, 0.512037f}).toXyz(), + (Vector3{0.153122f, 0.0806478f, 0.512037f})); + CORRADE_COMPARE(Color4::fromXyz({0.454279f, 0.413092f, 0.0607124f}, 0.175f).toXyz(), + (Vector3{0.454279f, 0.413092f, 0.0607124f})); +} + +void ColorTest::fromXyzDefaultAlpha() { + CORRADE_COMPARE(Color4::fromXyz({0.454279f, 0.413092f, 0.0607124f}), + (Color4{0.806952f, 0.337163f, 0.0051861f, 1.0f})); + + /* Integral */ + CORRADE_COMPARE(Math::Color4::fromXyz({0.454279f, 0.413092f, 0.0607124f}), + (Math::Color4{52883, 22095, 339, 65535})); +} + void ColorTest::swizzleType() { constexpr Color3 origColor3; constexpr Color4ub origColor4;