diff --git a/CMakeLists.txt b/CMakeLists.txt index ee9ffdcf..c1cb849f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,7 +76,8 @@ add_subdirectory(TMONafchi17) #Tomas Krivanek add_subdirectory(TMONafchi17_2/) #Boris Strbak add_subdirectory(TMOMikamo14/) #Jan Findra add_subdirectory(TMOSlomp12/) #Jan Findra -add_subdirectory(TMOThompson02/) #Jan Findra +add_subdirectory(TMOThompson02/) #Jan Findra +add_subdirectory(TMOKhan20/) #Milan Tichavský INSTALL(FILES tmolib/libtmo.so DESTINATION lib) INSTALL(FILES TMOgui/tmogui PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE DESTINATION . ) diff --git a/TMOKhan20/CMakeLists.txt b/TMOKhan20/CMakeLists.txt new file mode 100644 index 00000000..acc26e70 --- /dev/null +++ b/TMOKhan20/CMakeLists.txt @@ -0,0 +1,33 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.20) + +SET(CMAKE_COLOR_MAKEFILE ON) +SET(CMAKE_VERBOSE_MAKEFILE ON) +SET(CMAKE_INCLUDE_CURRENT_DIR TRUE) + +INCLUDE_DIRECTORIES( + ../tmolib/ + . +) + +SET(TMO_SOURCES + TMOKhan20.cpp + TMOPlugin.cpp +) + + +SET(TMOWARD_HEADERS + TMOKhan20.h + TMOPlugin.h +) + + + +add_library(Khan20 SHARED ${TMO_SOURCES}) +target_link_libraries( + Khan20 PRIVATE tmo +) + +add_custom_command( TARGET Khan20 POST_BUILD + COMMAND cp -f libKhan20.so ../TMOgui/Khan20.tml + COMMAND cp -f libKhan20.so ../Khan20.tml +) diff --git a/TMOKhan20/TMOKhan20.cpp b/TMOKhan20/TMOKhan20.cpp new file mode 100644 index 00000000..95e3b733 --- /dev/null +++ b/TMOKhan20/TMOKhan20.cpp @@ -0,0 +1,225 @@ +/* --------------------------------------------------------------------------- * + * TMOKhan.cpp: implementation of the TMOKhan20 class. * + * --------------------------------------------------------------------------- */ + +#include "TMOKhan20.h" + +#include +#include +#include + +// Constants from the paper, see equation #1 +#define M (1305.0/8192.0) +#define N (2523.0/32.0) +#define UNDEF_INT_PARAM 0 +#define EPSILON 0.01 + + +/** + * Construct a luminance lookup table (LUT) based on histogram binning. + * + * @param bins Number of bins in the histogram, in the paper N + * @param truncation Truncation factor to limit bin counts, in the paper k + * @param luminance Input image luminance values + * @param lum_len Length of the luminance array + * @param min_luminance Minimum observed luminance in the input image + * @param max_luminance Maximum observed luminance in the input image + */ +LuminanceLUT::LuminanceLUT(int bins, int truncation, const std::vector &luminance, int lum_len, + double min_luminance, double max_luminance) { + // At first, the LUT with N + 1 rows is implemented as two vectors + std::vector edges(bins + 1); // T(1) calculation + std::vector bin_count(bins + 1, 0); // T(2) calculation + + // Equation #3 in the paper + for (int i = 0; i <= bins; i++) { + edges[i] = min_luminance + i * (max_luminance - min_luminance) / bins; + } + // Otherwise for max luminance, we would be getting out of bounds index in upper_bound function + edges[bins] += EPSILON; + + // Equation #4 in the paper + for (int i = 0; i < lum_len; i++) { + long bin_index = std::upper_bound(edges.begin(), edges.end(), luminance[i]) - edges.begin(); + if (bin_index >= 0 && bin_index < bins + 1) { + bin_count[bin_index]++; + } + } + int prev = 0; + int max_bin_count = static_cast(lum_len * static_cast(truncation) / bins); + for (int i = 0; i < bins + 1; i++) { + if (bin_count[i] > max_bin_count) { // truncate to limit max bin count + bin_count[i] = max_bin_count; + } + bin_count[i] += prev; + prev = bin_count[i]; + } + // Equation #4, case for i=0 + assert(bin_count[0] == 0 && "LUT creation poorly implemented, see equation #4 case i=0."); + + // Equation #5 in the paper + for (int i = 0; i < bins + 1; i++) { + bin_count[i] = 255 * bin_count[i] / bin_count[bins]; + } + + for (int i = 0; i < bins + 1; i++) { + lut.emplace_back(edges[i], bin_count[i]); + } + assert(lut.size() == bins + 1 && "Implementation error (unexpected LUT size)."); +} + +/** + * Return the bin edges for the given key luminance value. Keys represent HDR values, and the corresponding values + * represent mapped LDR values. + * + * @param key Input luminance value to search for. + * @return Tuple containing (lower key, lower value, upper key, upper value). + */ +std::tuple LuminanceLUT::getValue(double key) const { + auto it = std::upper_bound(lut.begin(), lut.end(), key, + [](double val, const std::pair &elem) { + return val < elem.first; + }); + + if (it == lut.begin()) { + throw std::runtime_error("Invalid lookup key: " + std::to_string(key)); + } else if (it == lut.end()) { + return std::make_tuple(lut.back().first, lut.back().second, lut.back().first, lut.back().second); + } + + auto upper = it; + auto lower = std::prev(it); + return std::make_tuple(lower->first, lower->second, upper->first, upper->second); +} + +/** Print LUT to stderr for debug purposes */ +[[maybe_unused]] void LuminanceLUT::printLUT() const { + for (const auto &[edge, value]: lut) { + std::cerr << "Edge: " << edge << " -> Value: " << value << "\n"; + } +} + +TMOKhan20::TMOKhan20() { + SetName(L"Khan20"); + SetDescription(L"Tone-Mapping Using Perceptual-Quantizer and Image Histogram"); + + binParameter.SetName(L"Bins"); + binParameter.SetDescription(L"Labeled as N; the number of bins in the histogram"); + binParameter.SetDefault(256); + binParameter = UNDEF_INT_PARAM; + binParameter.SetRange(1, 100000); // max chosen arbitrarily + this->Register(binParameter); + truncationParameter.SetName(L"Truncation"); + truncationParameter.SetDescription( + L"Labeled as k, where `(k/N)*nof_pixels` is the maximum (truncated) count of pixels in one histogram bin" + ); + truncationParameter.SetDefault(5); + truncationParameter = UNDEF_INT_PARAM; + truncationParameter.SetRange(1, 100000); // max chosen arbitrarily + this->Register(truncationParameter); +} + +TMOKhan20::~TMOKhan20() += default; + +/** + * Apply tone mapping to the luminance values using LUT-based interpolation. + * + * @param luminance Input/output luminance values. Note that the input (HDR) luminance is in range [0, 1], after calling + * the function it is transformed (LDR) to [0, 255] + * @param lum_len Length of the luminance array. + * @param lut Precomputed luminance LUT. + */ +void TMOKhan20::ToneMap(std::vector &luminance, int lum_len, LuminanceLUT &lut) { + for (int i = 0; i < lum_len; i++){ + // Equation #6 in the paper + auto [k1, v1, k2, v2] = lut.getValue(luminance[i]); + if (v2 - v1 == 0) { + luminance[i] = v1; + } else if (k2 - k1 == 0) { + throw std::runtime_error("Trying to divide by zero in ToneMap method"); + } else { + luminance[i] = v1 + (v2 - v1) * (luminance[i] - k1) / (k2 - k1); + } + } +} + +/** + * Transform luminance using a PQ equation from the paper. + * + * @param pSrc Input image. + * @param luminance Output luminance values in range [0, 1] + * @return Tuple containing (min_luminance, max_luminance) in the output luminance vector. + */ +std::tuple TMOKhan20::ApplyPerceptualQuantizer(std::vector &luminance) { + double *pSourceData = pSrc->GetData(); + int k = 0; + double max_luminance = 0; + double min_luminance = 1; + for (int j = 0; j < pSrc->GetHeight(); j++) { + for (int i = 0; i < pSrc->GetWidth(); i++) { + double red = *pSourceData++; + double green = *pSourceData++; + double blue = *pSourceData++; + + // Equation #2 from the paper + double l_in = 0.2126 * red + 0.7152 * green + 0.0722 * blue; + if (l_in > 10000) { + l_in = 10000; + } else if (l_in < 0) { + l_in = 0; + } + + // Equation #1 from the paper + double luminance_out = ((107.0 + 2413.0 * pow(l_in / 10000.0, M)) / + (128.0 + 2392.0 * pow(l_in / 10000.0, M))); + luminance_out = pow(luminance_out, N); + + if (luminance_out > max_luminance) { + max_luminance = luminance_out; + } + if (luminance_out < min_luminance) { + min_luminance = luminance_out; + } + + luminance[k++] = luminance_out; + } + } + return std::make_tuple(min_luminance, max_luminance); +} + +/** + * Keeping the original color, just changing the luminance in Yxy model. This step wasn't properly discussed + * in the paper, so doing the final conversion to RGB what I consider to be the standard way. + * + * @param luminance the tone mapped luminance in [0, 255] range + */ +void TMOKhan20::ApplyTransformationToDstImage(std::vector luminance) { + pSrc->Convert(TMO_Yxy); + pDst->Convert(TMO_Yxy); + double *pSourceData = pSrc->GetData(); + double *destination = pDst->GetData(); + for (int i = 0; i < pSrc->GetWidth() * pDst->GetHeight(); i++) { + pSourceData++; + // Transforming to Yxy [0,1] range while copying to the output image + *destination++ = luminance[i] / 255; + *destination++ = *pSourceData++; + *destination++ = *pSourceData++; + } +} + +int TMOKhan20::Transform() { + if (binParameter == 0) binParameter = binParameter.GetDefault(); + if (truncationParameter == 0) truncationParameter = truncationParameter.GetDefault(); + + pSrc->Convert(TMO_RGB); + int luminance_len = pSrc->GetHeight() * pSrc->GetWidth(); + std::vector luminance(luminance_len); + auto [min_luminance, max_luminance] = TMOKhan20::ApplyPerceptualQuantizer(luminance); + + LuminanceLUT lut(binParameter, truncationParameter, luminance, luminance_len, min_luminance, max_luminance); + ToneMap(luminance, luminance_len, lut); + ApplyTransformationToDstImage(luminance); + return 0; +} + diff --git a/TMOKhan20/TMOKhan20.h b/TMOKhan20/TMOKhan20.h new file mode 100644 index 00000000..5c0b2608 --- /dev/null +++ b/TMOKhan20/TMOKhan20.h @@ -0,0 +1,30 @@ +#include "TMO.h" + +class LuminanceLUT { +private: + std::vector> lut; + +public: + LuminanceLUT(int bins, int truncation, const std::vector& luminance, + int lum_len, double min_luminance, double max_luminance); + + [[nodiscard]] std::tuple getValue(double key) const; + + [[maybe_unused]] void printLUT() const; +}; + +class TMOKhan20 : public TMO +{ +public: + TMOKhan20(); + virtual ~TMOKhan20(); + virtual int Transform(); + +protected: + TMOInt binParameter; + TMOInt truncationParameter; + + static void ToneMap(std::vector &luminance, int lum_len, LuminanceLUT &lut); + std::tuple ApplyPerceptualQuantizer(std::vector &luminance); + void ApplyTransformationToDstImage(std::vector luminance); +}; diff --git a/TMOKhan20/TMOPlugin.cpp b/TMOKhan20/TMOPlugin.cpp new file mode 100644 index 00000000..e9514e11 --- /dev/null +++ b/TMOKhan20/TMOPlugin.cpp @@ -0,0 +1,68 @@ +/* -------------------------------------------------------------------- * + * TMOPlugin.cpp : Template for tone mapping operator plugin * + * in Tone Mapping Studio 2004 * + * -------------------------------------------------------------------- */ +#include "./TMOPlugin.h" +#include "./TMOKhan20.h" + +/* -------------------------------------------------------------------- * + * Insert a number of implemented operators * + * -------------------------------------------------------------------- */ +int iOperatorCount = 1; + +/* -------------------------------------------------------------------- * + * DLL Entry point; no changes necessary * + * commenting out because TMOPlugin.cpp:16:1: error: ‘BOOL’ does not * + * name a type * + * -------------------------------------------------------------------- */ +/* BOOL APIENTRY DllMain(HANDLE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} +*/ + +/* -------------------------------------------------------------------- * + * Returns a number of implemented operators; no changes necessary * + * -------------------------------------------------------------------- */ +int TMOPLUGIN_API OperatorCount() +{ + return iOperatorCount; +} + +/* -------------------------------------------------------------------- * + * For each implemented operator create a new object in field operators,* + * then return number of created operators * + * For exemple : * + * * + * operators[0] = new TMOOperator1; * + * operators[1] = new TMOOperator2; * + * . * + * . * + * . * + * -------------------------------------------------------------------- */ +int TMOPLUGIN_API EnumOperators(TMO **operators) +{ + operators[0] = new TMOKhan20; + return iOperatorCount; +} + +/* -------------------------------------------------------------------- * + * Deletes operators; no changes necessary * + * -------------------------------------------------------------------- */ +int TMOPLUGIN_API DeleteOperators(TMO **operators) +{ + int i; + for (i = 0; i < iOperatorCount; i++) + delete operators[i]; + return iOperatorCount; +} diff --git a/TMOKhan20/TMOPlugin.h b/TMOKhan20/TMOPlugin.h new file mode 100644 index 00000000..8d8cd5a8 --- /dev/null +++ b/TMOKhan20/TMOPlugin.h @@ -0,0 +1,12 @@ +#include "TMO.h" + +// Just a drop-in replacement that ChatGPT suggested, otherwise it wouldn't compile +#ifdef _WIN32 +#define TMOPLUGIN_API __declspec(dllexport) +#else +#define TMOPLUGIN_API __attribute__((visibility("default"))) +#endif + +extern "C" TMOPLUGIN_API int EnumOperators(TMO **operators); +extern "C" TMOPLUGIN_API int DeleteOperators(TMO **operators); +extern "C" TMOPLUGIN_API int OperatorCount(); \ No newline at end of file diff --git a/tmolib/CMakeLists.txt b/tmolib/CMakeLists.txt index e9eeba16..d159321b 100644 --- a/tmolib/CMakeLists.txt +++ b/tmolib/CMakeLists.txt @@ -7,6 +7,10 @@ IF(UNIX AND NOT LINUX_SET) ADD_DEFINITIONS(-D LINUX) ENDIF(UNIX AND NOT LINUX_SET) +if(COMMAND cmake_policy) + cmake_policy(SET CMP0003 NEW) +endif(COMMAND cmake_policy) + FIND_PACKAGE(TIFF REQUIRED) FIND_PACKAGE(PNG REQUIRED)