Browse Source

Math: function to convert RGB to XYZ color space.

Building blocks for supporting other colorspaces such as L*a*b. I was
not happy with matrices from Wikipedia because they don't round-trip
perfectly so I have slightly different / more precise versions that do
round-trip.
pull/190/head
Vladimír Vondruš 10 years ago
parent
commit
ce050888fd
  1. 101
      src/Magnum/Math/Color.h
  2. 67
      src/Magnum/Math/Test/ColorTest.cpp

101
src/Magnum/Math/Color.h

@ -170,6 +170,32 @@ template<class T, class Integral> inline Vector4<Integral> toSrgbAlphaIntegral(c
return denormalize<Vector4<Integral>>(toSrgbAlpha<T>(rgba));
}
/* CIE XYZ -> RGB conversion */
template<class T> typename std::enable_if<std::is_floating_point<T>::value, Color3<T>>::type fromXyz(const Vector3<T>& 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<T>{
Vector3<T>{T(12831)/T(3959), T(-851781)/T(878810), T(705)/T(12673)},
Vector3<T>{T(-329)/T(214), T(1648619)/T(878810), T(-2585)/T(12673)},
Vector3<T>{T(-1974)/T(3959), T(36519)/T(878810), T(705)/T(667)}}*xyz;
}
template<class T> inline typename std::enable_if<std::is_integral<T>::value, Color3<T>>::type fromXyz(const Vector3<typename Color3<T>::FloatingPointType>& xyz) {
return denormalize<Color3<T>>(fromXyz<typename Color3<T>::FloatingPointType>(xyz));
}
/* RGB -> CIE XYZ conversion */
template<class T> Vector3<typename Color3<T>::FloatingPointType> toXyz(typename std::enable_if<std::is_floating_point<T>::value, const Color3<T>&>::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<T>{
Vector3<T>{T(506752)/T(1228815), T(87098)/T(409605), T(7918)/T(409605)},
Vector3<T>{T(87881)/T(245763), T(175762)/T(245763), T(87881)/T(737289)},
Vector3<T>{T(12673)/T(70218), T(12673)/T(175545), T(1001167)/T(1053270)}})*rgb;
}
template<class T> inline Vector3<typename Color3<T>::FloatingPointType> toXyz(typename std::enable_if<std::is_integral<T>::value, const Color3<T>&>::type rgb) {
return toXyz<typename Color3<T>::FloatingPointType>(normalize<Color3<typename Color3<T>::FloatingPointType>>(rgb));
}
/* Value for full channel (1.0f for floats, 255 for unsigned byte) */
template<class T> constexpr typename std::enable_if<std::is_floating_point<T>::value, T>::type fullChannel() {
return T(1);
@ -366,6 +392,26 @@ template<class T> class Color3: public Vector3<T> {
return Implementation::fromSrgbIntegral<T, Integral>(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<T> fromXyz(const Vector3<FloatingPointType>& xyz) {
return Implementation::fromXyz<T>(xyz);
}
/**
* @brief Default constructor
*
@ -508,6 +554,30 @@ template<class T> class Color3: public Vector3<T> {
return Implementation::toSrgbIntegral<T, Integral>(*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<FloatingPointType> toXyz() const {
return Implementation::toXyz<T>(*this);
}
MAGNUM_VECTOR_SUBCLASS_IMPLEMENTATION(3, Color3)
};
@ -708,6 +778,20 @@ class Color4: public Vector4<T> {
return {Implementation::fromSrgbIntegral<T, Integral>(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<T> fromXyz(const Vector3<FloatingPointType> xyz, T a = Implementation::fullChannel<T>()) {
return {Implementation::fromXyz<T>(xyz), a};
}
/**
* @brief Default constructor
*
@ -849,6 +933,23 @@ class Color4: public Vector4<T> {
return Implementation::toSrgbAlphaIntegral<T, Integral>(*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<FloatingPointType> toXyz() const {
return Implementation::toXyz<T>(Vector4<T>::rgb());
}
MAGNUM_VECTOR_SUBCLASS_IMPLEMENTATION(4, Color4)
};

67
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<UnsignedByte>({232, 157, 16}).toXyz(),
(Vector3{0.454279f, 0.413092f, 0.0607124f}));
CORRADE_COMPARE(Color3::fromXyz({0.454279f, 0.413092f, 0.0607124f}).toSrgb<UnsignedByte>(),
(Math::Vector3<UnsignedByte>{231, 156, 16}));
CORRADE_COMPARE(Color3::fromXyz({0.454279f, 0.413092f, 0.0607124f}),
(Color3{0.806952f, 0.337163f, 0.0051861f}));
CORRADE_COMPARE(Color3::fromSrgb<UnsignedByte>({96, 43, 193}).toXyz(),
(Vector3{0.153122f, 0.0806478f, 0.512037f}));
CORRADE_COMPARE(Color3::fromXyz({0.153122f, 0.0806478f, 0.512037f}).toSrgb<UnsignedByte>(),
(Math::Vector3<UnsignedByte>{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<UnsignedByte>({232, 157, 16}, 0.175f).toXyz(),
(Vector3{0.454279f, 0.413092f, 0.0607124f}));
/* Integral -- slight precision loss */
CORRADE_COMPARE(Math::Color3<UnsignedShort>::fromXyz({0.454279f, 0.413092f, 0.0607124f}),
(Math::Color3<UnsignedShort>{52883, 22095, 339}));
CORRADE_COMPARE(Math::Color4<UnsignedShort>::fromXyz({0.454279f, 0.413092f, 0.0607124f}, 15299),
(Math::Color4<UnsignedShort>{52883, 22095, 339, 15299}));
CORRADE_COMPARE((Math::Color3<UnsignedShort>{52883, 22095, 339}).toXyz(),
(Vector3{0.454268f, 0.413079f, 0.0607021f}));
CORRADE_COMPARE((Math::Color4<UnsignedShort>{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<UnsignedShort>::fromXyz({0.454279f, 0.413092f, 0.0607124f}),
(Math::Color4<UnsignedShort>{52883, 22095, 339, 65535}));
}
void ColorTest::swizzleType() {
constexpr Color3 origColor3;
constexpr Color4ub origColor4;

Loading…
Cancel
Save