From d734bc6a0a3fd300632bb625371d4bf960fe6702 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 1 Jul 2021 22:05:47 -0500 Subject: [PATCH] Color table implementation --- .gitmodules | 3 + external/CMakeLists.txt | 7 + external/hsluv-c | 1 + external/hsluv-c.cmake | 9 + test/CMakeLists.txt | 6 + test/data | 2 +- test/source/scwx/common/color_table.test.cpp | 34 ++ test/test.cmake | 4 + wxdata/include/scwx/common/color_table.hpp | 48 +++ wxdata/source/scwx/common/color_table.cpp | 313 +++++++++++++++++++ wxdata/source/scwx/wsr88d/ar2v_file.cpp | 4 +- wxdata/wxdata.cmake | 11 +- 12 files changed, 437 insertions(+), 5 deletions(-) create mode 160000 external/hsluv-c create mode 100644 external/hsluv-c.cmake create mode 100644 test/source/scwx/common/color_table.test.cpp create mode 100644 wxdata/include/scwx/common/color_table.hpp create mode 100644 wxdata/source/scwx/common/color_table.cpp diff --git a/.gitmodules b/.gitmodules index 5690c4f5..a07f6e5f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "test/data"] path = test/data url = ../supercell-wx-test-data +[submodule "external/hsluv-c"] + path = external/hsluv-c + url = https://github.com/hsluv/hsluv-c.git diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index b6513bcb..d134523d 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,4 +1,11 @@ cmake_minimum_required(VERSION 3.11) set(PROJECT_NAME scwx-external) +set_property(DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS + hsluv-c.cmake + mapbox-gl-native.cmake) + +include(hsluv-c.cmake) include(mapbox-gl-native.cmake) diff --git a/external/hsluv-c b/external/hsluv-c new file mode 160000 index 00000000..59539e04 --- /dev/null +++ b/external/hsluv-c @@ -0,0 +1 @@ +Subproject commit 59539e04a6fa648935cbe57c2104041f23136c4a diff --git a/external/hsluv-c.cmake b/external/hsluv-c.cmake new file mode 100644 index 00000000..5a74a156 --- /dev/null +++ b/external/hsluv-c.cmake @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.11) +set(PROJECT_NAME scwx-hsluv-c) + +set(HSLUV_C_TESTS OFF) +add_subdirectory(hsluv-c) + +set(HSLUV_C_INCLUDE_DIR ${hsluv-c_SOURCE_DIR}/src PARENT_SCOPE) + +set_target_properties(hsluv-c PROPERTIES FOLDER hsluv) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 45c59dd0..2ef7354d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,2 +1,8 @@ cmake_minimum_required(VERSION 3.11) + +set_property(DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS + test.cmake) + include(test.cmake) diff --git a/test/data b/test/data index 40f68fe0..4bc8b828 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 40f68fe08977e5168679ce0419834312a6369205 +Subproject commit 4bc8b8283fcd9d3ed0bcb3099d36db269209b7df diff --git a/test/source/scwx/common/color_table.test.cpp b/test/source/scwx/common/color_table.test.cpp new file mode 100644 index 00000000..c95afa86 --- /dev/null +++ b/test/source/scwx/common/color_table.test.cpp @@ -0,0 +1,34 @@ +#include + +#include + +namespace scwx +{ +namespace common +{ + +TEST(color_table, reflectivity) +{ + std::string filename(std::string(SCWX_TEST_DATA_DIR) + + "/colors/reflectivity.pal"); + + std::shared_ptr ct = ColorTable::Load(filename); + + EXPECT_EQ(ct->Color(5), boost::gil::rgba8_pixel_t(164, 164, 255, 255)); + EXPECT_EQ(ct->Color(10), boost::gil::rgba8_pixel_t(164, 164, 255, 255)); + EXPECT_EQ(ct->Color(20), boost::gil::rgba8_pixel_t(64, 128, 255, 255)); + EXPECT_EQ(ct->Color(30), boost::gil::rgba8_pixel_t(0, 255, 0, 255)); + EXPECT_EQ(ct->Color(32), boost::gil::rgba8_pixel_t(0, 230, 0, 255)); + EXPECT_EQ(ct->Color(35), boost::gil::rgba8_pixel_t(0, 192, 0, 255)); + EXPECT_EQ(ct->Color(40), boost::gil::rgba8_pixel_t(255, 255, 0, 255)); + EXPECT_EQ(ct->Color(50), boost::gil::rgba8_pixel_t(255, 0, 0, 255)); + EXPECT_EQ(ct->Color(55), boost::gil::rgba8_pixel_t(208, 0, 0, 255)); + EXPECT_EQ(ct->Color(60), boost::gil::rgba8_pixel_t(255, 0, 255, 255)); + EXPECT_EQ(ct->Color(65), boost::gil::rgba8_pixel_t(192, 0, 192, 255)); + EXPECT_EQ(ct->Color(70), boost::gil::rgba8_pixel_t(255, 255, 255, 255)); + EXPECT_EQ(ct->Color(80), boost::gil::rgba8_pixel_t(128, 128, 128, 255)); + EXPECT_EQ(ct->Color(85), boost::gil::rgba8_pixel_t(128, 128, 128, 255)); +} + +} // namespace common +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index a24bd763..319e4192 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -8,15 +8,18 @@ find_package(BZip2) find_package(GTest) set(SRC_MAIN source/scwx/wxtest.cpp) +set(SRC_COMMON_TESTS source/scwx/common/color_table.test.cpp) set(SRC_UTIL_TESTS source/scwx/util/rangebuf.test.cpp source/scwx/util/vectorbuf.test.cpp) set(SRC_WSR88D_TESTS source/scwx/wsr88d/ar2v_file.test.cpp) add_executable(wxtest ${SRC_MAIN} + ${SRC_COMMON_TESTS} ${SRC_UTIL_TESTS} ${SRC_WSR88D_TESTS}) source_group("Source Files\\main" FILES ${SRC_MAIN}) +source_group("Source Files\\common" FILES ${SRC_COMMON_TESTS}) source_group("Source Files\\util" FILES ${SRC_UTIL_TESTS}) source_group("Source Files\\wsr88d" FILES ${SRC_WSR88D_TESTS}) @@ -38,6 +41,7 @@ target_link_libraries(wxtest Boost::iostreams Boost::log BZip2::BZip2 GTest::gtest + hsluv-c wxdata) if (WIN32) diff --git a/wxdata/include/scwx/common/color_table.hpp b/wxdata/include/scwx/common/color_table.hpp new file mode 100644 index 00000000..6d8654bc --- /dev/null +++ b/wxdata/include/scwx/common/color_table.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +#include + +namespace scwx +{ +namespace common +{ + +class ColorTableImpl; + +/** + * @brief Color Table + * + * Implementation based on: + * Color Table File Specification + * Mike Gibson + * Gibson Ridge Software, LLC. Used with permission. + * http://www.grlevelx.com/manuals/color_tables/files_color_table.htm + */ +class ColorTable +{ +public: + explicit ColorTable(); + ~ColorTable(); + + ColorTable(const ColorTable&) = delete; + ColorTable& operator=(const ColorTable&) = delete; + + ColorTable(ColorTable&&) noexcept; + ColorTable& operator=(ColorTable&&) noexcept; + + boost::gil::rgba8_pixel_t Color(float value) const; + + static std::shared_ptr Load(const std::string& filename); + +private: + std::unique_ptr p; + + void ProcessLine(const std::vector& tokenList); +}; + +} // namespace common +} // namespace scwx diff --git a/wxdata/source/scwx/common/color_table.cpp b/wxdata/source/scwx/common/color_table.cpp new file mode 100644 index 00000000..5ac6098a --- /dev/null +++ b/wxdata/source/scwx/common/color_table.cpp @@ -0,0 +1,313 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace scwx +{ +namespace common +{ + +static const std::string logPrefix_ {"[scwx::common::color_table] "}; + +enum class ColorMode +{ + RGBA, + HSLuv +}; + +static boost::gil::rgba8_pixel_t +ParseColor(const std::vector& tokenList, + size_t startIndex, + ColorMode colorMode, + bool hasAlpha = true); +template +T RoundChannel(double value); +template +T StringToDecimal(const std::string& str); + +class ColorTableImpl +{ +public: + explicit ColorTableImpl() : + product_ {}, + units_ {}, + scale_ {1.0f}, + offset_ {0.0f}, + step_ {10}, + rfColor_ {0, 0, 0, 0}, + colorMode_ {ColorMode::RGBA}, + colorMap_ {} {}; + ~ColorTableImpl() = default; + + std::string product_; + std::string units_; + float scale_; + float offset_; + long step_; + boost::gil::rgba8_pixel_t rfColor_; + ColorMode colorMode_; + + std::map>> + colorMap_; +}; + +ColorTable::ColorTable() : p(std::make_unique()) {} +ColorTable::~ColorTable() = default; + +ColorTable::ColorTable(ColorTable&&) noexcept = default; +ColorTable& ColorTable::operator=(ColorTable&&) noexcept = default; + +boost::gil::rgba8_pixel_t ColorTable::Color(float value) const +{ + boost::gil::rgba8_pixel_t color; + bool found = false; + + value = value * p->scale_ + p->offset_; + + auto prev = p->colorMap_.cbegin(); + for (auto it = p->colorMap_.cbegin(); it != p->colorMap_.cend(); ++it) + { + if (value < it->first) + { + if (it == p->colorMap_.cbegin()) + { + color = it->second.first; + } + else + { + // Interpolate + float key1 = prev->first; + float key2 = it->first; + + boost::gil::rgba8_pixel_t color1 = prev->second.first; + boost::gil::rgba8_pixel_t color2 = (prev->second.second) ? + prev->second.second.value() : + it->second.first; + + float t = (value - key1) / (key2 - key1); + color[0] = + RoundChannel(std::lerp(color1[0], color2[0], t)); + color[1] = + RoundChannel(std::lerp(color1[1], color2[1], t)); + color[2] = + RoundChannel(std::lerp(color1[2], color2[2], t)); + color[3] = + RoundChannel(std::lerp(color1[3], color2[3], t)); + } + + found = true; + break; + } + + prev = it; + } + + if (!found) + { + color = prev->second.first; + } + + return color; +} + +std::shared_ptr ColorTable::Load(const std::string& filename) +{ + BOOST_LOG_TRIVIAL(debug) + << logPrefix_ << "Loading color table: " << filename; + + std::shared_ptr p = std::make_shared(); + + std::ifstream f(filename, std::ios_base::in); + + std::string line; + while (std::getline(f, line)) + { + std::string token; + std::istringstream tokens(line); + std::vector tokenList; + + while (tokens >> token) + { + if (token.find(';') != std::string::npos) + { + break; + } + + tokenList.push_back(std::move(token)); + } + + if (tokenList.size() >= 2) + { + try + { + p->ProcessLine(tokenList); + } + catch (const std::exception&) + { + BOOST_LOG_TRIVIAL(warning) + << logPrefix_ << "Could not parse line: " << line; + } + } + } + + return p; +} + +void ColorTable::ProcessLine(const std::vector& tokenList) +{ + if (tokenList[0] == "Product:") + { + // Product: string + p->product_ = tokenList[1]; + } + else if (tokenList[0] == "Units:") + { + // Units: string + p->units_ = tokenList[1]; + } + else if (tokenList[0] == "Scale:") + { + // Scale: float + p->scale_ = std::stof(tokenList[1]); + } + else if (tokenList[0] == "Offset:") + { + // Offset: float + p->offset_ = std::stof(tokenList[1]); + } + else if (tokenList[0] == "Step:") + { + // Step: float + p->step_ = std::stof(tokenList[1]); + } + else if (tokenList[0] == "RF") + { + // RF: R G B [A] + p->rfColor_ = ParseColor(tokenList, 1, p->colorMode_); + } + else if (tokenList[0] == "Color:") + { + // Color: value R G B [R G B] + float key = std::stof(tokenList[1]); + + boost::gil::rgba8_pixel_t color1 = + ParseColor(tokenList, 2, p->colorMode_, false); + std::optional color2; + + if (tokenList.size() >= 8) + { + color2 = ParseColor(tokenList, 5, p->colorMode_, false); + } + + p->colorMap_[key] = std::make_pair(color1, color2); + } + else if (tokenList[0] == "Color4:") + { + // Color4: value R G B A [R G B A] + float key = std::stof(tokenList[1]); + + boost::gil::rgba8_pixel_t color1 = + ParseColor(tokenList, 2, p->colorMode_); + std::optional color2; + + if (tokenList.size() >= 10) + { + color2 = ParseColor(tokenList, 6, p->colorMode_); + } + + p->colorMap_[key] = std::make_pair(color1, color2); + } + else if (tokenList[0] == "SolidColor:") + { + // SolidColor: value R G B + float key = std::stof(tokenList[1]); + + boost::gil::rgba8_pixel_t color1 = + ParseColor(tokenList, 2, p->colorMode_, false); + + p->colorMap_[key] = std::make_pair(color1, color1); + } + else if (tokenList[0] == "SolidColor4:") + { + // SolidColor4: value R G B A + float key = std::stof(tokenList[1]); + + boost::gil::rgba8_pixel_t color1 = + ParseColor(tokenList, 2, p->colorMode_); + + p->colorMap_[key] = std::make_pair(color1, color1); + } +} + +static boost::gil::rgba8_pixel_t +ParseColor(const std::vector& tokenList, + size_t startIndex, + ColorMode colorMode, + bool hasAlpha) +{ + + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a = 255; + + if (colorMode == ColorMode::RGBA) + { + r = StringToDecimal(tokenList[startIndex + 0]); + g = StringToDecimal(tokenList[startIndex + 1]); + b = StringToDecimal(tokenList[startIndex + 2]); + + if (hasAlpha && tokenList.size() >= startIndex + 4) + { + a = StringToDecimal(tokenList[startIndex + 3]); + } + } + else // if (colorMode == ColorMode::HSLuv) + { + double h = std::stod(tokenList[startIndex + 0]); + double s = std::stod(tokenList[startIndex + 1]); + double l = std::stod(tokenList[startIndex + 2]); + double dr; + double dg; + double db; + + hsluv2rgb(h, s, l, &dr, &dg, &db); + + r = RoundChannel(dr * 255.0); + g = RoundChannel(dg * 255.0); + b = RoundChannel(db * 255.0); + } + + return boost::gil::rgba8_pixel_t {r, g, b, a}; +} + +template +T RoundChannel(double value) +{ + return std::clamp(std::lround(value), + std::numeric_limits::min(), + std::numeric_limits::max()); +} + +template +T StringToDecimal(const std::string& str) +{ + return std::clamp(std::stoi(str), + std::numeric_limits::min(), + std::numeric_limits::max()); +} + +} // namespace common +} // namespace scwx diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 526c4100..9971eea9 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -38,7 +38,7 @@ public: void HandleMessage(std::shared_ptr& message); void LoadLDMRecords(std::ifstream& f); void ParseLDMRecords(); - void ProcessRadarData(std::shared_ptr& message); + void ProcessRadarData(std::shared_ptr message); void ProcessVcpData(); std::string tapeFilename_; @@ -245,7 +245,7 @@ void Ar2vFileImpl::HandleMessage(std::shared_ptr& message) } void Ar2vFileImpl::ProcessRadarData( - std::shared_ptr& message) + std::shared_ptr message) { uint16_t azimuthIndex = message->azimuth_number() - 1; uint16_t elevationIndex = message->elevation_number() - 1; diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 5f904228..ac2ebbf3 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -2,6 +2,8 @@ project(scwx-data) find_package(Boost) +set(HDR_COMMON include/scwx/common/color_table.hpp) +set(SRC_COMMON source/scwx/common/color_table.cpp) set(HDR_UTIL include/scwx/util/rangebuf.hpp include/scwx/util/vectorbuf.hpp) set(SRC_UTIL source/scwx/util/rangebuf.cpp @@ -28,13 +30,17 @@ set(SRC_WSR88D_RDA source/scwx/wsr88d/rda/clutter_filter_map.cpp source/scwx/wsr88d/rda/rda_status_data.cpp source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp) -add_library(wxdata OBJECT ${HDR_UTIL} +add_library(wxdata OBJECT ${HDR_COMMON} + ${SRC_COMMON} + ${HDR_UTIL} ${SRC_UTIL} ${HDR_WSR88D} ${SRC_WSR88D} ${HDR_WSR88D_RDA} ${SRC_WSR88D_RDA}) +source_group("Header Files\\common" FILES ${HDR_COMMON}) +source_group("Source Files\\common" FILES ${SRC_COMMON}) source_group("Header Files\\util" FILES ${HDR_UTIL}) source_group("Source Files\\util" FILES ${SRC_UTIL}) source_group("Header Files\\wsr88d" FILES ${HDR_WSR88D}) @@ -43,6 +49,7 @@ source_group("Header Files\\wsr88d\\rda" FILES ${HDR_WSR88D_RDA}) source_group("Source Files\\wsr88d\\rda" FILES ${SRC_WSR88D_RDA}) target_include_directories(wxdata PRIVATE ${Boost_INCLUDE_DIR} + ${HSLUV_C_INCLUDE_DIR} ${scwx-data_SOURCE_DIR}/include ${scwx-data_SOURCE_DIR}/source) target_include_directories(wxdata INTERFACE ${scwx-data_SOURCE_DIR}/include) @@ -51,6 +58,6 @@ if(MSVC) target_compile_options(wxdata PRIVATE /W3) endif() -set_target_properties(wxdata PROPERTIES CXX_STANDARD 17 +set_target_properties(wxdata PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF)