From 56f560c444d4a9dff44ea9b59b6f219801a9568d Mon Sep 17 00:00:00 2001 From: Nathan Hughes Date: Wed, 31 Jul 2024 19:13:08 +0000 Subject: [PATCH] add conversion to hsv and hls --- include/spark_dsg/color.h | 4 + src/color.cpp | 182 ++++++++++++++++++++++++++++---------- tests/utest_color.cpp | 67 +++++++++----- 3 files changed, 187 insertions(+), 66 deletions(-) diff --git a/include/spark_dsg/color.h b/include/spark_dsg/color.h index bd73b96..5fc8510 100644 --- a/include/spark_dsg/color.h +++ b/include/spark_dsg/color.h @@ -36,6 +36,7 @@ #include #include +#include #include namespace spark_dsg { @@ -99,8 +100,11 @@ struct Color { static Color random(); // Conversions. + std::array toUnitRange() const; static Color fromHSV(float hue, float saturation, float value); + std::array toHSV() const; static Color fromHLS(float hue, float luminance, float saturation); + std::array toHLS() const; // Color maps. /** diff --git a/src/color.cpp b/src/color.cpp index 39c450e..3795f12 100644 --- a/src/color.cpp +++ b/src/color.cpp @@ -36,6 +36,102 @@ std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dis(0, 255); +namespace { + +struct HueInfo { + float max = 0.0f; + float min = 1.0f; + float hue = 0.0; +}; + +std::array rgbFromChromaHue(float chroma, float hue) { + const float hue_prime = hue * 6.0f; + const float x = chroma * (1.0f - std::abs(std::fmod(hue_prime, 2) - 1.0f)); + if (hue_prime < 1) { + return {chroma, x, 0}; + } else if (hue_prime < 2) { + return {x, chroma, 0}; + } else if (hue_prime < 3) { + return {0, chroma, x}; + } else if (hue_prime < 4) { + return {0, x, chroma}; + } else if (hue_prime < 5) { + return {x, 0, chroma}; + } + return {chroma, 0, x}; +} + +// see https://docs.opencv.org/3.4/de/d25/imgproc_color_conversions.html for details +HueInfo getHue(const Color& color) { + // TODO(nathan) think about tie-breaking + std::array rgb{color.r / 255.0f, color.g / 255.0f, color.b / 255.0f}; + + HueInfo info; + size_t max_idx = 0; + for (size_t c = 0; c < 3; ++c) { + if (rgb[c] > info.max) { + info.max = rgb[c]; + max_idx = c; + } + + if (rgb[c] < info.min) { + info.min = rgb[c]; + } + } + + if (color.r == color.g && color.g == color.b) { + // maps to central axis, but we define it as 0.0 + info.hue = 0.0; + } else if (max_idx == 0 && color.r == color.g) { + // should be equidistant between red and green + info.hue = 1.0f / 6.0f; + } else if (max_idx == 0 && color.r == color.b) { + // should be equidistant between blue and red + info.hue = 5.0f / 6.0f; + } else if (max_idx == 1 && color.g == color.b) { + // should be equidistant between green and blue + info.hue = 0.5f; + } else { + const auto l_idx = (max_idx + 1) % 3; + const auto r_idx = (max_idx + 2) % 3; + const auto chroma = info.max - info.min; + info.hue = (rgb[l_idx] - rgb[r_idx]) / (2.0f * chroma) + max_idx; + info.hue /= 3.0; + } + + if (info.hue < 0.0f) { + info.hue += 1.0f; + } + + return info; +} + +/** + * @brief Map a potentially infinite number of ids to a never repeating pattern in [0, + * 1]. + */ +float exponentialOffsetId(size_t id, size_t ids_per_revolution) { + const size_t revolution = id / ids_per_revolution; + const float progress_along_revolution = + std::fmod(static_cast(id) / ids_per_revolution, 1.f); + float offset = 0.0f; + if (ids_per_revolution < id + 1u) { + const size_t current_episode = std::floor(std::log2(revolution)); + const size_t episode_start = std::exp2(current_episode); + const size_t current_subdivision = revolution - episode_start; + const float subdivision_step_size = 1.0f / (ids_per_revolution * 2 * episode_start); + offset = (2.0f * current_subdivision + 1) * subdivision_step_size; + } + return progress_along_revolution + offset; +} + +// remap unit-range color value to nearest uint8_t value +inline uint8_t fromUnitRange(float value) { + return static_cast(std::clamp(std::round(255.0f * value), 0.0f, 255.0f)); +} + +} // namespace + bool Color::operator==(const Color& other) const { return r == other.r && g == other.g && b == other.b && a == other.a; } @@ -65,21 +161,8 @@ Color Color::blend(const Color& other, float weight) const { Color Color::random() { return Color(dis(gen), dis(gen), dis(gen), dis(gen)); } -std::array rgbFromChromaHue(float chroma, float hue) { - const float hue_prime = hue * 6.0f; - const float x = chroma * (1.0f - std::abs(std::fmod(hue_prime, 2) - 1.0f)); - if (hue_prime < 1) { - return {chroma, x, 0}; - } else if (hue_prime < 2) { - return {x, chroma, 0}; - } else if (hue_prime < 3) { - return {0, chroma, x}; - } else if (hue_prime < 4) { - return {0, x, chroma}; - } else if (hue_prime < 5) { - return {x, 0, chroma}; - } - return {chroma, 0, x}; +std::array Color::toUnitRange() const { + return {r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f}; } Color Color::fromHSV(float hue, float saturation, float value) { @@ -89,9 +172,18 @@ Color Color::fromHSV(float hue, float saturation, float value) { const float chroma = value * saturation; const auto [r1, g1, b1] = rgbFromChromaHue(chroma, hue); const float m = value - chroma; - return Color(static_cast((r1 + m) * 255), - static_cast((g1 + m) * 255), - static_cast((b1 + m) * 255)); + return Color(fromUnitRange(r1 + m), fromUnitRange(g1 + m), fromUnitRange(b1 + m)); +} + +// see https://docs.opencv.org/3.4/de/d25/imgproc_color_conversions.html for details +std::array Color::toHSV() const { + const auto info = getHue(*this); + + std::array hsv; + hsv[0] = info.hue; + hsv[1] = info.max == 0.0f ? 0.0f : static_cast(info.max - info.min) / info.max; + hsv[2] = info.max; + return hsv; } Color Color::fromHLS(float hue, float luminance, float saturation) { @@ -101,28 +193,43 @@ Color Color::fromHLS(float hue, float luminance, float saturation) { const float chroma = (1.0f - std::abs(2.0f * luminance - 1.0f)) * saturation; const auto [r1, g1, b1] = rgbFromChromaHue(chroma, hue); const float m = luminance - chroma / 2.0f; - return Color(static_cast((r1 + m) * 255), - static_cast((g1 + m) * 255), - static_cast((b1 + m) * 255)); + return Color(fromUnitRange(r1 + m), fromUnitRange(g1 + m), fromUnitRange(b1 + m)); +} + +// see https://docs.opencv.org/3.4/de/d25/imgproc_color_conversions.html for details +std::array Color::toHLS() const { + const auto info = getHue(*this); + + std::array hls; + hls[0] = info.hue; + hls[1] = (info.max + info.min) / 2.0f; + if (hls[1] == 0.0f || hls[1] == 1.0f) { + hls[2] = 0.0f; + } else if (hls[1] < 0.5f) { + hls[2] = (info.max - info.min) / (info.max + info.min); + } else { + hls[2] = (info.max - info.min) / (2.0f - info.max - info.min); + } + + return hls; } Color Color::gray(float value) { - value = std::clamp(value, 0.0f, 1.0f); - return Color(static_cast(value * 255), - static_cast(value * 255), - static_cast(value * 255)); + const auto char_value = fromUnitRange(std::clamp(value, 0.0f, 1.0f)); + return Color(char_value, char_value, char_value); } Color Color::quality(float value) { value = std::clamp(value, 0.0f, 1.0f); Color color; if (value > 0.5f) { - color.r = (1.f - value) * 2 * 255; + color.r = fromUnitRange((1.0f - value) * 2.0f); color.g = 255; } else { color.r = 255; - color.g = value * 2 * 255; + color.g = fromUnitRange(value * 2.0f); } + return color; } @@ -130,12 +237,14 @@ Color Color::spectrum(float value, const std::vector& colors) { if (colors.empty()) { return Color::black(); } + value = std::clamp(value, 0.0f, 1.0f); const size_t num_steps = colors.size() - 1; const size_t index = static_cast(value * num_steps); if (index >= num_steps) { return colors.at(num_steps); } + const float weight = value * num_steps - index; return colors.at(index).blend(colors.at(index + 1), weight); } @@ -146,25 +255,6 @@ Color Color::rainbow(float value) { return fromHLS(std::clamp(value, 0.0f, 1.0f), 0.5f, 1.0f); } -/** - * @brief Map a potentially infinite number of ids to a never repeating pattern in [0, - * 1]. - */ -float exponentialOffsetId(size_t id, size_t ids_per_revolution) { - const size_t revolution = id / ids_per_revolution; - const float progress_along_revolution = - std::fmod(static_cast(id) / ids_per_revolution, 1.f); - float offset = 0.0f; - if (ids_per_revolution < id + 1u) { - const size_t current_episode = std::floor(std::log2(revolution)); - const size_t episode_start = std::exp2(current_episode); - const size_t current_subdivision = revolution - episode_start; - const float subdivision_step_size = 1.0f / (ids_per_revolution * 2 * episode_start); - offset = (2.0f * current_subdivision + 1) * subdivision_step_size; - } - return progress_along_revolution + offset; -} - Color Color::rainbowId(size_t id, size_t ids_per_revolution) { return Color::rainbow(exponentialOffsetId(id, ids_per_revolution)); } diff --git a/tests/utest_color.cpp b/tests/utest_color.cpp index 37e8c67..7475e0f 100644 --- a/tests/utest_color.cpp +++ b/tests/utest_color.cpp @@ -34,6 +34,7 @@ * -------------------------------------------------------------------------- */ #include +#include #include #include "spark_dsg/color.h" @@ -97,27 +98,27 @@ TEST(Color, Blend) { TEST(Color, Rainbow) { EXPECT_EQ(Color::rainbow(0), Color::red()); - EXPECT_EQ(Color::rainbow(0.1), Color(255,153,0)); - EXPECT_EQ(Color::rainbow(0.2), Color(203,255,0)); - EXPECT_EQ(Color::rainbow(0.3), Color(50,255,0)); - EXPECT_EQ(Color::rainbow(0.4), Color(0,255,102)); + EXPECT_EQ(Color::rainbow(0.1), Color(255, 153, 0)); + EXPECT_EQ(Color::rainbow(0.2), Color(204, 255, 0)); + EXPECT_EQ(Color::rainbow(0.3), Color(51, 255, 0)); + EXPECT_EQ(Color::rainbow(0.4), Color(0, 255, 102)); EXPECT_EQ(Color::rainbow(0.5), Color::cyan()); - EXPECT_EQ(Color::rainbow(0.6), Color(0,101,255)); - EXPECT_EQ(Color::rainbow(0.7), Color(50,0,255)); - EXPECT_EQ(Color::rainbow(0.8), Color(204,0,255)); - EXPECT_EQ(Color::rainbow(0.9), Color(255,0,153)); + EXPECT_EQ(Color::rainbow(0.6), Color(0, 102, 255)); + EXPECT_EQ(Color::rainbow(0.7), Color(51, 0, 255)); + EXPECT_EQ(Color::rainbow(0.8), Color(204, 0, 255)); + EXPECT_EQ(Color::rainbow(0.9), Color(255, 0, 153)); EXPECT_EQ(Color::rainbow(1), Color::red()); } TEST(Color, RainbowID) { - EXPECT_EQ(Color::rainbowId(0,16), Color::red()); - EXPECT_EQ(Color::rainbowId(1,16), Color(255,95,0)); - EXPECT_EQ(Color::rainbowId(8,16), Color::cyan()); - EXPECT_EQ(Color::rainbowId(15,16), Color(255,0,95)); - EXPECT_EQ(Color::rainbowId(16,16), Color(255, 47, 0)); - EXPECT_EQ(Color::rainbowId(17,16), Color(255, 143, 0)); - EXPECT_EQ(Color::rainbowId(32,16), Color(255, 23, 0)); - EXPECT_EQ(Color::rainbowId(33,16), Color(255, 119, 0)); + EXPECT_EQ(Color::rainbowId(0, 16), Color::red()); + EXPECT_EQ(Color::rainbowId(1, 16), Color(255, 96, 0)); + EXPECT_EQ(Color::rainbowId(8, 16), Color::cyan()); + EXPECT_EQ(Color::rainbowId(15, 16), Color(255, 0, 96)); + EXPECT_EQ(Color::rainbowId(16, 16), Color(255, 48, 0)); + EXPECT_EQ(Color::rainbowId(17, 16), Color(255, 143, 0)); + EXPECT_EQ(Color::rainbowId(32, 16), Color(255, 24, 0)); + EXPECT_EQ(Color::rainbowId(33, 16), Color(255, 120, 0)); } TEST(Color, fromHSV) { @@ -126,8 +127,8 @@ TEST(Color, fromHSV) { EXPECT_EQ(Color::fromHSV(0.0f, 1.0f, 1.0f), Color::red()); EXPECT_EQ(Color::fromHSV(0.3333f, 1.0f, 1.0f), Color::green()); EXPECT_EQ(Color::fromHSV(0.6666f, 1.0f, 1.0f), Color::blue()); - EXPECT_EQ(Color::fromHSV(0.25f, 0.8f, 0.3f), Color(45, 76, 15)); - EXPECT_EQ(Color::fromHSV(0.6f, 0.45f, 0.65f), Color(91, 120, 165)); + EXPECT_EQ(Color::fromHSV(0.25f, 0.8f, 0.3f), Color(46, 77, 15)); + EXPECT_EQ(Color::fromHSV(0.6f, 0.45f, 0.65f), Color(91, 121, 166)); } TEST(Color, fromHLS) { @@ -136,8 +137,34 @@ TEST(Color, fromHLS) { EXPECT_EQ(Color::fromHLS(0.0f, 0.5f, 1.0f), Color::red()); EXPECT_EQ(Color::fromHLS(0.3333f, 0.5f, 1.0f), Color::green()); EXPECT_EQ(Color::fromHLS(0.6666f, 0.5f, 1.0f), Color::blue()); - EXPECT_EQ(Color::fromHLS(0.25f, 0.3f, 0.8f), Color(76, 137, 15)); - EXPECT_EQ(Color::fromHLS(0.6f, 0.65f, 0.45f), Color(125, 157, 205)); + EXPECT_EQ(Color::fromHLS(0.25f, 0.3f, 0.8f), Color(77, 138, 15)); + EXPECT_EQ(Color::fromHLS(0.6f, 0.65f, 0.45f), Color(126, 158, 206)); +} + +TEST(Color, HLSConversion) { + const uint32_t max = 0x00FFFFFF; + for (uint32_t i = 0; i <= max; i += 10) { + const auto red = static_cast(i & 0x000000FF); + const auto green = static_cast((i & 0x0000FF00) >> 8); + const auto blue = static_cast((i & 0x00FF0000) >> 16); + const Color expected(red, green, blue); + const auto [hue, luminance, saturation] = expected.toHLS(); + const auto result = Color::fromHLS(hue, luminance, saturation); + ASSERT_EQ(expected, result); + } +} + +TEST(Color, HSVConversion) { + const uint32_t max = 0x00FFFFFF; + for (uint32_t i = 0; i <= max; i += 10) { + const auto red = static_cast(i & 0x000000FF); + const auto green = static_cast((i & 0x0000FF00) >> 8); + const auto blue = static_cast((i & 0x00FF0000) >> 16); + const Color expected(red, green, blue); + const auto [hue, luminance, saturation] = expected.toHSV(); + const auto result = Color::fromHSV(hue, luminance, saturation); + ASSERT_EQ(expected, result); + } } } // namespace spark_dsg