Commit f58059d84cbc28079785d8fa92e5ef4d9940b50b

Authored by Jojo-1000
Committed by Moritz Wirger
1 parent 77148179

Change brightness of XYBrightness from Y to max(r,g,b).

As a result, conversion from XY to RGB is no longer exact, but the lights' brightness matches expectations better.
include/hueplusplus/ColorUnits.h
@@ -58,6 +58,7 @@ struct XY @@ -58,6 +58,7 @@ struct XY
58 //! \brief Color and brightness in CIE 58 //! \brief Color and brightness in CIE
59 //! 59 //!
60 //! The brightness is needed to convert back to RGB colors if necessary. 60 //! The brightness is needed to convert back to RGB colors if necessary.
  61 +//! \note brightness is not the actual luminance of the color, but instead the brightness the light is set to.
61 struct XYBrightness 62 struct XYBrightness
62 { 63 {
63 //! \brief XY color 64 //! \brief XY color
@@ -128,12 +129,16 @@ struct RGB @@ -128,12 +129,16 @@ struct RGB
128 //! \brief Create from XYBrightness 129 //! \brief Create from XYBrightness
129 //! 130 //!
130 //! Performs gamma correction so the light color matches the screen color better. 131 //! Performs gamma correction so the light color matches the screen color better.
  132 + //! \note The conversion formula is not exact, it can be off by up to 9 for each channel.
  133 + //! This is because the color luminosity is not saved.
131 static RGB fromXY(const XYBrightness& xy); 134 static RGB fromXY(const XYBrightness& xy);
132 //! \brief Create from XYBrightness and clip to \c gamut 135 //! \brief Create from XYBrightness and clip to \c gamut
133 //! 136 //!
134 //! A light may have XY set out of its range. Then this function returns the actual color 137 //! A light may have XY set out of its range. Then this function returns the actual color
135 //! the light shows rather than what it is set to. 138 //! the light shows rather than what it is set to.
136 //! Performs gamma correction so the light color matches the screen color better. 139 //! Performs gamma correction so the light color matches the screen color better.
  140 + //! \note The conversion formula is not exact, it can be off by up to 9 for each channel.
  141 + //! This is because the color luminosity is not saved.
137 static RGB fromXY(const XYBrightness& xy, const ColorGamut& gamut); 142 static RGB fromXY(const XYBrightness& xy, const ColorGamut& gamut);
138 }; 143 };
139 } // namespace hueplusplus 144 } // namespace hueplusplus
src/ColorUnits.cpp
@@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
19 along with hueplusplus. If not, see <http://www.gnu.org/licenses/>. 19 along with hueplusplus. If not, see <http://www.gnu.org/licenses/>.
20 **/ 20 **/
21 21
  22 +#include <algorithm>
22 #include <cmath> 23 #include <cmath>
23 24
24 #include <hueplusplus/ColorUnits.h> 25 #include <hueplusplus/ColorUnits.h>
@@ -109,7 +110,8 @@ XYBrightness RGB::toXY() const @@ -109,7 +110,8 @@ XYBrightness RGB::toXY() const
109 { 110 {
110 if (r == 0 && g == 0 && b == 0) 111 if (r == 0 && g == 0 && b == 0)
111 { 112 {
112 - return XYBrightness {XY {0.f, 0.f}, 0.f}; 113 + // Return white with minimum brightness
  114 + return XYBrightness {XY {0.32272673f, 0.32902291f}, 0.f};
113 } 115 }
114 const float red = r / 255.f; 116 const float red = r / 255.f;
115 const float green = g / 255.f; 117 const float green = g / 255.f;
@@ -125,7 +127,9 @@ XYBrightness RGB::toXY() const @@ -125,7 +127,9 @@ XYBrightness RGB::toXY() const
125 127
126 const float x = X / (X + Y + Z); 128 const float x = X / (X + Y + Z);
127 const float y = Y / (X + Y + Z); 129 const float y = Y / (X + Y + Z);
128 - return XYBrightness {XY {x, y}, Y}; 130 + // Set brightness to the brightest channel value (rather than average of them),
  131 + // so full red/green/blue can be displayed
  132 + return XYBrightness {XY {x, y}, std::max({red, green, blue})};
129 } 133 }
130 134
131 XYBrightness RGB::toXY(const ColorGamut& gamut) const 135 XYBrightness RGB::toXY(const ColorGamut& gamut) const
@@ -140,8 +144,19 @@ XYBrightness RGB::toXY(const ColorGamut&amp; gamut) const @@ -140,8 +144,19 @@ XYBrightness RGB::toXY(const ColorGamut&amp; gamut) const
140 144
141 RGB RGB::fromXY(const XYBrightness& xy) 145 RGB RGB::fromXY(const XYBrightness& xy)
142 { 146 {
  147 + if (xy.brightness < 1e-4)
  148 + {
  149 + return RGB{ 0,0,0 };
  150 + }
143 const float z = 1.f - xy.xy.x - xy.xy.y; 151 const float z = 1.f - xy.xy.x - xy.xy.y;
144 - const float Y = xy.brightness; 152 + // use a fixed luminosity and rescale the resulting rgb values using brightness
  153 + // randomly sampled conversions shown a minimum difference between original values
  154 + // and values after rgb -> xy -> rgb conversion for Y = 0.3
  155 + // (r-r')^2, (g-g')^2, (b-b')^2:
  156 + // 4.48214, 4.72039, 3.12141
  157 + // Max. Difference:
  158 + // 9, 9, 8
  159 + const float Y = 0.3f;
145 const float X = (Y / xy.xy.y) * xy.xy.x; 160 const float X = (Y / xy.xy.y) * xy.xy.x;
146 const float Z = (Y / xy.xy.y) * z; 161 const float Z = (Y / xy.xy.y) * z;
147 162
@@ -154,8 +169,20 @@ RGB RGB::fromXY(const XYBrightness&amp; xy) @@ -154,8 +169,20 @@ RGB RGB::fromXY(const XYBrightness&amp; xy)
154 const float gammaG = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f; 169 const float gammaG = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f;
155 const float gammaB = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f; 170 const float gammaB = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f;
156 171
157 - return RGB {static_cast<uint8_t>(std::round(gammaR * 255.f)), static_cast<uint8_t>(std::round(gammaG * 255.f)),  
158 - static_cast<uint8_t>(std::round(gammaB * 255.f))}; 172 + // Scale color values so that the brightness matches
  173 + const float maxColor = std::max({gammaR, gammaG, gammaB});
  174 + if (maxColor < 1e-4)
  175 + {
  176 + // Low color values, out of gamut?
  177 + return RGB {0, 0, 0};
  178 + }
  179 + const float rScaled = gammaR / maxColor * xy.brightness * 255.f;
  180 + const float gScaled = gammaG / maxColor * xy.brightness * 255.f;
  181 + const float bScaled = gammaB / maxColor * xy.brightness * 255.f;
  182 +
  183 + return RGB {static_cast<uint8_t>(std::round(std::max(0.f, rScaled))),
  184 + static_cast<uint8_t>(std::round(std::max(0.f, gScaled))),
  185 + static_cast<uint8_t>(std::round(std::max(0.f, bScaled)))};
159 } 186 }
160 187
161 RGB RGB::fromXY(const XYBrightness& xy, const ColorGamut& gamut) 188 RGB RGB::fromXY(const XYBrightness& xy, const ColorGamut& gamut)
test/test_ColorUnits.cpp
@@ -19,6 +19,8 @@ @@ -19,6 +19,8 @@
19 along with hueplusplus. If not, see <http://www.gnu.org/licenses/>. 19 along with hueplusplus. If not, see <http://www.gnu.org/licenses/>.
20 **/ 20 **/
21 21
  22 +#include <random>
  23 +
22 #include <hueplusplus/ColorUnits.h> 24 #include <hueplusplus/ColorUnits.h>
23 25
24 #include <gtest/gtest.h> 26 #include <gtest/gtest.h>
@@ -75,41 +77,41 @@ TEST(RGB, toXY) @@ -75,41 +77,41 @@ TEST(RGB, toXY)
75 XYBrightness xy = red.toXY(); 77 XYBrightness xy = red.toXY();
76 EXPECT_FLOAT_EQ(xy.xy.x, 0.70060623f); 78 EXPECT_FLOAT_EQ(xy.xy.x, 0.70060623f);
77 EXPECT_FLOAT_EQ(xy.xy.y, 0.299301f); 79 EXPECT_FLOAT_EQ(xy.xy.y, 0.299301f);
78 - EXPECT_FLOAT_EQ(xy.brightness, 0.28388101f); 80 + EXPECT_FLOAT_EQ(xy.brightness, 1.f);
79 } 81 }
80 { 82 {
81 const RGB red {255, 0, 0}; 83 const RGB red {255, 0, 0};
82 XYBrightness xy = red.toXY(gamut::gamutC); 84 XYBrightness xy = red.toXY(gamut::gamutC);
83 EXPECT_FLOAT_EQ(xy.xy.x, 0.69557756f); 85 EXPECT_FLOAT_EQ(xy.xy.x, 0.69557756f);
84 EXPECT_FLOAT_EQ(xy.xy.y, 0.30972576f); 86 EXPECT_FLOAT_EQ(xy.xy.y, 0.30972576f);
85 - EXPECT_FLOAT_EQ(xy.brightness, 0.28388101f); 87 + EXPECT_FLOAT_EQ(xy.brightness, 1.f);
86 } 88 }
87 { 89 {
88 const RGB white {255, 255, 255}; 90 const RGB white {255, 255, 255};
89 XYBrightness xy = white.toXY(); 91 XYBrightness xy = white.toXY();
90 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f); 92 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f);
91 EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f); 93 EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f);
92 - EXPECT_FLOAT_EQ(xy.brightness, 0.99999905f); 94 + EXPECT_FLOAT_EQ(xy.brightness, 1.f);
93 } 95 }
94 { 96 {
95 const RGB white {255, 255, 255}; 97 const RGB white {255, 255, 255};
96 XYBrightness xy = white.toXY(gamut::gamutA); 98 XYBrightness xy = white.toXY(gamut::gamutA);
97 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f); 99 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f);
98 EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f); 100 EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f);
99 - EXPECT_FLOAT_EQ(xy.brightness, 0.99999905f); 101 + EXPECT_FLOAT_EQ(xy.brightness, 1.f);
100 } 102 }
101 { 103 {
102 const RGB white {255, 255, 255}; 104 const RGB white {255, 255, 255};
103 XYBrightness xy = white.toXY(gamut::gamutB); 105 XYBrightness xy = white.toXY(gamut::gamutB);
104 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f); 106 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f);
105 EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f); 107 EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f);
106 - EXPECT_FLOAT_EQ(xy.brightness, 0.99999905f); 108 + EXPECT_FLOAT_EQ(xy.brightness, 1.f);
107 } 109 }
108 { 110 {
109 const RGB black{ 0,0,0 }; 111 const RGB black{ 0,0,0 };
110 XYBrightness xy = black.toXY(gamut::maxGamut); 112 XYBrightness xy = black.toXY(gamut::maxGamut);
111 - EXPECT_FLOAT_EQ(xy.xy.x, 0.0f);  
112 - EXPECT_FLOAT_EQ(xy.xy.y, 0.0f); 113 + EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f);
  114 + EXPECT_FLOAT_EQ(xy.xy.y, 0.32902291f);
113 EXPECT_FLOAT_EQ(xy.brightness, 0.0f); 115 EXPECT_FLOAT_EQ(xy.brightness, 0.0f);
114 } 116 }
115 } 117 }
@@ -117,7 +119,7 @@ TEST(RGB, toXY) @@ -117,7 +119,7 @@ TEST(RGB, toXY)
117 TEST(RGB, fromXY) 119 TEST(RGB, fromXY)
118 { 120 {
119 { 121 {
120 - const XYBrightness xyRed {{0.70060623f, 0.299301f}, 0.28388101f}; 122 + const XYBrightness xyRed {{0.70060623f, 0.299301f}, 1.f};
121 const RGB red = RGB::fromXY(xyRed); 123 const RGB red = RGB::fromXY(xyRed);
122 EXPECT_EQ(255, red.r); 124 EXPECT_EQ(255, red.r);
123 EXPECT_EQ(0, red.g); 125 EXPECT_EQ(0, red.g);
@@ -128,10 +130,61 @@ TEST(RGB, fromXY) @@ -128,10 +130,61 @@ TEST(RGB, fromXY)
128 EXPECT_FLOAT_EQ(xyRed.brightness, reversed.brightness); 130 EXPECT_FLOAT_EQ(xyRed.brightness, reversed.brightness);
129 } 131 }
130 { 132 {
131 - const XYBrightness xyRed {{0.70060623f, 0.299301f}, 0.28388101f}; 133 + const XYBrightness xyWhite{ {0.32272673f, 0.32902291f}, 1.f };
  134 + const RGB white = RGB::fromXY(xyWhite);
  135 + EXPECT_EQ(255, white.r);
  136 + EXPECT_EQ(255, white.g);
  137 + EXPECT_EQ(255, white.b);
  138 + const XYBrightness reversed = white.toXY();
  139 + EXPECT_FLOAT_EQ(xyWhite.xy.x, reversed.xy.x);
  140 + EXPECT_FLOAT_EQ(xyWhite.xy.y, reversed.xy.y);
  141 + EXPECT_FLOAT_EQ(xyWhite.brightness, reversed.brightness);
  142 + }
  143 + {
  144 + const XYBrightness xyRed {{0.70060623f, 0.299301f}, 1.f};
132 const RGB red = RGB::fromXY(xyRed, gamut::gamutB); 145 const RGB red = RGB::fromXY(xyRed, gamut::gamutB);
133 - EXPECT_EQ(242, red.r);  
134 - EXPECT_EQ(63, red.g);  
135 - EXPECT_EQ(208, red.b); 146 + const RGB red2 = RGB::fromXY({ gamut::gamutB.corrected(xyRed.xy), xyRed.brightness });
  147 + EXPECT_EQ(red2.r, red.r);
  148 + EXPECT_EQ(red2.g, red.g);
  149 + EXPECT_EQ(red2.b, red.b);
136 } 150 }
  151 +
  152 + // Statistical tests of conversion accuracy
  153 + // Fixed seed so the tests dont fail randomly
  154 + std::mt19937 rng {12374682};
  155 + std::uniform_int_distribution<int> dist(0, 255);
  156 +
  157 + uint64_t N = 1000;
  158 +
  159 + uint64_t totalDiffR = 0;
  160 + uint64_t totalDiffG = 0;
  161 + uint64_t totalDiffB = 0;
  162 + int maxDiffR = 0;
  163 + int maxDiffG = 0;
  164 + int maxDiffB = 0;
  165 + for (int i = 0; i < N; ++i)
  166 + {
  167 + const RGB rgb {dist(rng), dist(rng), dist(rng)};
  168 + const XYBrightness xy = rgb.toXY();
  169 + const RGB back = RGB::fromXY(xy);
  170 + int diffR = (rgb.r - back.r) * (rgb.r - back.r);
  171 + int diffG = (rgb.g - back.g) * (rgb.g - back.g);
  172 + int diffB = (rgb.b - back.b) * (rgb.b - back.b);
  173 + totalDiffR += diffR;
  174 + totalDiffG += diffG;
  175 + totalDiffB += diffB;
  176 + maxDiffR = std::max(diffR, maxDiffR);
  177 + maxDiffG = std::max(diffG, maxDiffG);
  178 + maxDiffB = std::max(diffB, maxDiffB);
  179 + }
  180 + float varR = (float)totalDiffR / N;
  181 + float varG = (float)totalDiffG / N;
  182 + float varB = (float)totalDiffB / N;
  183 + EXPECT_LT(varR, 5.f);
  184 + EXPECT_LT(varG, 5.f);
  185 + EXPECT_LT(varB, 4.f);
  186 + EXPECT_LE(maxDiffR, 81);
  187 + EXPECT_LE(maxDiffG, 81);
  188 + EXPECT_LE(maxDiffB, 64);
  189 +
137 } 190 }