Skip to content

Commit

Permalink
add conversion to hsv and hls
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanhhughes committed Jul 31, 2024
1 parent 4909df7 commit 56f560c
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 66 deletions.
4 changes: 4 additions & 0 deletions include/spark_dsg/color.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

#include <cstdint>
#include <ostream>
#include <array>
#include <vector>

namespace spark_dsg {
Expand Down Expand Up @@ -99,8 +100,11 @@ struct Color {
static Color random();

// Conversions.
std::array<float, 4> toUnitRange() const;
static Color fromHSV(float hue, float saturation, float value);
std::array<float, 3> toHSV() const;
static Color fromHLS(float hue, float luminance, float saturation);
std::array<float, 3> toHLS() const;

// Color maps.
/**
Expand Down
182 changes: 136 additions & 46 deletions src/color.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,102 @@ std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<uint8_t> dis(0, 255);

namespace {

struct HueInfo {
float max = 0.0f;
float min = 1.0f;
float hue = 0.0;
};

std::array<float, 3> 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<float, 3> 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<float>(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<uint8_t>(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;
}
Expand Down Expand Up @@ -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<float, 3> 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<float, 4> 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) {
Expand All @@ -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<uint8_t>((r1 + m) * 255),
static_cast<uint8_t>((g1 + m) * 255),
static_cast<uint8_t>((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<float, 3> Color::toHSV() const {
const auto info = getHue(*this);

std::array<float, 3> hsv;
hsv[0] = info.hue;
hsv[1] = info.max == 0.0f ? 0.0f : static_cast<float>(info.max - info.min) / info.max;
hsv[2] = info.max;
return hsv;
}

Color Color::fromHLS(float hue, float luminance, float saturation) {
Expand All @@ -101,41 +193,58 @@ 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<uint8_t>((r1 + m) * 255),
static_cast<uint8_t>((g1 + m) * 255),
static_cast<uint8_t>((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<float, 3> Color::toHLS() const {
const auto info = getHue(*this);

std::array<float, 3> 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<uint8_t>(value * 255),
static_cast<uint8_t>(value * 255),
static_cast<uint8_t>(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;
}

Color Color::spectrum(float value, const std::vector<Color>& 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<size_t>(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);
}
Expand All @@ -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<float>(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));
}
Expand Down
67 changes: 47 additions & 20 deletions tests/utest_color.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
* -------------------------------------------------------------------------- */
#include <gtest/gtest.h>

#include <iostream>
#include <unordered_map>

#include "spark_dsg/color.h"
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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<uint8_t>(i & 0x000000FF);
const auto green = static_cast<uint8_t>((i & 0x0000FF00) >> 8);
const auto blue = static_cast<uint8_t>((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<uint8_t>(i & 0x000000FF);
const auto green = static_cast<uint8_t>((i & 0x0000FF00) >> 8);
const auto blue = static_cast<uint8_t>((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

0 comments on commit 56f560c

Please sign in to comment.