diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e3f85d7 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +--- +BasedOnStyle: Google +ColumnLimit: '120' + +... diff --git a/.gitignore b/.gitignore index cefc70f..5d038fe 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ Build/* Generated/* + +.vscode/* \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2dd7178..43f2ac5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.8) set(CMAKE_CXX_STANDARD 17) @@ -28,8 +28,6 @@ else() endif() - - project(HDR LANGUAGES CXX) set(CMAKE_VERBOSE_MAKEFILE TRUE) @@ -51,14 +49,6 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR (CMAKE_CXX_COMPILER_ID MATCHES "C endif() -# set(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL) -# set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC) - - -# find_package(OpenCV REQUIRED) -# add_subdirectory(HDRI) - - #debug messages message("CMAKE_BUILD_TYPE= ${CMAKE_BUILD_TYPE}") message("CMAKE_SOURCE_DIR= ${CMAKE_SOURCE_DIR}") @@ -72,16 +62,13 @@ message("CMAKE_CXX_COMPILER_ID = ${CMAKE_CXX_COMPILER_ID}") message("CMAKE_GENERATOR= ${CMAKE_GENERATOR}") - set(BASE_SRC - HDRI/src/Common.cpp - HDRI/src/HDRImage.cpp - HDRI/src/LinearLeastSquares.cpp - HDRI/src/DebevecWeight.cpp - HDRI/src/rawImage.cpp - HDRI/src/ReinhardAlgo.cpp - HDRI/src/main.cpp + HDRI/hdr_image.cpp + HDRI/debevec_weight.cpp + HDRI/raw_image.cpp + HDRI/tone_map_algo.cpp + HDRI/main.cpp ) @@ -91,7 +78,6 @@ add_executable( ) include_directories(${HDRI_DIR}/include) -# include_directories( ${OpenCV_INCLUDE_DIRS} ) target_link_libraries( hdri ${CONAN_LIBS} ) target_compile_features(hdri PUBLIC cxx_std_17) diff --git a/HDRI/common.hpp b/HDRI/common.hpp new file mode 100644 index 0000000..b749fd6 --- /dev/null +++ b/HDRI/common.hpp @@ -0,0 +1,10 @@ + +#pragma once + +#include + +struct pixel { + std::uint8_t r; + std::uint8_t g; + std::uint8_t b; +}; diff --git a/HDRI/debevec_weight.cpp b/HDRI/debevec_weight.cpp new file mode 100644 index 0000000..1c9836d --- /dev/null +++ b/HDRI/debevec_weight.cpp @@ -0,0 +1,33 @@ +#include "debevec_weight.hpp" + +namespace { +constexpr int z_min = 0; +constexpr int z_max = 255; +constexpr int range = z_max - z_min + 1; +} // namespace + +namespace HDRI { + +debevec_weight::debevec_weight() { + table.resize(range); + for (auto i = 0; i < range; ++i) { + if (i <= (0.5 * (z_max + z_min))) { + table[i] = static_cast(i - z_min); + } else { + table[i] = static_cast(z_max - i); + } + } +} + +auto debevec_weight::get_size() const noexcept -> size_t { return range; } + +auto debevec_weight::get_weight(int index) const noexcept -> double { + // cap ? + + if (index < z_min || index > z_max) { + return 0; + } + return table[index]; +} + +} // namespace HDRI diff --git a/HDRI/debevec_weight.hpp b/HDRI/debevec_weight.hpp new file mode 100644 index 0000000..d691891 --- /dev/null +++ b/HDRI/debevec_weight.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace HDRI { + +class debevec_weight final { + public: + debevec_weight(); + [[nodiscard]] double get_weight(const int index) const noexcept; + [[nodiscard]] std::size_t get_size() const noexcept; + + private: + std::vector table; +}; + +} // namespace HDRI diff --git a/HDRI/hdr_image.cpp b/HDRI/hdr_image.cpp new file mode 100644 index 0000000..6639eb7 --- /dev/null +++ b/HDRI/hdr_image.cpp @@ -0,0 +1,179 @@ + +#include "hdr_image.hpp" + +#include +#include + +#include "common.hpp" +#include "debevec_weight.hpp" +#include "linear_least_square_solver.hpp" +#include "raw_image.hpp" + +namespace { + +template +cv::Mat construct_radiance(const std::vector &image_files, const std::array &curves, + weight_function &&weighting, const std::vector &expo) noexcept { + // Size + const auto width = image_files[0UL].get_width(); + const auto height = image_files[0UL].get_height(); + + // Radiance + cv::Mat radiance(height, width, CV_32FC3); + + for (auto idx = 0UL; idx < std::size(curves); ++idx) { // r, g, b + for (auto y = 0UL; y < height; ++y) { + for (auto x = 0UL; x < width; ++x) { + double weighted_sum = 0.0; + double result = 0.0; + + // loop over all images + for (auto k = 0UL; k < std::size(image_files); ++k) { + const auto color = static_cast(image_files[k].get_image_data().at(y, x)[idx]); + const double w = std::invoke(std::forward(weighting), color); + + result += static_cast(w * (curves[idx].at(color, 0) - std::log(expo[k]))); + weighted_sum += w; + } + + if (weighted_sum < std::numeric_limits::epsilon() && + weighted_sum > -std::numeric_limits::epsilon()) { // near 0.0 + radiance.at(y, x)[idx] = 0; + } else { + radiance.at(y, x)[idx] = std::exp(result / weighted_sum); + } + } + } + } + + return radiance; +} + +std::vector shrink_images(const std::vector &input) noexcept { + std::vector out; + out.reserve(std::size(input)); + + constexpr size_t ratio = 100UL; + + for (const auto &img : input) { + const auto &image_data = img.get_image_data(); + + size_t resized_col = image_data.cols / ratio; + size_t resized_row = image_data.rows / ratio; + + if (resized_col < 15UL) { + resized_col = 15UL; + } + + if (resized_row < 15UL) { + resized_row = 15UL; + } + + cv::Mat shrink_mat; + cv::resize(image_data, shrink_mat, cv::Size(image_data.cols, image_data.rows)); + cv::resize(shrink_mat, shrink_mat, cv::Size(resized_col, resized_row)); + + out.push_back(shrink_mat); + } + + return out; +} + +std::vector> generate_raw_pixel(const std::vector &shrink_mat) noexcept { + const auto width = shrink_mat[0UL].size().width; + const auto height = shrink_mat[0UL].size().height; + + std::vector> pixels(shrink_mat.size()); + + for (auto idx = 0UL; idx < shrink_mat.size(); ++idx) { + pixels[idx].resize(width * height); + + for (auto y = 0UL; y < height; ++y) { + for (auto x = 0UL; x < width; ++x) { + pixels[idx][y * width + x].b = shrink_mat[idx].at(y, x)[0UL]; + pixels[idx][y * width + x].g = shrink_mat[idx].at(y, x)[1UL]; + pixels[idx][y * width + x].r = shrink_mat[idx].at(y, x)[2UL]; + } + } + } + + return pixels; +} + +std::array>, 3UL> convert_to_z(const std::vector> &pixel, + const size_t image_size, const size_t num_images) noexcept { + std::array>, 3UL> z_values; // r, g, b + + for (auto &z_colors : z_values) { + z_colors.resize(image_size); + } + + for (size_t i = 0; i < image_size; ++i) { // image pixel + + z_values[0UL][i].resize(num_images); + z_values[1UL][i].resize(num_images); + z_values[2UL][i].resize(num_images); + + for (size_t j = 0; j < num_images; ++j) { // num of iamge + z_values[0UL][i][j] = pixel[j][i].b; + z_values[1UL][i][j] = pixel[j][i].g; + z_values[2UL][i][j] = pixel[j][i].r; + } + } + + return z_values; +} + +} // namespace + +namespace HDRI { + +hdr_image::hdr_image(const std::vector &raw_images) { + // set exposure + std::vector exposure; + exposure.reserve(std::size(raw_images)); + + for (const auto &raw_image : raw_images) { + exposure.push_back(raw_image.get_exposure()); + } + + compute_curves(raw_images, exposure); + compute_radiance(raw_images, exposure); +} + +const std::array &hdr_image::get_curves() const noexcept { return curves; } + +void hdr_image::compute_curves(const std::vector &raw_images, + const std::vector &exposure) noexcept { + const auto shrink_mat = shrink_images(raw_images); + std::vector> raw_pixel = generate_raw_pixel(shrink_mat); + + // convert + const auto z_values = convert_to_z(raw_pixel, shrink_mat[0UL].total(), shrink_mat.size()); + + debevec_weight dwf; + constexpr auto lambda = 10; + + const auto weight_function = [&dwf](const auto color_index) { return dwf.get_weight(color_index); }; + + std::array, 3UL> futures; + for (auto c = 0U; c < std::size(z_values); ++c) { + futures[c] = std::async(std::launch::async, linear_least_square_solver, weight_function, + z_values[c], exposure, dwf.get_size(), lambda); + } + + for (auto c = 0U; c < std::size(futures); ++c) { + curves[c] = futures[c].get(); + } +} + +void hdr_image::compute_radiance(const std::vector &raw_images, + const std::vector &exposure) noexcept { + debevec_weight dwf; + radiance = construct_radiance( + raw_images, curves, [&dwf](auto color_index) { return dwf.get_weight(color_index); }, exposure); +} + +const cv::Mat &hdr_image::get_radiance() const noexcept { return radiance; } + +} // namespace HDRI \ No newline at end of file diff --git a/HDRI/hdr_image.hpp b/HDRI/hdr_image.hpp new file mode 100644 index 0000000..986e3d1 --- /dev/null +++ b/HDRI/hdr_image.hpp @@ -0,0 +1,33 @@ + +#pragma once + +#include +#include + +namespace HDRI { +class raw_image; + +class hdr_image final { + public: + explicit hdr_image(const std::vector &raw_images); + + [[nodiscard]] const cv::Mat &get_radiance() const noexcept; + [[nodiscard]] const std::array &get_curves() const noexcept; + + template + [[nodiscard]] cv::Mat compute_tone_mapping(mapping_function &&algo) const noexcept; + + private: + void compute_curves(const std::vector &raw_images, const std::vector &expo) noexcept; + void compute_radiance(const std::vector &raw_images, const std::vector &expo) noexcept; + + std::array curves; + cv::Mat radiance; +}; + +template +cv::Mat hdr_image::compute_tone_mapping(mapping_function &&algo) const noexcept { + return std::invoke(std::forward(algo), radiance); +} + +} // namespace HDRI diff --git a/HDRI/include/Common.hpp b/HDRI/include/Common.hpp deleted file mode 100644 index ac61d46..0000000 --- a/HDRI/include/Common.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef _COMMON_HPP_ -#define _COMMON_HPP_ - -#include -#include - -#include "WeightFunction.hpp" -#include "rawImage.hpp" - -struct PixelData { - std::uint8_t r; - std::uint8_t g; - std::uint8_t b; -}; - -void loadRawImages(const std::string &basePath, const std::string &fileName, std::vector &images); -cv::Mat constructRadiance(const std::vector &imageFiles, const std::array &gCurves, - HDRI::WeightFunction &dwf, const std::vector &expo); - -float convertRGB(const float r, const float g, const float b); - -#endif diff --git a/HDRI/include/DebevecWeight.hpp b/HDRI/include/DebevecWeight.hpp deleted file mode 100644 index f2cb6a5..0000000 --- a/HDRI/include/DebevecWeight.hpp +++ /dev/null @@ -1,28 +0,0 @@ - -#ifndef _DEBEVEC_WEIGHT_HPP_ -#define _DEBEVEC_WEIGHT_HPP_ - -#include "WeightFunction.hpp" - -#include - -namespace HDRI { - -class DebevecWeight : public WeightFunction { - -public: - DebevecWeight(); - [[nodiscard]] double getWeight(int index) const override; - [[nodiscard]] std::size_t getSize() const override; - -private: - static const int kZmin; - static const int kZmax; - static const int N; - - std::vector mTable; -}; - -} // namespace HDRI - -#endif diff --git a/HDRI/include/HDRImage.hpp b/HDRI/include/HDRImage.hpp deleted file mode 100644 index cb49402..0000000 --- a/HDRI/include/HDRImage.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef _HDRI_HDRIMAGE_HPP_ -#define _HDRI_HDRIMAGE_HPP_ - -#include - -#include "Common.hpp" -#include "ToneMapAlgo.hpp" -#include "WeightFunction.hpp" -#include "rawImage.hpp" - -namespace HDRI { - -class HDRImage { - -public: - HDRImage() = default; - - void computeRadiance(const std::vector &imageFiles, const std::array &gCurves, - HDRI::WeightFunction &dwf, const std::vector &expo); - [[nodiscard]] cv::Mat getRadiance() const; - - void setToneMappingAlgorithm(ToneMapAlgo *algo); - - [[nodiscard]] cv::Mat getToneMappingResult() const; - -private: - cv::Mat mRadiance; - - ToneMapAlgo *mAlgoSelect = nullptr; -}; - -} // namespace HDRI - -#endif diff --git a/HDRI/include/LinearLeastSquares.hpp b/HDRI/include/LinearLeastSquares.hpp deleted file mode 100644 index c1cee94..0000000 --- a/HDRI/include/LinearLeastSquares.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef _HDRI_LINEARLEASTSQARES_HPP_ -#define _HDRI_LINEARLEASTSQARES_HPP_ - -#include "WeightFunction.hpp" -#include - -#include "rawImage.hpp" - -#include "Common.hpp" - -namespace HDRI { - -class LinearLeastSquares { - -public: - LinearLeastSquares() = delete; - - [[nodiscard]] static cv::Mat solver(const std::vector> &Z, const std::vector &deltaT, - const WeightFunction &wf, int lambda); -}; - -} // namespace HDRI - -#endif diff --git a/HDRI/include/ReinhardAlgo.hpp b/HDRI/include/ReinhardAlgo.hpp deleted file mode 100644 index a789cfd..0000000 --- a/HDRI/include/ReinhardAlgo.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef _HDRI_REINHARDALGO_HPP_ -#define _HDRI_REINHARDALGO_HPP_ - -#include "ToneMapAlgo.hpp" - -namespace HDRI { - -class ReinhardAlgo : public ToneMapAlgo { - -public: - [[nodiscard]] cv::Mat toneMap(const cv::Mat &inputRadiance) override; -}; - -} // namespace HDRI - -#endif diff --git a/HDRI/include/ToneMapAlgo.hpp b/HDRI/include/ToneMapAlgo.hpp deleted file mode 100644 index 4d71afa..0000000 --- a/HDRI/include/ToneMapAlgo.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef _HDRI_TONEMAPALGO_HPP_ -#define _HDRI_TONEMAPALGO_HPP_ - -#include - -namespace HDRI { - -class ToneMapAlgo { - -public: - virtual cv::Mat toneMap(const cv::Mat &inputRadiance) = 0; - virtual ~ToneMapAlgo() = default; -}; - -} // namespace HDRI - -#endif diff --git a/HDRI/include/WeightFunction.hpp b/HDRI/include/WeightFunction.hpp deleted file mode 100644 index c8a12bc..0000000 --- a/HDRI/include/WeightFunction.hpp +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef _HDRI_WEIGHTFUNCTION_HPP_ -#define _HDRI_WEIGHTFUNCTION_HPP_ - -#include - -namespace HDRI { - -class WeightFunction { - -public: - virtual double getWeight(int index) const = 0; - virtual std::size_t getSize() const = 0; - virtual ~WeightFunction() = default; -}; - -} // namespace HDRI - -#endif diff --git a/HDRI/include/rawImage.hpp b/HDRI/include/rawImage.hpp deleted file mode 100644 index 2fd5ebf..0000000 --- a/HDRI/include/rawImage.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef _HDRI_RAWIMAGE_HPP_ -#define _HDRI_RAWIMAGE_HPP_ - -#include - -namespace HDRI { - -class RawImage { - -public: - RawImage() = default; - - void load(const std::string &fileName, double ss); - - [[nodiscard]] size_t getTotalSize() const; - - [[nodiscard]] int getWidth() const; - [[nodiscard]] int getHeight() const; - - [[nodiscard]] const cv::Mat &getImageData() const; - - [[nodiscard]] double getExposure() const; - -private: - cv::Mat mImageData; - std::string mName; - double expo; -}; - -} // namespace HDRI - -#endif diff --git a/HDRI/linear_least_square_solver.hpp b/HDRI/linear_least_square_solver.hpp new file mode 100644 index 0000000..ff70ea8 --- /dev/null +++ b/HDRI/linear_least_square_solver.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +namespace HDRI { + +template +[[nodiscard]] cv::Mat linear_least_square_solver(weight_function &&weighting, + const std::vector> &z_value, + const std::vector &delta_time, const size_t range, + const int lambda) noexcept { + // zij , shutter , w, g, lE + // From the paper. + + /// NOTE: Ax = b + + const auto n = range; + + cv::Mat A = cv::Mat::zeros(z_value.size() * z_value[0UL].size() + n + 1, n + z_value.size(), CV_64F); + cv::Mat b = cv::Mat::zeros(A.size().height, 1, CV_64F); + + int k = 0; + for (auto i = 0; i < z_value.size(); ++i) { // N (pixel) + for (auto j = 0; j < z_value[i].size(); ++j) { // P (image) + + const auto w_ij = std::invoke(std::forward(weighting), z_value[i][j]); + A.at(k, z_value[i][j]) = w_ij; + A.at(k, n + i) = -w_ij; + b.at(k, 0) = w_ij * std::log(delta_time[j]); + + ++k; + } + } + + A.at(k, 128) = 1; // set the middle to Zero + ++k; + + // get x ! + + // g(z - 1) -2g(z) + g(z + 1) + for (auto i = 0; i < n - 1; ++i) { + const auto result = std::invoke(std::forward(weighting), i + 1); + + A.at(k, i) = lambda * result; + A.at(k, i + 1) = -2 * lambda * result; + A.at(k, i + 2) = lambda * result; + + ++k; + } + + cv::Mat x; + cv::solve(A, b, x, cv::DECOMP_SVD); + + // get g and lE + + return x; +} + +} // namespace HDRI diff --git a/HDRI/main.cpp b/HDRI/main.cpp new file mode 100644 index 0000000..b1854be --- /dev/null +++ b/HDRI/main.cpp @@ -0,0 +1,168 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "hdr_image.hpp" +#include "raw_image.hpp" +#include "tone_map_algo.hpp" + +namespace { + +// (1/shutter_speed) +/// NOTE: factor of two +constexpr std::array default_shutter_speed = {1 / 32, 1 / 16, 1 / 8, 1 / 4, 1 / 2, 1, 2, 4, + 8, 16, 32, 64, 128, 256, 512, 1024}; + +void output_curve(const cv::Mat &curve) noexcept { + const auto width = curve.size().width; + const auto height = curve.size().height; + + std::ofstream fout("out_curve.txt"); + + for (auto q = 0UL; q < height; ++q) { + for (auto p = 0UL; p < width; ++p) { + fout << curve.at(q, p) << std::endl; + } + } +} + +std::vector load_raw_images(const std::string &base_path, const std::string &file_name) noexcept { + const auto full_path = base_path + file_name; + + std::ifstream input(full_path); + + if (!input) { + std::cerr << "Cannot read input. Path: " << full_path << std::endl; + return {}; + } + + std::string line; + + bool num_flag = false; + std::size_t num_images = 0UL; + std::vector images; + + while (std::getline(input, line)) { + std::stringstream line_splitter(line); + char token{}; + + line_splitter >> token; + if (!line_splitter || token == '#') { + continue; + } + + line_splitter.putback(token); + + if (!num_flag) { + std::size_t num{}; + line_splitter >> num; + + // first number + num_images = num; + std::cout << "Number of images: " << num_images << std::endl; + num_flag = true; + + images.reserve(num_images); + + } else { + std::string image_file_name; + double inv_shutter_speed{}; + + line_splitter >> image_file_name >> inv_shutter_speed; + + std::cout << "file: " << image_file_name << " inv shutter speed: " << inv_shutter_speed << std::endl; + + // read image + images.emplace_back(base_path + image_file_name, inv_shutter_speed); + + if (std::size(images) == num_images) { + break; + } + } + } + + return images; +} + +} // namespace + +int main(int argc, char *argv[]) { + constexpr auto default_base_path{"../InputImage/"}; + constexpr auto default_files{"list.txt"}; + + std::string base_path; + if (argc > 1) { + base_path = argv[1]; + } else { + base_path = default_base_path; + } + + std::string file_list; + if (argc > 2) { + file_list = argv[2]; + } else { + file_list = default_files; + } + + const auto start_time = std::chrono::high_resolution_clock::now(); + + const auto raw_images = load_raw_images(base_path, file_list); + if (raw_images.empty()) { + std::cerr << "No input images" << std::endl; + return EXIT_FAILURE; + } + + /// HDR Image Creation + + std::cout << "Create HDR Image" << std::endl; + + HDRI::hdr_image hdr_image(raw_images); + + /// Tone Map + + std::cout << "Tone Mapping" << std::endl; + const auto tone_mapping_image = hdr_image.compute_tone_mapping(HDRI::reinhard_tone_map_algo); + + const auto end_time = std::chrono::high_resolution_clock::now(); + const auto time_span = std::chrono::duration_cast>(end_time - start_time); + + std::cout << "Execution time : " << time_span.count() << "s\n"; + + /// Output + + const auto &curves = hdr_image.get_curves(); + + std::cout << "Output Curves" << std::endl; + output_curve(curves[0UL]); + + constexpr auto output_radiance_file_name{"radiance.exr"}; + + std::cout << "Output HDR radiance" << std::endl; + + try { + cv::imwrite(output_radiance_file_name, hdr_image.get_radiance()); + } catch (std::exception &e) { + std::cerr << e.what() << '\n'; + std::exit(EXIT_FAILURE); + } + + constexpr auto output_image_file_name{"tone_mapping_image.jpg"}; + + std::cout << "Output Tone Mapping Image" << std::endl; + + try { + cv::imwrite(output_image_file_name, tone_mapping_image); + } catch (std::exception &e) { + std::cerr << e.what() << '\n'; + std::exit(EXIT_FAILURE); + } + + return EXIT_SUCCESS; +} diff --git a/HDRI/raw_image.cpp b/HDRI/raw_image.cpp new file mode 100644 index 0000000..bdc1baf --- /dev/null +++ b/HDRI/raw_image.cpp @@ -0,0 +1,28 @@ +#include "raw_image.hpp" + +#include +#include + +namespace HDRI { + +raw_image::raw_image(std::string file_name, const double shutter_speed) + : image_data{cv::imread(file_name)}, expo{1.0 / shutter_speed}, name{std::move(file_name)} { + if (image_data.empty()) { + std::cerr << "Fail to load: " + file_name << std::endl; + throw std::runtime_error("Fail to load" + file_name); + } + + std::cerr << "Loaded: " << file_name << std::endl; +} + +auto raw_image::get_total_size() const noexcept -> size_t { return image_data.total(); } + +auto raw_image::get_width() const noexcept -> int { return image_data.size().width; } + +auto raw_image::get_height() const noexcept -> int { return image_data.size().height; } + +auto raw_image::get_image_data() const noexcept -> const cv::Mat& { return image_data; } + +auto raw_image::get_exposure() const noexcept -> double { return expo; } + +} // namespace HDRI diff --git a/HDRI/raw_image.hpp b/HDRI/raw_image.hpp new file mode 100644 index 0000000..c1d5998 --- /dev/null +++ b/HDRI/raw_image.hpp @@ -0,0 +1,34 @@ + + +#pragma once + +#include + +namespace HDRI { + +class raw_image final { + public: + explicit raw_image(std::string file_name, const double shutter_speed); + + raw_image(const raw_image &) noexcept = default; + raw_image(raw_image &&) noexcept = default; + raw_image &operator=(const raw_image &) noexcept = default; + raw_image &operator=(raw_image &&) noexcept = default; + ~raw_image() = default; + + [[nodiscard]] size_t get_total_size() const noexcept; + + [[nodiscard]] int get_width() const noexcept; + [[nodiscard]] int get_height() const noexcept; + + [[nodiscard]] const cv::Mat &get_image_data() const noexcept; + + [[nodiscard]] double get_exposure() const noexcept; + + private: + cv::Mat image_data; + std::string name; + double expo; +}; + +} // namespace HDRI diff --git a/HDRI/src/Common.cpp b/HDRI/src/Common.cpp deleted file mode 100644 index 7d5f9a1..0000000 --- a/HDRI/src/Common.cpp +++ /dev/null @@ -1,115 +0,0 @@ - -#include "Common.hpp" - -#include "WeightFunction.hpp" -#include "rawImage.hpp" - -#include -#include -#include -#include -#include - -void loadRawImages(const std::string &basePath, const std::string &fileName, std::vector &images) { - - const std::string fullPath = basePath + fileName; - - std::ifstream inputStream(fullPath); - - if (!inputStream) { - throw std::runtime_error("Could not open file: " + fullPath); - } - - std::string lineString; - - bool numFlag = false; - - std::size_t numOfImages = 0; - std::size_t currentImageIndex = 0; - - while (std::getline(inputStream, lineString)) { - - std::stringstream lineSplitter(lineString); - char token{}; - - lineSplitter >> token; - if ((!lineSplitter) || token == '#') { - continue; - } - - lineSplitter.putback(token); - - if (!numFlag) { - - std::size_t num{}; - lineSplitter >> num; - - // first number - numOfImages = num; - std::cout << "Number of image: " << numOfImages << '\n'; - numFlag = true; - - images.resize(numOfImages); - - } else { - - std::string imageFileName; - double invShutterSpeed{}; - - lineSplitter >> imageFileName >> invShutterSpeed; - - std::cout << "file: " << imageFileName << " shutter: " << invShutterSpeed << std::endl; - - // read image - images[currentImageIndex++].load(basePath + imageFileName, invShutterSpeed); - - if (currentImageIndex == numOfImages) { - break; - } - } - } -} - -auto constructRadiance(const std::vector &imageFiles, const std::array &gCurves, - HDRI::WeightFunction &dwf, const std::vector &expo) -> cv::Mat { - - // Size ? - int width = imageFiles[0].getWidth(); - int height = imageFiles[0].getHeight(); - - // radiance ? - cv::Mat hdrImg(height, width, CV_32FC3); - - for (auto idx = 0; idx < gCurves.size(); ++idx) { // r, g, b - for (auto y = 0; y < height; ++y) { - for (auto x = 0; x < width; ++x) { - - double weightedSum = 0.0; - double result = 0.0; - - // loop over all imgs - for (auto k = 0U; k < imageFiles.size(); ++k) { - - auto pixelColor = static_cast(imageFiles[k].getImageData().at(y, x)[idx]); - double w = dwf.getWeight(pixelColor); - - result += static_cast(w * (gCurves[idx].at(pixelColor, 0) - std::log(expo[k]))); - weightedSum += w; - } - - if (weightedSum < std::numeric_limits::epsilon() && - weightedSum > -std::numeric_limits::epsilon()) { // near 0.0 - hdrImg.at(y, x)[idx] = 0; - } else { - hdrImg.at(y, x)[idx] = std::exp(result / weightedSum); - } - } - } - } - - return hdrImg; -} - -auto convertRGB(const float r, const float g, const float b) -> float { - return 0.2126f * r + 0.7152f * g + 0.0722f * b; -} diff --git a/HDRI/src/DebevecWeight.cpp b/HDRI/src/DebevecWeight.cpp deleted file mode 100644 index f440570..0000000 --- a/HDRI/src/DebevecWeight.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "DebevecWeight.hpp" - -#include - -namespace HDRI { - -const int DebevecWeight::kZmin = 0; -const int DebevecWeight::kZmax = 255; -const int DebevecWeight::N = 256; - -DebevecWeight::DebevecWeight() { - - mTable.resize(N); - for (auto i = 0; i < N; ++i) { - - if (i <= (0.5 * (kZmax + kZmin))) { - mTable[i] = static_cast(i - kZmin); - } else { - mTable[i] = static_cast(kZmax - i); - } - } - - std::cerr << N << std::endl; -} - -auto DebevecWeight::getSize() const -> size_t { return N; } - -auto DebevecWeight::getWeight(int index) const -> double { - - // cap ? - - if (index < kZmin || index > kZmax) { - return 0; - } - return mTable[index]; -} - -} // namespace HDRI diff --git a/HDRI/src/HDRImage.cpp b/HDRI/src/HDRImage.cpp deleted file mode 100644 index 87f4a48..0000000 --- a/HDRI/src/HDRImage.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "HDRImage.hpp" - -#include - -void HDRI::HDRImage::computeRadiance(const std::vector &imageFiles, - const std::array &gCurves, HDRI::WeightFunction &dwf, - const std::vector &expo) { - - mRadiance = constructRadiance(imageFiles, gCurves, dwf, expo); -} - -auto HDRI::HDRImage::getRadiance() const -> cv::Mat { return mRadiance; } - -void HDRI::HDRImage::setToneMappingAlgorithm(ToneMapAlgo *algo) { mAlgoSelect = algo; } - -auto HDRI::HDRImage::getToneMappingResult() const -> cv::Mat { - - if (mAlgoSelect != nullptr) { - return mAlgoSelect->toneMap(mRadiance); - } - - std::cerr << "No algorithm selected\n"; - - return cv::Mat(); -} diff --git a/HDRI/src/LinearLeastSquares.cpp b/HDRI/src/LinearLeastSquares.cpp deleted file mode 100644 index 93fdb14..0000000 --- a/HDRI/src/LinearLeastSquares.cpp +++ /dev/null @@ -1,53 +0,0 @@ - -#include - -#include "LinearLeastSquares.hpp" - -auto HDRI::LinearLeastSquares::solver(const std::vector> &Z, const std::vector &deltaT, - const WeightFunction &wf, int lambda) -> cv::Mat { // zij , shutter , w, g, lE - - // tmp - // int lambda = 1; - // From the paper. - - // Note : Ax = b - - auto n = wf.getSize(); - - cv::Mat A = cv::Mat::zeros(Z.size() * Z[0].size() + n + 1, n + Z.size(), CV_64F); - cv::Mat b = cv::Mat::zeros(A.size().height, 1, CV_64F); - - int k = 0; - for (auto i = 0; i < Z.size(); ++i) { // N (pixel) - for (auto j = 0; j < Z[i].size(); ++j) { // P (image) - - double w_ij = wf.getWeight(Z[i][j]); // +1 ?? - A.at(k, Z[i][j]) = w_ij; - A.at(k, n + i) = -w_ij; - b.at(k, 0) = w_ij * std::log(deltaT[j]); - - ++k; - } - } - - A.at(k, 128) = 1; // set the middle to Zero - ++k; - - // get x ! - - // g(z - 1) -2g(z) + g(z + 1) - for (auto i = 0; i < n - 1; ++i) { - A.at(k, i) = lambda * wf.getWeight(i + 1); - A.at(k, i + 1) = -2 * lambda * wf.getWeight(i + 1); - A.at(k, i + 2) = lambda * wf.getWeight(i + 1); - ++k; - } - - // cv::Mat x(A.rows, 1, CV_64F); - cv::Mat x; - cv::solve(A, b, x, cv::DECOMP_SVD); - - // get g and lE - - return x; -} diff --git a/HDRI/src/ReinhardAlgo.cpp b/HDRI/src/ReinhardAlgo.cpp deleted file mode 100644 index 8b4cb09..0000000 --- a/HDRI/src/ReinhardAlgo.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "ReinhardAlgo.hpp" - -#include "Common.hpp" - -#include - -auto HDRI::ReinhardAlgo::toneMap(const cv::Mat &inputRadiance) -> cv::Mat { - - const float epsilon = 0.00001f; - const float a = 0.18f; // from paper - - // color space transform - cv::Mat lumi(inputRadiance.size(), CV_32FC1); - for (auto y = 0U; y < inputRadiance.size().height; ++y) { - for (auto x = 0U; x < inputRadiance.size().width; ++x) { - - // L = 0.2126 * R + 0.7152 * G + 0.0722 * B; - // lumi.at(y, x) = 0.2126 * inputRadiance.at(y, - // x)[2] + 0.7152 * inputRadiance.at(y, x)[1] + 0.0722 * - // inputRadiance.at(y, x)[0]; - lumi.at(y, x) = convertRGB(inputRadiance.at(y, x)[2], inputRadiance.at(y, x)[1], - inputRadiance.at(y, x)[0]); - } - } - - double Lw_bar = 0.0; - - // loop over all values in the Mat - for (auto y = 0U; y < inputRadiance.size().height; ++y) { - for (auto x = 0U; x < inputRadiance.size().width; ++x) { - Lw_bar += std::log(lumi.at(y, x) + epsilon); // from paper - } - } - - std::cerr << "Lw_bar: " << Lw_bar << std::endl; - - const size_t N = inputRadiance.total(); - - // Equation 1 in the paper is wrong. The division by N should be placed - // before the summation, not outside the exponentiation. - Lw_bar = std::exp(Lw_bar / N); - - float coeff = a / Lw_bar; - - float L_white = 1.7f; // test - - // compute Ld - cv::Mat Ld(inputRadiance.size(), CV_32FC1); - - for (auto y = 0; y < inputRadiance.size().height; ++y) { - for (auto x = 0; x < inputRadiance.size().width; ++x) { - - float L = coeff * lumi.at(y, x); // Ld = (a / Lw_bar ) * (Lw(x,y)) - Ld.at(y, x) = L * (1.0f + L / (L_white * L_white)) / (1.0f + L); - - // Ld.at(y, x) = L; - - if (L > L_white) { - Ld.at(y, x) = 1; - } - } - } - - cv::Mat outputImage(inputRadiance.size(), CV_8UC3); - for (auto idx = 0U; idx < 3U; ++idx) { // rgb - for (auto y = 0; y < inputRadiance.size().height; ++y) { - for (auto x = 0; x < inputRadiance.size().width; ++x) { - outputImage.at(y, x)[idx] = cv::saturate_cast( - inputRadiance.at(y, x)[idx] * (Ld.at(y, x) * 255.0 / lumi.at(y, x))); - } - } - } - - return outputImage; -} diff --git a/HDRI/src/main.cpp b/HDRI/src/main.cpp deleted file mode 100644 index de1c9d6..0000000 --- a/HDRI/src/main.cpp +++ /dev/null @@ -1,225 +0,0 @@ - -#include "Common.hpp" -#include "DebevecWeight.hpp" -#include "HDRImage.hpp" -#include "LinearLeastSquares.hpp" -#include "ReinhardAlgo.hpp" -#include "rawImage.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -const std::string kDefaultBasePath = "../InputImage/"; -const std::string kDefaultFileList = "list.txt"; - -// (1/shutter_speed) -// Note: factor of two -static constexpr std::array defaultShutterSpeed = {1 / 32, 1 / 16, 1 / 8, 1 / 4, 1 / 2, 1, 2, 4, - 8, 16, 32, 64, 128, 256, 512, 1024}; - -static void outputCurve(const cv::Mat &curve) { - auto tmpW = curve.size().width; - auto tmpH = curve.size().height; - - std::ofstream fout("out.txt"); - - for (auto q = 0U; q < tmpH; ++q) { - for (auto p = 0U; p < tmpW; ++p) { - fout << curve.at(q, p) << std::endl; - } - } -} - -static auto shrinkImages(const std::vector &in) -> std::vector { - - std::vector out; - - const size_t kRatio = 100; - - for (const auto &img : in) { - - const auto &ref = img.getImageData(); - - // size_t resizeCol = ref.cols / kRatio; - // size_t resizeRow = ref.rows / kRatio; - - int resizeCol = 15; - int resizeRow = 15; - - if (resizeCol < 15) { - resizeCol = 15; - } - - if (resizeRow < 15) { - resizeRow = 15; - } - - std::cerr << "sample Size:" << resizeCol << " " << resizeRow << '\n'; - - cv::Mat shrinkMat; - cv::resize(ref, shrinkMat, cv::Size(ref.cols, ref.rows)); - cv::resize(shrinkMat, shrinkMat, cv::Size(resizeCol, resizeRow)); - - out.push_back(shrinkMat); - } - - return out; -} - -static auto generateRawPixelData(const std::vector &shrinkMat) -> std::vector> { - - auto width = shrinkMat[0].size().width; - auto height = shrinkMat[0].size().height; - - std::vector> pixelRaw(shrinkMat.size()); - - for (auto idx = 0U; idx < shrinkMat.size(); ++idx) { - - pixelRaw[idx].resize(width * height); - for (auto y = 0; y < height; ++y) { - for (auto x = 0; x < width; ++x) { - - pixelRaw[idx][y * width + x].b = shrinkMat[idx].at(y, x)[0]; - pixelRaw[idx][y * width + x].g = shrinkMat[idx].at(y, x)[1]; - pixelRaw[idx][y * width + x].r = shrinkMat[idx].at(y, x)[2]; - } - } - } - - return pixelRaw; -} - -static auto convertToZ(const std::vector> &pixelRaw, const size_t imageSize, - const size_t numOfImage) -> std::array>, 3> { - - std::array>, 3> Z; // r, g, b - - for (auto &zColors : Z) { - zColors.resize(imageSize); - } - - for (size_t i = 0; i < imageSize; ++i) { // image pixel - - Z[0][i].resize(numOfImage); - Z[1][i].resize(numOfImage); - Z[2][i].resize(numOfImage); - - for (size_t j = 0; j < numOfImage; ++j) { // num of iamge - Z[0][i][j] = pixelRaw[j][i].b; - Z[1][i][j] = pixelRaw[j][i].g; - Z[2][i][j] = pixelRaw[j][i].r; - } - } - - return Z; -} - -auto main(int argc, char *argv[]) -> int { - - std::vector imageFiles; - - std::string basePath; - if (argc > 1) { - basePath = argv[1]; - } else { - basePath = kDefaultBasePath; - } - - std::string fileList; - if (argc > 2) { - fileList = argv[2]; - } else { - fileList = kDefaultFileList; - } - - auto start_time = std::chrono::high_resolution_clock::now(); - - try { - loadRawImages(basePath, fileList, imageFiles); - } catch (std::exception &e) { - std::cerr << e.what() << std::endl; - std::exit(-1); - } - - HDRI::DebevecWeight dwf; - - auto shrinkMat = shrinkImages(imageFiles); - std::vector> pixelRaw = generateRawPixelData(shrinkMat); - - std::cerr << "Convert\n"; - - // convert - auto Z = convertToZ(pixelRaw, shrinkMat[0].total(), shrinkMat.size()); - - // set exp - std::vector expo; - expo.reserve(imageFiles.size()); - - for (const auto &img : imageFiles) { - expo.push_back(img.getExposure()); - } - - std::cerr << "Linear Least Squares\n"; - constexpr int lambda = 10; - std::array gCurves; // R, G, B - - std::array, 3> gFutures; - for (auto c = 0U; c < Z.size(); ++c) { - gFutures[c] = std::async(std::launch::async, HDRI::LinearLeastSquares::solver, Z[c], expo, dwf, lambda); - std::cerr << "Async Compute\n"; - } - - for (auto c = 0U; c < Z.size(); ++c) { - gCurves[c] = gFutures[c].get(); - } - - std::cerr << "Done\n"; - - // test - outputCurve(gCurves[0]); - - std::cerr << "Compute radiance\n"; - // radiance ? - // cv::Mat hdrImg = constructRadiance(imageFiles, gCurves, dwf, expo); - - HDRI::HDRImage hdrImage; - hdrImage.computeRadiance(imageFiles, gCurves, dwf, expo); - - try { - cv::imwrite("radiance.hdr", hdrImage.getRadiance()); - } catch (std::exception &e) { - std::cerr << e.what() << '\n'; - std::exit(-1); - } - - // tone map - - std::cerr << "tone map\n"; - - HDRI::ReinhardAlgo rAlgo; - - hdrImage.setToneMappingAlgorithm(&rAlgo); - auto outimage = hdrImage.getToneMappingResult(); - - try { - cv::imwrite("output_image.jpg", outimage); - } catch (std::exception &e) { - std::cerr << e.what() << '\n'; - std::exit(-1); - } - - auto end_time = std::chrono::high_resolution_clock::now(); - auto time_span = std::chrono::duration_cast>(end_time - start_time); - - std::cout << "Execution time : " << time_span.count() << "s\n"; - - return 0; -} diff --git a/HDRI/src/rawImage.cpp b/HDRI/src/rawImage.cpp deleted file mode 100644 index 0668db1..0000000 --- a/HDRI/src/rawImage.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "rawImage.hpp" -#include - -#include - -namespace HDRI { - -void RawImage::load(const std::string &fileName, double ss) { - - mImageData = cv::imread(fileName); - expo = 1 / ss; - mName = fileName; - - if (mImageData.empty()) { - std::cerr << "Fail to load: " + fileName << std::endl; - throw std::runtime_error("Fail to load" + fileName); - } - std::cerr << "Loaded: " << fileName << std::endl; -} - -auto RawImage::getTotalSize() const -> size_t { return mImageData.total(); } - -auto RawImage::getWidth() const -> int { return mImageData.size().width; } - -auto RawImage::getHeight() const -> int { return mImageData.size().height; } - -auto RawImage::getImageData() const -> const cv::Mat & { return mImageData; } - -auto RawImage::getExposure() const -> double { return expo; } - -} // namespace HDRI diff --git a/HDRI/tone_map_algo.cpp b/HDRI/tone_map_algo.cpp new file mode 100644 index 0000000..c549dfe --- /dev/null +++ b/HDRI/tone_map_algo.cpp @@ -0,0 +1,77 @@ +#include "tone_map_algo.hpp" + +#include + +namespace { + +constexpr float rgb_to_luminance(const float r, const float g, const float b) noexcept { + return 0.2126f * r + 0.7152f * g + 0.0722f * b; +} + +} // namespace + +namespace HDRI { + +auto reinhard_tone_map_algo(const cv::Mat &input_radiance) noexcept -> cv::Mat { + constexpr float epsilon = 0.00001f; + constexpr float a = 0.18f; // from paper + + // color space transform + cv::Mat lumi(input_radiance.size(), CV_32FC1); + for (auto y = 0U; y < input_radiance.size().height; ++y) { + for (auto x = 0U; x < input_radiance.size().width; ++x) { + lumi.at(y, x) = + rgb_to_luminance(input_radiance.at(y, x)[2], input_radiance.at(y, x)[1], + input_radiance.at(y, x)[0]); + } + } + + double Lw_bar = 0.0; + + // loop over all values in the Mat + for (auto y = 0U; y < input_radiance.size().height; ++y) { + for (auto x = 0U; x < input_radiance.size().width; ++x) { + Lw_bar += std::log(lumi.at(y, x) + epsilon); // from paper + } + } + + std::cerr << "Lw_bar: " << Lw_bar << std::endl; + + const size_t N = input_radiance.total(); + + // Equation 1 in the paper is wrong. The division by N should be placed + // before the summation, not outside the exponentiation. + Lw_bar = std::exp(Lw_bar / N); + + const float coeff = a / Lw_bar; + + const float L_white = 1.7f; // test + + // compute Ld + cv::Mat Ld(input_radiance.size(), CV_32FC1); + + for (auto y = 0; y < input_radiance.size().height; ++y) { + for (auto x = 0; x < input_radiance.size().width; ++x) { + float L = coeff * lumi.at(y, x); // Ld = (a / Lw_bar ) * (Lw(x,y)) + Ld.at(y, x) = L * (1.0f + L / (L_white * L_white)) / (1.0f + L); + + if (L > L_white) { + Ld.at(y, x) = 1; + } + } + } + + cv::Mat output_image(input_radiance.size(), CV_8UC3); + for (auto idx = 0U; idx < 3U; ++idx) { // rgb + for (auto y = 0; y < input_radiance.size().height; ++y) { + for (auto x = 0; x < input_radiance.size().width; ++x) { + output_image.at(y, x)[idx] = cv::saturate_cast( + input_radiance.at(y, x)[idx] * (Ld.at(y, x) * 255.0 / lumi.at(y, x))); + } + } + } + + return output_image; +} + +} // namespace HDRI \ No newline at end of file diff --git a/HDRI/tone_map_algo.hpp b/HDRI/tone_map_algo.hpp new file mode 100644 index 0000000..4d64aa0 --- /dev/null +++ b/HDRI/tone_map_algo.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace HDRI { + +[[nodiscard]] cv::Mat reinhard_tone_map_algo(const cv::Mat &input_radiance) noexcept; + +} // namespace HDRI diff --git a/README.md b/README.md index ab12fdc..6a62bca 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,20 @@ High-dynamic-range imaging ## System requirements and Dependencies -- [CMake 3.6+](https://cmake.org/) -- [Conan 1.20.3+](https://conan.io/) +- [CMake 3.8+](https://cmake.org/) +- [Conan 1.40+](https://conan.io/) The following compilers are tested: - Visual Studio 2017 (Windows x64) - GCC 5.5.0 (Linux x64) -- Apple LLVM version 11.0.0 (Mac x64) - +- Apple LLVM version 13.0.0 (Mac x64) ## Build ### Windows & Mac OS X & Linux -I have recently changed from premake to [CMake](https://cmake.org/). -Below are instructions of using a GCC-based compiler as an example. - -1. Create your build directory `mkdir build && cd build` +1. Create the build directory `mkdir build && cd build` 2. Run Conan `conan install .. -s compiler.cppstd=17 --build missing` . The dependencies should be resolved by conan 3. Run CMake `cmake ..` for development or `cmake -DCMAKE_BUILD_TYPE=Release` for a release build 4. Compile by running `make` @@ -33,7 +29,7 @@ Note that for Windows platform you may need to copy necessary files such as dlls ## Usage -`./hdri (BaseDirPath) (FileListName)` +`./hdri [BaseDirPath] [FileListName]` For example: @@ -41,8 +37,8 @@ For example: Note: The default base path is "../InputImage/" and the default name of the file list is "list.txt". You do not have to type it explicitly. -The program will generate the radiance map called "radiance.hdr". Based on the radiance map, the tone algorithm will be triggered to generate the output image. The file name will be "output_image.jpg". -Moreover, the reconstructed G curve (and ln E) will be written to a file called "out.txt". +The program will first generate the radiance map. After that, it will generate the output image using tone mapping. +Moreover, the reconstructed G curve (and ln E) will be written to a file. ## Layout diff --git a/conanfile.txt b/conanfile.txt index 0abb409..18587e2 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,5 +1,5 @@ [requires] -opencv/4.1.1@conan/stable +opencv/4.5.3 [generators] cmake