Parallelized Image + main program skeleton.
This commit is contained in:
parent
71a78e12e4
commit
3fff8ce15b
7 changed files with 252 additions and 50 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
165
cpp/src/main.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
std::cout << "Hello, World!\n";
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue