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 58 //! \brief Color and brightness in CIE
59 59 //!
60 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 62 struct XYBrightness
62 63 {
63 64 //! \brief XY color
... ... @@ -128,12 +129,16 @@ struct RGB
128 129 //! \brief Create from XYBrightness
129 130 //!
130 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 134 static RGB fromXY(const XYBrightness& xy);
132 135 //! \brief Create from XYBrightness and clip to \c gamut
133 136 //!
134 137 //! A light may have XY set out of its range. Then this function returns the actual color
135 138 //! the light shows rather than what it is set to.
136 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 142 static RGB fromXY(const XYBrightness& xy, const ColorGamut& gamut);
138 143 };
139 144 } // namespace hueplusplus
... ...
src/ColorUnits.cpp
... ... @@ -19,6 +19,7 @@
19 19 along with hueplusplus. If not, see <http://www.gnu.org/licenses/>.
20 20 **/
21 21  
  22 +#include <algorithm>
22 23 #include <cmath>
23 24  
24 25 #include <hueplusplus/ColorUnits.h>
... ... @@ -109,7 +110,8 @@ XYBrightness RGB::toXY() const
109 110 {
110 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 116 const float red = r / 255.f;
115 117 const float green = g / 255.f;
... ... @@ -125,7 +127,9 @@ XYBrightness RGB::toXY() const
125 127  
126 128 const float x = X / (X + Y + Z);
127 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 135 XYBrightness RGB::toXY(const ColorGamut& gamut) const
... ... @@ -140,8 +144,19 @@ XYBrightness RGB::toXY(const ColorGamut&amp; gamut) const
140 144  
141 145 RGB RGB::fromXY(const XYBrightness& xy)
142 146 {
  147 + if (xy.brightness < 1e-4)
  148 + {
  149 + return RGB{ 0,0,0 };
  150 + }
143 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 160 const float X = (Y / xy.xy.y) * xy.xy.x;
146 161 const float Z = (Y / xy.xy.y) * z;
147 162  
... ... @@ -154,8 +169,20 @@ RGB RGB::fromXY(const XYBrightness&amp; xy)
154 169 const float gammaG = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f;
155 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 188 RGB RGB::fromXY(const XYBrightness& xy, const ColorGamut& gamut)
... ...
test/test_ColorUnits.cpp
... ... @@ -19,6 +19,8 @@
19 19 along with hueplusplus. If not, see <http://www.gnu.org/licenses/>.
20 20 **/
21 21  
  22 +#include <random>
  23 +
22 24 #include <hueplusplus/ColorUnits.h>
23 25  
24 26 #include <gtest/gtest.h>
... ... @@ -75,41 +77,41 @@ TEST(RGB, toXY)
75 77 XYBrightness xy = red.toXY();
76 78 EXPECT_FLOAT_EQ(xy.xy.x, 0.70060623f);
77 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 83 const RGB red {255, 0, 0};
82 84 XYBrightness xy = red.toXY(gamut::gamutC);
83 85 EXPECT_FLOAT_EQ(xy.xy.x, 0.69557756f);
84 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 90 const RGB white {255, 255, 255};
89 91 XYBrightness xy = white.toXY();
90 92 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f);
91 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 97 const RGB white {255, 255, 255};
96 98 XYBrightness xy = white.toXY(gamut::gamutA);
97 99 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f);
98 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 104 const RGB white {255, 255, 255};
103 105 XYBrightness xy = white.toXY(gamut::gamutB);
104 106 EXPECT_FLOAT_EQ(xy.xy.x, 0.32272673f);
105 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 111 const RGB black{ 0,0,0 };
110 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 115 EXPECT_FLOAT_EQ(xy.brightness, 0.0f);
114 116 }
115 117 }
... ... @@ -117,7 +119,7 @@ TEST(RGB, toXY)
117 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 123 const RGB red = RGB::fromXY(xyRed);
122 124 EXPECT_EQ(255, red.r);
123 125 EXPECT_EQ(0, red.g);
... ... @@ -128,10 +130,61 @@ TEST(RGB, fromXY)
128 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 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 }
... ...