Color table implementation

This commit is contained in:
Dan Paulat 2021-07-01 22:05:47 -05:00
parent 6c25ce4230
commit d734bc6a0a
12 changed files with 437 additions and 5 deletions

3
.gitmodules vendored
View file

@ -7,3 +7,6 @@
[submodule "test/data"] [submodule "test/data"]
path = test/data path = test/data
url = ../supercell-wx-test-data url = ../supercell-wx-test-data
[submodule "external/hsluv-c"]
path = external/hsluv-c
url = https://github.com/hsluv/hsluv-c.git

View file

@ -1,4 +1,11 @@
cmake_minimum_required(VERSION 3.11) cmake_minimum_required(VERSION 3.11)
set(PROJECT_NAME scwx-external) 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) include(mapbox-gl-native.cmake)

1
external/hsluv-c vendored Submodule

@ -0,0 +1 @@
Subproject commit 59539e04a6fa648935cbe57c2104041f23136c4a

9
external/hsluv-c.cmake vendored Normal file
View file

@ -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)

View file

@ -1,2 +1,8 @@
cmake_minimum_required(VERSION 3.11) cmake_minimum_required(VERSION 3.11)
set_property(DIRECTORY
APPEND
PROPERTY CMAKE_CONFIGURE_DEPENDS
test.cmake)
include(test.cmake) include(test.cmake)

@ -1 +1 @@
Subproject commit 40f68fe08977e5168679ce0419834312a6369205 Subproject commit 4bc8b8283fcd9d3ed0bcb3099d36db269209b7df

View file

@ -0,0 +1,34 @@
#include <scwx/common/color_table.hpp>
#include <gtest/gtest.h>
namespace scwx
{
namespace common
{
TEST(color_table, reflectivity)
{
std::string filename(std::string(SCWX_TEST_DATA_DIR) +
"/colors/reflectivity.pal");
std::shared_ptr<ColorTable> 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

View file

@ -8,15 +8,18 @@ find_package(BZip2)
find_package(GTest) find_package(GTest)
set(SRC_MAIN source/scwx/wxtest.cpp) 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 set(SRC_UTIL_TESTS source/scwx/util/rangebuf.test.cpp
source/scwx/util/vectorbuf.test.cpp) source/scwx/util/vectorbuf.test.cpp)
set(SRC_WSR88D_TESTS source/scwx/wsr88d/ar2v_file.test.cpp) set(SRC_WSR88D_TESTS source/scwx/wsr88d/ar2v_file.test.cpp)
add_executable(wxtest ${SRC_MAIN} add_executable(wxtest ${SRC_MAIN}
${SRC_COMMON_TESTS}
${SRC_UTIL_TESTS} ${SRC_UTIL_TESTS}
${SRC_WSR88D_TESTS}) ${SRC_WSR88D_TESTS})
source_group("Source Files\\main" FILES ${SRC_MAIN}) 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\\util" FILES ${SRC_UTIL_TESTS})
source_group("Source Files\\wsr88d" FILES ${SRC_WSR88D_TESTS}) source_group("Source Files\\wsr88d" FILES ${SRC_WSR88D_TESTS})
@ -38,6 +41,7 @@ target_link_libraries(wxtest Boost::iostreams
Boost::log Boost::log
BZip2::BZip2 BZip2::BZip2
GTest::gtest GTest::gtest
hsluv-c
wxdata) wxdata)
if (WIN32) if (WIN32)

View file

@ -0,0 +1,48 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <boost/gil.hpp>
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<ColorTable> Load(const std::string& filename);
private:
std::unique_ptr<ColorTableImpl> p;
void ProcessLine(const std::vector<std::string>& tokenList);
};
} // namespace common
} // namespace scwx

View file

@ -0,0 +1,313 @@
#include <scwx/common/color_table.hpp>
#include <cmath>
#include <fstream>
#include <limits>
#include <map>
#include <optional>
#include <sstream>
#include <boost/gil.hpp>
#include <boost/log/trivial.hpp>
#include <hsluv.h>
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<std::string>& tokenList,
size_t startIndex,
ColorMode colorMode,
bool hasAlpha = true);
template<typename T>
T RoundChannel(double value);
template<typename T>
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<float,
std::pair<boost::gil::rgba8_pixel_t,
std::optional<boost::gil::rgba8_pixel_t>>>
colorMap_;
};
ColorTable::ColorTable() : p(std::make_unique<ColorTableImpl>()) {}
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<uint8_t>(std::lerp(color1[0], color2[0], t));
color[1] =
RoundChannel<uint8_t>(std::lerp(color1[1], color2[1], t));
color[2] =
RoundChannel<uint8_t>(std::lerp(color1[2], color2[2], t));
color[3] =
RoundChannel<uint8_t>(std::lerp(color1[3], color2[3], t));
}
found = true;
break;
}
prev = it;
}
if (!found)
{
color = prev->second.first;
}
return color;
}
std::shared_ptr<ColorTable> ColorTable::Load(const std::string& filename)
{
BOOST_LOG_TRIVIAL(debug)
<< logPrefix_ << "Loading color table: " << filename;
std::shared_ptr<ColorTable> p = std::make_shared<ColorTable>();
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<std::string> 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<std::string>& 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<boost::gil::rgba8_pixel_t> 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<boost::gil::rgba8_pixel_t> 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<std::string>& 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<uint8_t>(tokenList[startIndex + 0]);
g = StringToDecimal<uint8_t>(tokenList[startIndex + 1]);
b = StringToDecimal<uint8_t>(tokenList[startIndex + 2]);
if (hasAlpha && tokenList.size() >= startIndex + 4)
{
a = StringToDecimal<uint8_t>(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<uint8_t>(dr * 255.0);
g = RoundChannel<uint8_t>(dg * 255.0);
b = RoundChannel<uint8_t>(db * 255.0);
}
return boost::gil::rgba8_pixel_t {r, g, b, a};
}
template<typename T>
T RoundChannel(double value)
{
return std::clamp<int>(std::lround(value),
std::numeric_limits<T>::min(),
std::numeric_limits<T>::max());
}
template<typename T>
T StringToDecimal(const std::string& str)
{
return std::clamp<int>(std::stoi(str),
std::numeric_limits<T>::min(),
std::numeric_limits<T>::max());
}
} // namespace common
} // namespace scwx

View file

@ -38,7 +38,7 @@ public:
void HandleMessage(std::shared_ptr<rda::Message>& message); void HandleMessage(std::shared_ptr<rda::Message>& message);
void LoadLDMRecords(std::ifstream& f); void LoadLDMRecords(std::ifstream& f);
void ParseLDMRecords(); void ParseLDMRecords();
void ProcessRadarData(std::shared_ptr<rda::DigitalRadarData>& message); void ProcessRadarData(std::shared_ptr<rda::DigitalRadarData> message);
void ProcessVcpData(); void ProcessVcpData();
std::string tapeFilename_; std::string tapeFilename_;
@ -245,7 +245,7 @@ void Ar2vFileImpl::HandleMessage(std::shared_ptr<rda::Message>& message)
} }
void Ar2vFileImpl::ProcessRadarData( void Ar2vFileImpl::ProcessRadarData(
std::shared_ptr<rda::DigitalRadarData>& message) std::shared_ptr<rda::DigitalRadarData> message)
{ {
uint16_t azimuthIndex = message->azimuth_number() - 1; uint16_t azimuthIndex = message->azimuth_number() - 1;
uint16_t elevationIndex = message->elevation_number() - 1; uint16_t elevationIndex = message->elevation_number() - 1;

View file

@ -2,6 +2,8 @@ project(scwx-data)
find_package(Boost) 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 set(HDR_UTIL include/scwx/util/rangebuf.hpp
include/scwx/util/vectorbuf.hpp) include/scwx/util/vectorbuf.hpp)
set(SRC_UTIL source/scwx/util/rangebuf.cpp 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/rda_status_data.cpp
source/scwx/wsr88d/rda/volume_coverage_pattern_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} ${SRC_UTIL}
${HDR_WSR88D} ${HDR_WSR88D}
${SRC_WSR88D} ${SRC_WSR88D}
${HDR_WSR88D_RDA} ${HDR_WSR88D_RDA}
${SRC_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("Header Files\\util" FILES ${HDR_UTIL})
source_group("Source Files\\util" FILES ${SRC_UTIL}) source_group("Source Files\\util" FILES ${SRC_UTIL})
source_group("Header Files\\wsr88d" FILES ${HDR_WSR88D}) 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}) source_group("Source Files\\wsr88d\\rda" FILES ${SRC_WSR88D_RDA})
target_include_directories(wxdata PRIVATE ${Boost_INCLUDE_DIR} target_include_directories(wxdata PRIVATE ${Boost_INCLUDE_DIR}
${HSLUV_C_INCLUDE_DIR}
${scwx-data_SOURCE_DIR}/include ${scwx-data_SOURCE_DIR}/include
${scwx-data_SOURCE_DIR}/source) ${scwx-data_SOURCE_DIR}/source)
target_include_directories(wxdata INTERFACE ${scwx-data_SOURCE_DIR}/include) target_include_directories(wxdata INTERFACE ${scwx-data_SOURCE_DIR}/include)
@ -51,6 +58,6 @@ if(MSVC)
target_compile_options(wxdata PRIVATE /W3) target_compile_options(wxdata PRIVATE /W3)
endif() endif()
set_target_properties(wxdata PROPERTIES CXX_STANDARD 17 set_target_properties(wxdata PROPERTIES CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF) CXX_EXTENSIONS OFF)