From 3fff8ce15b9490542c058420667630f7498ad4e7 Mon Sep 17 00:00:00 2001 From: Jerome Date: Sat, 6 May 2023 10:01:28 +0200 Subject: [PATCH] Parallelized Image + main program skeleton. --- cpp/include/Image.hpp | 4 + cpp/include/Slicer360ToPerspective.hpp | 15 ++- cpp/src/Image.cpp | 69 +++++++++-- cpp/src/Slicer360ToPerspective.cpp | 43 +++---- cpp/src/main.cpp | 165 +++++++++++++++++++++++++ cpp/src/main.cpp.nope | 6 - cpp/src/{tests.cpp => tests.cpp.nope} | 0 7 files changed, 252 insertions(+), 50 deletions(-) create mode 100644 cpp/src/main.cpp delete mode 100644 cpp/src/main.cpp.nope rename cpp/src/{tests.cpp => tests.cpp.nope} (100%) diff --git a/cpp/include/Image.hpp b/cpp/include/Image.hpp index 3a8f362..786ee18 100644 --- a/cpp/include/Image.hpp +++ b/cpp/include/Image.hpp @@ -159,6 +159,10 @@ class Image { /// @return Reference to the image. Image Colorized(unsigned char const* colormap) const; + std::tuple ComputeMinMax() const; + + std::tuple ComputeMinMaxAvg() const; + /// @brief Computes the histogram of the image. /// All channels are used to compute the histogram. If the downscale factor is a multiple of the number of channels, the histogram will be skewed and will only use the first channel. /// @param downscale_factor Factor by which the image is downsampled before computing the histogram. diff --git a/cpp/include/Slicer360ToPerspective.hpp b/cpp/include/Slicer360ToPerspective.hpp index 8f84abf..066eae5 100644 --- a/cpp/include/Slicer360ToPerspective.hpp +++ b/cpp/include/Slicer360ToPerspective.hpp @@ -9,7 +9,6 @@ /// The virtual cameras are added to the slicer using the AddCamera() method. /// The output images are saved in the folder specified by the SetOutputFolder() method. /// By default, the ProjectToCameras and ProjectToAllCameras are parallelized using OpenMP. -/// If the DO_NOT_USE_OPENMP macro is defined, the OpenMP parallization is disabled and the computation is sequential regardless of the value of "parallel". class Slicer360ToPerspective { public: /// @brief Builds a Slicer360ToPerspective object using a 360-degree image. @@ -32,6 +31,12 @@ class Slicer360ToPerspective { /// @return A reference to the current object. Slicer360ToPerspective & SetOutputFolder(std::string const& folder); + /// @brief Sets the cameras used to convert the 360-degree image to perspective images. + /// @details The camera vector is directly accessible through the public "cameras" attribute. This method is provided for convenience and consistency. + /// @param cameras_ A vector of cameras used to project the 360-degree image. + /// @return A reference to the current object. + Slicer360ToPerspective & SetCameras(std::vector const& cameras_); + /// @brief Computes the coverage of the 360-degree image by the cameras added to the slicer. /// @param width Width of the generated 360-degree image containing the results of the analysis. /// @param raw If true, the output is the number of cameras that see the pixel of the 360 image (8-bit monochrome image). If false, the output is a colored RGB image. @@ -41,18 +46,16 @@ class Slicer360ToPerspective { /// @brief Projects the 360-degree image to a perspective image using the specified camera. /// @param camera The camera used to project the 360-degree image. /// @return The generated perspective image. - Image ProjectToCamera(Camera const& camera, bool parallel = true) const; + Image ProjectToCamera(Camera const& camera) const; /// @brief Projects the 360-degree image to perspective images using the specified cameras. /// @param cameras A vector of cameras used to project the 360-degree image. - /// @param parallel If true, the projection is done in parallel using all the available cores. /// @return A vector of generated perspective images. - std::vector ProjectToCameras(std::vector const& cameras_, bool parallel = true) const; + std::vector ProjectToCameras(std::vector const& cameras_) const; /// @brief Projects the 360-degree image to perspective images using all the cameras added to the slicer. - /// @param parallel If true, the projection is done in parallel using all the available cores. /// @return A vector of generated perspective images. - std::vector ProjectToAllCameras(bool parallel = true) const; + std::vector ProjectToAllCameras() const; /// @brief Loads the cameras from a CSV file. /// @details The CSV file should contain one camera per row and the columns should be in the following order: diff --git a/cpp/src/Image.cpp b/cpp/src/Image.cpp index 645872c..0a48911 100644 --- a/cpp/src/Image.cpp +++ b/cpp/src/Image.cpp @@ -1,4 +1,5 @@ #include +#include Image::Image(int width_, int height_, int depth_) : width(width_), height(height_), depth(depth_) { data = new unsigned char[width * height * depth]; @@ -76,6 +77,7 @@ Image & Image::Fill(unsigned char value) { } Image & Image::Fill(Eigen::Vector3i const& rgb) { + #pragma omp parallel for for(int i = 0 ; i < height ; i++) for(int j = 0 ; j < width ; j++) SetPixel(i, j, rgb); @@ -137,6 +139,7 @@ unsigned char Image::operator[](int i) const { Image Image::Downsampled(int factor) const { factor = std::max(factor, 1); Image downsampled(width / factor, height / factor, depth); + #pragma omp parallel for for(int i = 0 ; i < downsampled.GetHeight() ; i++) for(int j = 0 ; j < downsampled.GetWidth() ; j++) downsampled.SetPixel(i, j, GetPixel(i * factor, j * factor)); @@ -145,6 +148,7 @@ Image Image::Downsampled(int factor) const { Image Image::Grayscale() const { Image grayscale(width, height, 1); + #pragma omp parallel for for(int i = 0 ; i < height ; i++) for(int j = 0 ; j < width ; j++) { Eigen::Vector3i rgb = GetPixel(i, j); @@ -155,6 +159,7 @@ Image Image::Grayscale() const { Image Image::LumaREC709() const { Image grayscale(width, height, 1); + #pragma omp parallel for for(int i = 0 ; i < height ; i++) for(int j = 0 ; j < width ; j++) { Eigen::Vector3i rgb = GetPixel(i, j); @@ -170,6 +175,7 @@ Image & Image::HistogramNormalize(int downsampling_factor) { std::vector cumul_hist = CumulativeHistogram(hist); // Finally, normalize the image uint64_t max_cum_hist_val = cumul_hist[cumul_hist.size()-1]; + #pragma omp parallel for for(int i = 0 ; i < height ; i++) for(int j = 0 ; j < width ; j++) { Eigen::Vector3i rgb = GetPixel(i, j); @@ -186,18 +192,15 @@ Image Image::HistogramNormalized(int downsampling_factor) const { Image & Image::Normalize() { // First, compute the min and max values using all channels - unsigned char min_val = 255, max_val = 0; - for(int i = 0 ; i < width*height*depth ; i++) { - if(data[i] < min_val) - min_val = data[i]; - else if(data[i] > max_val) - max_val = data[i]; - } + auto [min_val, max_val] = ComputeMinMax(); // Then, normalize the image - for(int i = 0 ; i < width*height*depth ; i++) - data[i] = ((int)data[i] - min_val) * 255 / (max_val - min_val); - + if(max_val != min_val) { + #pragma omp parallel for + for(int i = 0 ; i < width*height*depth ; i++) + data[i] = ((int)data[i] - min_val) * 255 / (max_val - min_val); + } + return *this; } @@ -211,6 +214,7 @@ Image Image::Colorized(unsigned char const* colormap) const { if (depth != 1) return *this; Image colorized(width, height, 3); + #pragma omp parallel for for(int i = 0 ; i < height ; i++) for(int j = 0 ; j < width ; j++) for(int c = 0 ; c < 3 ; c++) @@ -218,10 +222,53 @@ Image Image::Colorized(unsigned char const* colormap) const { return colorized; } +std::tuple Image::ComputeMinMax() const { + unsigned char min_val = 255, max_val = 0; + #pragma omp parallel for reduction(min:min_val) reduction(max:max_val) + for(int i = 0 ; i < width*height*depth ; i++) { + if(data[i] < min_val) + min_val = data[i]; + else if(data[i] > max_val) + max_val = data[i]; + } + return std::make_tuple(min_val, max_val); +} + +std::tuple Image::ComputeMinMaxAvg() const +{ + unsigned char min_val = 255, max_val = 0; + uint64_t avg_val = 0; + #pragma omp parallel for reduction(min:min_val) reduction(max:max_val) reduction(+:avg_val) + for(int i = 0 ; i < width*height*depth ; i++) { + if(data[i] < min_val) + min_val = data[i]; + else if(data[i] > max_val) + max_val = data[i]; + avg_val += data[i]; + } + return std::make_tuple(min_val, max_val, ((double)avg_val)/(width*height*depth)); +} + std::vector Image::ComputeHistogram(int downsampling_factor) const { + downsampling_factor = std::max(downsampling_factor, 1); std::vector hist(256, 0); - for(int i = 0 ; i < width*height*depth ; i += downsampling_factor) + + // OpenMP locks + omp_lock_t locks[256]; + for(int i = 0 ; i < 256 ; i++) + omp_init_lock(&(locks[i])); + + #pragma omp parallel for shared(hist, data, locks, downsampling_factor) default(none) + for(int i = 0 ; i < width*height*depth ; i += downsampling_factor) { + omp_set_lock(&(locks[data[i]]));// Protect the histogram's bin during the write hist[data[i]]++; + omp_unset_lock(&(locks[data[i]])); + } + + // Free OpenMP locks + for(int i = 0 ; i < 256 ; i++) + omp_destroy_lock(&(locks[i])); + return hist; } diff --git a/cpp/src/Slicer360ToPerspective.cpp b/cpp/src/Slicer360ToPerspective.cpp index 5b457af..7f94749 100644 --- a/cpp/src/Slicer360ToPerspective.cpp +++ b/cpp/src/Slicer360ToPerspective.cpp @@ -3,9 +3,7 @@ #include #include -#ifndef DO_NOT_USE_OPENMP #include -#endif Slicer360ToPerspective::Slicer360ToPerspective(Image const& panorama_image_) : panorama_image(panorama_image_), output_image_width(2), interpolation_method(Image::InterpMethod::BILINEAR), output_folder(".") {} @@ -26,11 +24,17 @@ Slicer360ToPerspective & Slicer360ToPerspective::SetOutputFolder(std::string con return *this; } +Slicer360ToPerspective & Slicer360ToPerspective::SetCameras(std::vector const& cameras_) { + cameras = cameras_; + return *this; +} + Image Slicer360ToPerspective::ComputeCoverage(int width, bool raw) const { int height = width/2; Image coverage(width, height, 1); // Compute the coverage of the 360-degree image by the cameras added to the slicer. + #pragma omp parallel for for(int i = 0; i < height; i++) { for(int j = 0; j < width; j++) { // Compute view vector in inertial frame from equirectangular coordinates @@ -66,40 +70,25 @@ Eigen::Vector3i Slicer360ToPerspective::ProjectToCameraPixel(Camera const& camer return panorama_image.GetPixelInterp(p_equi, interpolation_method); } -Image Slicer360ToPerspective::ProjectToCamera(Camera const& camera, bool parallel) const { +Image Slicer360ToPerspective::ProjectToCamera(Camera const& camera) const { Image image(output_image_width, output_image_height, 3); - if(parallel) { - #ifndef DO_NOT_USE_OPENMP - #pragma omp parallel for - #endif - for(int i = 0 ; i < output_image_height ; i++) - for(int j = 0 ; j < output_image_width ; j++) - image.SetPixel(i, j, ProjectToCameraPixel(camera, i, j)); - } else { - for(int i = 0 ; i < output_image_height ; i++) - for(int j = 0 ; j < output_image_width ; j++) - image.SetPixel(i, j, ProjectToCameraPixel(camera, i, j)); - } + #pragma omp parallel for + for(int i = 0 ; i < output_image_height ; i++) + for(int j = 0 ; j < output_image_width ; j++) + image.SetPixel(i, j, ProjectToCameraPixel(camera, i, j)); return image; } -std::vector Slicer360ToPerspective::ProjectToCameras(std::vector const& cameras_, bool parallel) const { +std::vector Slicer360ToPerspective::ProjectToCameras(std::vector const& cameras_) const { std::vector images(cameras_.size()); - if(parallel) { - #ifndef DO_NOT_USE_OPENMP - #pragma omp parallel for - #endif + #pragma omp parallel for for(unsigned int i = 0 ; i < cameras_.size(); i++) - images[i] = ProjectToCamera(cameras_[i], false);// Parallelization is already done at the camera level. - } else { - for(unsigned int i = 0 ; i < cameras_.size(); i++) - images[i] = ProjectToCamera(cameras_[i], false);// Parallelization is already done at the camera level. - } + images[i] = ProjectToCamera(cameras_[i]); return images; } -std::vector Slicer360ToPerspective::ProjectToAllCameras(bool parallel) const { - return ProjectToCameras(cameras, parallel); +std::vector Slicer360ToPerspective::ProjectToAllCameras() const { + return ProjectToCameras(cameras); } Slicer360ToPerspective & Slicer360ToPerspective::LoadCamerasFromFile(std::string const& filename) { diff --git a/cpp/src/main.cpp b/cpp/src/main.cpp new file mode 100644 index 0000000..bdd4953 --- /dev/null +++ b/cpp/src/main.cpp @@ -0,0 +1,165 @@ +#include +#include +#include + +std::string convert_interpolation_mode_to_str(Image::InterpMethod interpolation_mode) { + switch(interpolation_mode) { + case Image::InterpMethod::NEAREST: + return "nearest"; + case Image::InterpMethod::BILINEAR: + return "bilinear"; + default: + return "unknown"; + } +} + +struct ProgramConfiguration { + std::string cameras_filename; + std::string images_folder; + std::string output_folder = "."; + std::string output_format = "jpg"; + Image::InterpMethod interpolation_mode = Image::InterpMethod::BILINEAR; + bool normalize = false; + bool coverage = false; + int size = 1000; +}; + +void print_program_configuration(ProgramConfiguration const& program_config) { + std::cout << "---------------------" << std::endl; + std::cout << "Current configuration" << std::endl; + std::cout << "---------------------" << std::endl; + std::cout << "Cameras input file " << program_config.cameras_filename << std::endl; + std::cout << "Images folder " << program_config.images_folder << std::endl; + std::cout << "Output folder " << program_config.output_folder << std::endl; + std::cout << "Output format " << program_config.output_format << std::endl; + std::cout << "Interpolation mode " << convert_interpolation_mode_to_str(program_config.interpolation_mode) << std::endl; + std::cout << "Normalize input images " << program_config.normalize << std::endl; + std::cout << "Coverage image generation " << program_config.coverage << std::endl; + std::cout << "Output image size " << program_config.size << std::endl; + std::cout << std::endl; +} + +bool decode_arguments(cmd_line_parser::parser const& parser, ProgramConfiguration & program_config) { + // Decode the arguments and check their validity + if(parser.parsed("cameras")) + program_config.cameras_filename = parser.get("cameras"); + + if(parser.parsed("images")) + program_config.images_folder = parser.get("images"); + + if(parser.parsed("output_folder")) + program_config.output_folder = parser.get("output_folder"); + + if(parser.parsed("output_format")) { + std::string output_format = parser.get("output_format"); + if(output_format != "jpg" && output_format != "png" && output_format != "bmp" && output_format != "tga") { + std::cerr << "Error: invalid output format : " << output_format << std::endl; + return false; + } + program_config.output_format = output_format; + } + + if(parser.parsed("interpolation")) { + std::string interpolation_mode = parser.get("interpolation"); + if(interpolation_mode == "nearest") + program_config.interpolation_mode = Image::InterpMethod::NEAREST; + else if(interpolation_mode == "bilinear") + program_config.interpolation_mode = Image::InterpMethod::BILINEAR; + else { + std::cerr << "Error: invalid interpolation mode : " << interpolation_mode << std::endl; + return false; + } + } + + if(parser.parsed("normalize")) { + program_config.normalize = parser.get("normalize"); + } + + if(parser.parsed("coverage")) { + program_config.coverage = parser.get("coverage"); + } + + if(parser.parsed("size")) { + int size = parser.get("size"); + if(size < 2) { + std::cerr << "Error: invalid size : " << size << ". Size must be > 1." << std::endl; + return false; + } + program_config.size = size; + } + + return true; +} + +void configure_parser(cmd_line_parser::parser & parser) { + // name, description, shorthand, required, is a boolean or not (false by default) + parser.add("cameras", "The path of a CSV file containing the specifications of the cameras : yaw, pitch, roll, and FOV, all in degrees.", "-c", true); + parser.add("images", "Input folder containing the equirectangular (360 degree) images to convert to perspective. The input format must be one of the following : jpg, png, bmp, tga.", "-i", false); + parser.add("output_folder", "Output folder containing the projected images.", "-o", false); + parser.add("size", "Size of generated images, in pixels. By default, 1000x1000 px.", "-s", false); + parser.add("interpolation", "Interpolation mode when projecting the equirectangular (360 degree) image : 'nearest' (fast but blocky), 'bilinear' (slow but better). By default, 'bilinear'.", "--interp", false); + parser.add("output_format", "Output format among the following : jpg, png, bmp, tga. By default, jpg.", "-o", false); + parser.add("coverage", "Generate an colorized image depicting the coverage of the cameras over the whole sphere. The image produced is in equirectangular coordinates.", "--cov", false, true); + parser.add("normalize", "If set, the images are histogram-normalized before being converted. This equalizes the utilization of all intensity values available. It might help the photogrammetry process to normalize the images.", "-n", false, true); +} + +int main(int argc, char** argv) { + std::cout.precision(16); + + // Default configuration + ProgramConfiguration program_config; + + // declare the parser + cmd_line_parser::parser parser(argc, argv); + + // configure the parser + configure_parser(parser); + + // parse command line and return on failure + bool success = parser.parse(); + if (!success) return 1; + + // Decode the arguments + if(!decode_arguments(parser, program_config)) return 1; + + // Print the configuration + print_program_configuration(program_config); + + // Load the cameras + std::vector cameras; + { + std::cout << "Loading cameras from \"" << program_config.cameras_filename << "\"..." << std::endl; + Slicer360ToPerspective slicer; // create an empty slicer + slicer.LoadCamerasFromFile(program_config.cameras_filename); // load the cameras from the file into the slicer + cameras = slicer.cameras; // copy the cameras that were loaded + std::cout << "Loaded " << cameras.size() << " cameras." << std::endl; + if(!cameras.size()) { // If no cameras were loaded, abort + std::cerr << "Aborting now." << std::endl; + return 1; + } + } + + // DEBUG + for(unsigned int i = 0; i < cameras.size(); ++i) { + std::cout << "Camera " << i << " :\nR =\n" << cameras[i].GetR() << "\nFOV = " << cameras[i].GetFOV() << std::endl; + } + + // Build the slicer + Slicer360ToPerspective slicer = Slicer360ToPerspective() + .SetOutputFolder(program_config.output_folder) + .SetInterpolationMethod(program_config.interpolation_mode) + .SetOutputImageSize(program_config.size) + .SetCameras(cameras); + + // Generate the coverage image if requested + if(program_config.coverage) { + std::cout << "Generating coverage image..." << std::endl; + Image coverage_image = slicer.ComputeCoverage(program_config.size, true); + std::cout << "Coverage image generated." << std::endl; + auto [min_cov, max_cov, avg_cov] = coverage_image.ComputeMinMaxAvg(); + std::cout << "Coverage statistics (per pixel) :\n\tmin = " << int(min_cov) << "\n\tmax = " << int(max_cov) << "\n\tavg = " << avg_cov << std::endl; + std::cout << "Exporting coverage image..." << std::endl; + coverage_image.Normalized().Colorized(Image::Colormap::PARULA); + coverage_image.Save(program_config.output_folder + "/coverage." + program_config.output_format); + } +} diff --git a/cpp/src/main.cpp.nope b/cpp/src/main.cpp.nope deleted file mode 100644 index ad8283b..0000000 --- a/cpp/src/main.cpp.nope +++ /dev/null @@ -1,6 +0,0 @@ -#include - -int main() { - std::cout << "Hello, World!\n"; - return 0; -} diff --git a/cpp/src/tests.cpp b/cpp/src/tests.cpp.nope similarity index 100% rename from cpp/src/tests.cpp rename to cpp/src/tests.cpp.nope