From f58059d84cbc28079785d8fa92e5ef4d9940b50b Mon Sep 17 00:00:00 2001 From: Jojo-1000 <33495614+Jojo-1000@users.noreply.github.com> Date: Sat, 16 Jan 2021 21:12:10 +0100 Subject: [PATCH] Change brightness of XYBrightness from Y to max(r,g,b). --- include/hueplusplus/ColorUnits.h | 5 +++++ src/ColorUnits.cpp | 37 ++++++++++++++++++++++++++++++++----- test/test_ColorUnits.cpp | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 102 insertions(+), 17 deletions(-) diff --git a/include/hueplusplus/ColorUnits.h b/include/hueplusplus/ColorUnits.h index 6e39610..184da96 100644 --- a/include/hueplusplus/ColorUnits.h +++ b/include/hueplusplus/ColorUnits.h @@ -58,6 +58,7 @@ struct XY //! \brief Color and brightness in CIE //! //! The brightness is needed to convert back to RGB colors if necessary. +//! \note brightness is not the actual luminance of the color, but instead the brightness the light is set to. struct XYBrightness { //! \brief XY color @@ -128,12 +129,16 @@ struct RGB //! \brief Create from XYBrightness //! //! Performs gamma correction so the light color matches the screen color better. + //! \note The conversion formula is not exact, it can be off by up to 9 for each channel. + //! This is because the color luminosity is not saved. static RGB fromXY(const XYBrightness& xy); //! \brief Create from XYBrightness and clip to \c gamut //! //! A light may have XY set out of its range. Then this function returns the actual color //! the light shows rather than what it is set to. //! Performs gamma correction so the light color matches the screen color better. + //! \note The conversion formula is not exact, it can be off by up to 9 for each channel. + //! This is because the color luminosity is not saved. static RGB fromXY(const XYBrightness& xy, const ColorGamut& gamut); }; } // namespace hueplusplus diff --git a/src/ColorUnits.cpp b/src/ColorUnits.cpp index 6bace3e..adc3c4a 100644 --- a/src/ColorUnits.cpp +++ b/src/ColorUnits.cpp @@ -19,6 +19,7 @@ along with hueplusplus. If not, see . **/ +#include #include #include @@ -109,7 +110,8 @@ XYBrightness RGB::toXY() const { if (r == 0 && g == 0 && b == 0) { - return XYBrightness {XY {0.f, 0.f}, 0.f}; + // Return white with minimum brightness + return XYBrightness {XY {0.32272673f, 0.32902291f}, 0.f}; } const float red = r / 255.f; const float green = g / 255.f; @@ -125,7 +127,9 @@ XYBrightness RGB::toXY() const const float x = X / (X + Y + Z); const float y = Y / (X + Y + Z); - return XYBrightness {XY {x, y}, Y}; + // Set brightness to the brightest channel value (rather than average of them), + // so full red/green/blue can be displayed + return XYBrightness {XY {x, y}, std::max({red, green, blue})}; } XYBrightness RGB::toXY(const ColorGamut& gamut) const @@ -140,8 +144,19 @@ XYBrightness RGB::toXY(const ColorGamut& gamut) const RGB RGB::fromXY(const XYBrightness& xy) { + if (xy.brightness < 1e-4) + { + return RGB{ 0,0,0 }; + } const float z = 1.f - xy.xy.x - xy.xy.y; - const float Y = xy.brightness; + // use a fixed luminosity and rescale the resulting rgb values using brightness + // randomly sampled conversions shown a minimum difference between original values + // and values after rgb -> xy -> rgb conversion for Y = 0.3 + // (r-r')^2, (g-g')^2, (b-b')^2: + // 4.48214, 4.72039, 3.12141 + // Max. Difference: + // 9, 9, 8 + const float Y = 0.3f; const float X = (Y / xy.xy.y) * xy.xy.x; const float Z = (Y / xy.xy.y) * z; @@ -154,8 +169,20 @@ RGB RGB::fromXY(const XYBrightness& xy) const float gammaG = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f; const float gammaB = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f; - return RGB {static_cast(std::round(gammaR * 255.f)), static_cast(std::round(gammaG * 255.f)), - static_cast(std::round(gammaB * 255.f))}; + // Scale color values so that the brightness matches + const float maxColor = std::max({gammaR, gammaG, gammaB}); + if (maxColor < 1e-4) + { + // Low color values, out of gamut? + return RGB {0, 0, 0}; + } + const float rScaled = gammaR / maxColor * xy.brightness * 255.f; + const float gScaled = gammaG / maxColor * xy.brightness * 255.f; + const float bScaled = gammaB / maxColor * xy.brightness * 255.f; + + return RGB {static_cast(std::round(std::max(0.f, rScaled))), + static_cast(std::round(std::max(0.f, gScaled))), + static_cast(std::round(std::max(0.f, bScaled)))}; } RGB RGB::fromXY(const XYBrightness& xy, const ColorGamut& gamut) diff --git a/test/test_ColorUnits.cpp b/test/test_ColorUnits.cpp index 4fcf29e..1af4809 100644 --- a/test/test_ColorUnits.cpp +++ b/test/test_ColorUnits.cpp @@ -19,6 +19,8 @@ along with hueplusplus. If not, see . **/ +#include + #include #include @@ -75,41 +77,41 @@ TEST(RGB, toXY) XYBrightness xy = red.toXY(); EXPECT_FLOAT_EQ(xy.xy.x, 0.70060623f); EXPECT_FLOAT_EQ(xy.xy.y, 0.299301f); - EXPECT_FLOAT_EQ(xy.brightness, 0.28388101f); + EXPECT_FLOAT_EQ(xy.brightness, 1.f); } { const RGB red {255, 0, 0}; XYBrightness xy = red.toXY(gamut::gamutC); EXPECT_FLOAT_EQ(xy.xy.x, 0.69557756f); EXPECT_FLOAT_EQ(xy.xy.y, 0.30972576f); - EXPECT_FLOAT_EQ(xy.brightness, 0.28388101f); + EXPECT_FLOAT_EQ(xy.brightness, 1.f); } { const RGB white {255, 255, 255}; XYBrightness xy = white.toXY(); EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f); EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f); - EXPECT_FLOAT_EQ(xy.brightness, 0.99999905f); + EXPECT_FLOAT_EQ(xy.brightness, 1.f); } { const RGB white {255, 255, 255}; XYBrightness xy = white.toXY(gamut::gamutA); EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f); EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f); - EXPECT_FLOAT_EQ(xy.brightness, 0.99999905f); + EXPECT_FLOAT_EQ(xy.brightness, 1.f); } { const RGB white {255, 255, 255}; XYBrightness xy = white.toXY(gamut::gamutB); EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f); EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f); - EXPECT_FLOAT_EQ(xy.brightness, 0.99999905f); + EXPECT_FLOAT_EQ(xy.brightness, 1.f); } { const RGB black{ 0,0,0 }; XYBrightness xy = black.toXY(gamut::maxGamut); - EXPECT_FLOAT_EQ(xy.xy.x, 0.0f); - EXPECT_FLOAT_EQ(xy.xy.y, 0.0f); + EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f); + EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f); EXPECT_FLOAT_EQ(xy.brightness, 0.0f); } } @@ -117,7 +119,7 @@ TEST(RGB, toXY) TEST(RGB, fromXY) { { - const XYBrightness xyRed {{0.70060623f, 0.299301f}, 0.28388101f}; + const XYBrightness xyRed {{0.70060623f, 0.299301f}, 1.f}; const RGB red = RGB::fromXY(xyRed); EXPECT_EQ(255, red.r); EXPECT_EQ(0, red.g); @@ -128,10 +130,61 @@ TEST(RGB, fromXY) EXPECT_FLOAT_EQ(xyRed.brightness, reversed.brightness); } { - const XYBrightness xyRed {{0.70060623f, 0.299301f}, 0.28388101f}; + const XYBrightness xyWhite{ {0.32272673f, 0.32902291f}, 1.f }; + const RGB white = RGB::fromXY(xyWhite); + EXPECT_EQ(255, white.r); + EXPECT_EQ(255, white.g); + EXPECT_EQ(255, white.b); + const XYBrightness reversed = white.toXY(); + EXPECT_FLOAT_EQ(xyWhite.xy.x, reversed.xy.x); + EXPECT_FLOAT_EQ(xyWhite.xy.y, reversed.xy.y); + EXPECT_FLOAT_EQ(xyWhite.brightness, reversed.brightness); + } + { + const XYBrightness xyRed {{0.70060623f, 0.299301f}, 1.f}; const RGB red = RGB::fromXY(xyRed, gamut::gamutB); - EXPECT_EQ(242, red.r); - EXPECT_EQ(63, red.g); - EXPECT_EQ(208, red.b); + const RGB red2 = RGB::fromXY({ gamut::gamutB.corrected(xyRed.xy), xyRed.brightness }); + EXPECT_EQ(red2.r, red.r); + EXPECT_EQ(red2.g, red.g); + EXPECT_EQ(red2.b, red.b); } + + // Statistical tests of conversion accuracy + // Fixed seed so the tests dont fail randomly + std::mt19937 rng {12374682}; + std::uniform_int_distribution dist(0, 255); + + uint64_t N = 1000; + + uint64_t totalDiffR = 0; + uint64_t totalDiffG = 0; + uint64_t totalDiffB = 0; + int maxDiffR = 0; + int maxDiffG = 0; + int maxDiffB = 0; + for (int i = 0; i < N; ++i) + { + const RGB rgb {dist(rng), dist(rng), dist(rng)}; + const XYBrightness xy = rgb.toXY(); + const RGB back = RGB::fromXY(xy); + int diffR = (rgb.r - back.r) * (rgb.r - back.r); + int diffG = (rgb.g - back.g) * (rgb.g - back.g); + int diffB = (rgb.b - back.b) * (rgb.b - back.b); + totalDiffR += diffR; + totalDiffG += diffG; + totalDiffB += diffB; + maxDiffR = std::max(diffR, maxDiffR); + maxDiffG = std::max(diffG, maxDiffG); + maxDiffB = std::max(diffB, maxDiffB); + } + float varR = (float)totalDiffR / N; + float varG = (float)totalDiffG / N; + float varB = (float)totalDiffB / N; + EXPECT_LT(varR, 5.f); + EXPECT_LT(varG, 5.f); + EXPECT_LT(varB, 4.f); + EXPECT_LE(maxDiffR, 81); + EXPECT_LE(maxDiffG, 81); + EXPECT_LE(maxDiffB, 64); + } -- libgit2 0.21.4