Commit f58059d84cbc28079785d8fa92e5ef4d9940b50b
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.
Showing
3 changed files
with
102 additions
and
17 deletions
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& 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& 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 | } | ... | ... |