Parallelized Image + main program skeleton.

This commit is contained in:
Jerome 2023-05-06 10:01:28 +02:00
parent 71a78e12e4
commit 3fff8ce15b
7 changed files with 252 additions and 50 deletions

View file

@ -159,6 +159,10 @@ class Image {
/// @return Reference to the image.
Image Colorized(unsigned char const* colormap) const;
std::tuple<unsigned char, unsigned char> ComputeMinMax() const;
std::tuple<unsigned char, unsigned char, double> 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.

View file

@ -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<Camera> 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<Image> ProjectToCameras(std::vector<Camera> const& cameras_, bool parallel = true) const;
std::vector<Image> ProjectToCameras(std::vector<Camera> 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<Image> ProjectToAllCameras(bool parallel = true) const;
std::vector<Image> 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:

View file

@ -1,4 +1,5 @@
#include <Image.hpp>
#include <omp.h>
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<uint64_t> 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<unsigned char, unsigned char> 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<unsigned char, unsigned char, double> 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<uint64_t> Image::ComputeHistogram(int downsampling_factor) const {
downsampling_factor = std::max(downsampling_factor, 1);
std::vector<uint64_t> 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;
}

View file

@ -3,9 +3,7 @@
#include <csv_parser.hpp>
#include <iostream>
#ifndef DO_NOT_USE_OPENMP
#include <omp.h>
#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<Camera> 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<Image> Slicer360ToPerspective::ProjectToCameras(std::vector<Camera> const& cameras_, bool parallel) const {
std::vector<Image> Slicer360ToPerspective::ProjectToCameras(std::vector<Camera> const& cameras_) const {
std::vector<Image> 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<Image> Slicer360ToPerspective::ProjectToAllCameras(bool parallel) const {
return ProjectToCameras(cameras, parallel);
std::vector<Image> Slicer360ToPerspective::ProjectToAllCameras() const {
return ProjectToCameras(cameras);
}
Slicer360ToPerspective & Slicer360ToPerspective::LoadCamerasFromFile(std::string const& filename) {

165
cpp/src/main.cpp Normal file
View file

@ -0,0 +1,165 @@
#include <Slicer360ToPerspective.hpp>
#include <parser.hpp>
#include <csv_parser.hpp>
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<std::string>("cameras");
if(parser.parsed("images"))
program_config.images_folder = parser.get<std::string>("images");
if(parser.parsed("output_folder"))
program_config.output_folder = parser.get<std::string>("output_folder");
if(parser.parsed("output_format")) {
std::string output_format = parser.get<std::string>("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<std::string>("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<bool>("normalize");
}
if(parser.parsed("coverage")) {
program_config.coverage = parser.get<bool>("coverage");
}
if(parser.parsed("size")) {
int size = parser.get<int>("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<Camera> 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);
}
}

View file

@ -1,6 +0,0 @@
#include <iostream>
int main() {
std::cout << "Hello, World!\n";
return 0;
}