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