Compare commits

...

10 commits

20 changed files with 1314 additions and 116 deletions

19
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "360 to perspective",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/cpp/obj/360_to_perspective.exe",
"cwd": "${workspaceFolder}/cpp",
"environment": [],
"args": [],
"stopAtEntry": false,
"externalConsole": false
}
]
}

21
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,21 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "D:/Programmes/Octave/Octave-7.3.0/usr/bin/make.exe",
"args": [
"-f",
"${workspaceFolder}/cpp/Makefile"
],
"options": {
"cwd": "${workspaceFolder}/cpp"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View file

@ -1,6 +1,7 @@
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -pedantic -g
INCLUDES = -I./include -ID:/Users/Jerome/Documents/Ingenierie/Programmation/eigen-3.4.0 -I./stb-master
CXXFLAGS = -std=c++17 -Wall -Wextra -pedantic -O3
INCLUDES = -I./include -ID:/Users/Jerome/Documents/Ingenierie/Programmation/eigen-3.4.0 -I./stb-master -ID:/Users/Jerome/Documents/Ingenierie/Programmation/cmd_line_parser-master/include
LDFLAGS = -fopenmp
SRC_DIR = src
OBJ_DIR = obj
@ -14,10 +15,10 @@ EXEC = $(OBJ_DIR)/360_to_perspective
all: $(EXEC)
$(EXEC): $(OBJS)
$(CXX) $(CXXFLAGS) -o $@ $^
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $^
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR)
$(CXX) $(CXXFLAGS) $(INCLUDES) -c -o $@ $<
$(CXX) $(CXXFLAGS) $(INCLUDES) $(LDFLAGS) -c -o $@ $<
$(OBJ_DIR):
mkdir -p $@

View file

@ -3,7 +3,8 @@
#include <Eigen/Dense>
#define DEG2RAD 0.017453292519943295
#define DEG2RAD 0.01745329251994329576923690768488612713442871888541725456097191440171009114603449443682241569634509482
#define RAD2DEG 57.29577951308232087679815481410517033240547246656432154916024386120284714832155263244096899585111094
/// @brief A camera class to perform frame transformations and projections.
class Camera {
@ -22,6 +23,8 @@ class Camera {
Camera & SetForward(Eigen::Vector3d const& fwd_);
Camera & SetUp(Eigen::Vector3d const& up_);
Camera & SetR(Eigen::Matrix3d const& R_);
/// @brief Sets the field of view of the camera, in radians.
Camera & SetFOV(double fov);
// Methods
@ -31,6 +34,12 @@ class Camera {
/// @brief Orients the camera so that the forward vector points at the target. The position of the camera is (0,0,0).
Camera & SetTarget(Eigen::Vector3d const& target);
/// @brief Orients the camera according to the provided Euler angles. The position of the camera is (0,0,0).
Camera & SetEulerAngles(double yaw, double pitch, double roll, bool radians = false);
/// @brief Returns the Euler angles of the camera (technically the Tait-Bryan ZYX angles).
Eigen::Vector3d GetEulerAngles() const;
/// @brief Computes the ray direction in camera frame for a point (x,y) on the surface of the camera's sensor. p_norm is in normalized coordinates [-1;1].
/// @param p_norm Normalized coordinates of the point on the sensor.
/// @return The ray direction in camera frame, cartesian coordinates.
@ -64,6 +73,11 @@ class Camera {
// Static methods
/// @brief Converts pixel coordinates to normalized coordinates. i is the row (y), j is the column (x). Normalized coordinates range from -1 to 1.
static Eigen::Vector2d PixelToNormalizedCoordinates(unsigned int i, unsigned int j, unsigned int width, unsigned int height);
/// @brief Creates a Camera object from a vector of yaw, pitch, roll and FOV, read from a CSV file.
/// @param yaw_pitch_roll_fov 4-D Vector containing yaw, pitch, roll and FOV as double-precision floating point numbers.
/// @return A Camera object with the specified parameters.
static Camera FromYawPitchRollFOV(std::vector<double> const& yaw_pitch_roll_fov);
private:
Eigen::Vector3d fwd; //!< Forward vector in inertial frame

View file

@ -21,27 +21,38 @@ class Image {
LANCZOS ///< Lanczos filtering interpolation.
};
/// @brief Colormaps LUTs.
struct Colormap {
/// @brief MATLAB's parula colormap. 256x3 values : R0G0B0, R1G1B1, ...
static constexpr unsigned char PARULA[256*3] = {62,38,168 ,62,39,172 ,63,40,175 ,63,41,178 ,64,42,180 ,64,43,183 ,65,44,186 ,65,45,189 ,66,46,191 ,66,47,194 ,67,48,197 ,67,49,200 ,67,50,202 ,68,51,205 ,68,52,208 ,69,53,210 ,69,55,213 ,69,56,215 ,70,57,217 ,70,58,220 ,70,59,222 ,70,61,224 ,71,62,225 ,71,63,227 ,71,65,229 ,71,66,230 ,71,68,232 ,71,69,233 ,71,70,235 ,72,72,236 ,72,73,237 ,72,75,238 ,72,76,240 ,72,78,241 ,72,79,242 ,72,80,243 ,72,82,244 ,72,83,245 ,72,84,246 ,71,86,247 ,71,87,247 ,71,89,248 ,71,90,249 ,71,91,250 ,71,93,250 ,70,94,251 ,70,96,251 ,70,97,252 ,69,98,252 ,69,100,253 ,68,101,253 ,67,103,253 ,67,104,254 ,66,106,254 ,65,107,254 ,64,109,254 ,63,110,255 ,62,112,255 ,60,113,255 ,59,115,255 ,57,116,255 ,56,118,254 ,54,119,254 ,53,121,253 ,51,122,253 ,50,124,252 ,49,125,252 ,48,127,251 ,47,128,250 ,47,130,250 ,46,131,249 ,46,132,248 ,46,134,248 ,46,135,247 ,45,136,246 ,45,138,245 ,45,139,244 ,45,140,243 ,45,142,242 ,44,143,241 ,44,144,240 ,43,145,239 ,42,147,238 ,41,148,237 ,40,149,236 ,39,151,235 ,39,152,234 ,38,153,233 ,38,154,232 ,37,155,232 ,37,156,231 ,36,158,230 ,36,159,229 ,35,160,229 ,35,161,228 ,34,162,228 ,33,163,227 ,32,165,227 ,31,166,226 ,30,167,225 ,29,168,225 ,29,169,224 ,28,170,223 ,27,171,222 ,26,172,221 ,25,173,220 ,23,174,218 ,22,175,217 ,20,176,216 ,18,177,214 ,16,178,213 ,14,179,212 ,11,179,210 ,8,180,209 ,6,181,207 ,4,182,206 ,2,183,204 ,1,183,202 ,0,184,201 ,0,185,199 ,0,186,198 ,1,186,196 ,2,187,194 ,4,187,193 ,6,188,191 ,9,189,189 ,13,189,188 ,16,190,186 ,20,190,184 ,23,191,182 ,26,192,181 ,29,192,179 ,32,193,177 ,35,193,175 ,37,194,174 ,39,194,172 ,41,195,170 ,43,195,168 ,44,196,166 ,46,196,165 ,47,197,163 ,49,197,161 ,50,198,159 ,51,199,157 ,53,199,155 ,54,200,153 ,56,200,150 ,57,201,148 ,59,201,146 ,61,202,144 ,64,202,141 ,66,202,139 ,69,203,137 ,72,203,134 ,75,203,132 ,78,204,129 ,81,204,127 ,84,204,124 ,87,204,122 ,90,204,119 ,94,205,116 ,97,205,114 ,100,205,111 ,103,205,108 ,107,205,105 ,110,205,102 ,114,205,100 ,118,204,97 ,121,204,94 ,125,204,91 ,129,204,89 ,132,204,86 ,136,203,83 ,139,203,81 ,143,203,78 ,147,202,75 ,150,202,72 ,154,201,70 ,157,201,67 ,161,200,64 ,164,200,62 ,167,199,59 ,171,199,57 ,174,198,55 ,178,198,53 ,181,197,51 ,184,196,49 ,187,196,47 ,190,195,45 ,194,195,44 ,197,194,42 ,200,193,41 ,203,193,40 ,206,192,39 ,208,191,39 ,211,191,39 ,214,190,39 ,217,190,40 ,219,189,40 ,222,188,41 ,225,188,42 ,227,188,43 ,230,187,45 ,232,187,46 ,234,186,48 ,236,186,50 ,239,186,53 ,241,186,55 ,243,186,57 ,245,186,59 ,247,186,61 ,249,186,62 ,251,187,62 ,252,188,62 ,254,189,61 ,254,190,60 ,254,192,59 ,254,193,58 ,254,194,57 ,254,196,56 ,254,197,55 ,254,199,53 ,254,200,52 ,254,202,51 ,253,203,50 ,253,205,49 ,253,206,49 ,252,208,48 ,251,210,47 ,251,211,46 ,250,213,46 ,249,214,45 ,249,216,44 ,248,217,43 ,247,219,42 ,247,221,42 ,246,222,41 ,246,224,40 ,245,225,40 ,245,227,39 ,245,229,38 ,245,230,38 ,245,232,37 ,245,233,36 ,245,235,35 ,245,236,34 ,245,238,33 ,246,239,32 ,246,241,31 ,246,242,30 ,247,244,28 ,247,245,27 ,248,247,26 ,248,248,24 ,249,249,22 ,249,251,21};
};
/// @brief Initializes the image with the given width, height, and optionally depth. Warning : the values are uninitialized.
/// @param width_ Width of the image.
/// @param height_ Height of the image.
/// @param depth_ Depth of the image.
Image(int width_, int height_, int depth_ = 1);
Image(int width_ = 1, int height_ = 1, int depth_ = 1);
/// @brief Copies another image. Performs a deep copy.
/// @param other The image to copy.
Image(Image const& other);
/// @brief Frees the allocated memory of the image.
~Image();
/// @brief Loads an image from a file.
/// @param filename Path to the image file.
Image(std::string const& filename);
/// @brief Frees the allocated memory of the image.
~Image();
/// @brief Copies the other image into this one. Performs a deep copy.
/// @param other Image to copy.
/// @return A reference to this image.
Image & operator=(Image const& other);
/// @brief Saves the image to a file.
/// Supports the following formats : PNG, BMP, TGA, JPG. The format is determined from the file extension.
/// @param filename Path to save the image to.
Save(std::string const& filename) const;
int Save(std::string const& filename) const;
int GetWidth() const;
int GetHeight() const;
@ -127,7 +138,37 @@ class Image {
/// All channels are used to compute the histogram, and all channels are normalized using the same global histogram.
/// @param downsampling_factor Downsampling factor to use while computing the histogram.
/// @return A reference to the image.
Image & HistogramNormalize(int downsampling_factor=1);
Image & HistogramNormalize(int downsampling_factor = 1);
/// @brief Returns a histrogram-normalized copy of the image. Histogram normalization is a contrast enhancement technique that ensures that the whole range of values is used.
/// All channels are used to compute the histogram, and all channels are normalized using the same global histogram.
/// @param downsampling_factor Downsampling factor to use while computing the histogram.
/// @return A reference to the image.
Image HistogramNormalized(int downsampling_factor = 1) const;
/// @brief Normalizes the image (in place), so that the minimum value is 0 and the maximum value is 255.
/// @return A reference to the image.
Image & Normalize();
/// @brief Returns a normalized copy of the image, so that the minimum value is 0 and the maximum value is 255.
/// @return A normalized copy of the image.
Image Normalized() const;
/// @brief Applies a colormap to the image. After applying the colormap, the image will be 3-channel (RGB). If the image is not single-channel, a unnchanged copy of the image is returned.
/// @param colormap Array of 256*3 values, representing the RGB values of the colormap. Order is R0, G0, B0, R1, G1, B1, etc.
/// @return Reference to the image.
Image Colorized(unsigned char const* colormap) const;
/// @brief Resizes the image using the given interpolation method and returns the result. The original image is unchanged.
/// @param width Width of the resized image.
/// @param height Height of the resized image.
/// @param interp_method Interpolation method to use.
/// @return The resized image.
Image Resized(int new_width, int new_height, InterpMethod const& interp_method = InterpMethod::BILINEAR) 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.

View file

@ -2,16 +2,91 @@
#define H_Slicer360ToPerspective
#include <Camera.hpp>
#include <Image.hpp>
/// @brief This class encapsulates the algorithms to convert a 360-degree image to a perspective image.
/// @brief This class encapsulates the algorithms to convert a 360-degree image to perspective images.
/// @details The 360-degree image is converted to perspective images by projecting the pixels of the 360-degree image using perspective projection and virtual cameras.
/// 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.
class Slicer360ToPerspective {
public:
Slicer360ToPerspective();
/// @brief Builds a Slicer360ToPerspective object using a 360-degree image.
/// @param panorama_image The 360-degree image to be used as an input.
Slicer360ToPerspective(Image const& panorama_image_ = Image(1,1,3));
/// @brief Sets the 360-degree image to be used as an input.
/// @param panorama_image_ 360-degree image to be used as an input.
/// @return A reference to the current object.
Slicer360ToPerspective & SetPanoramaImage(Image const& panorama_image_);
/// @brief Sets the width of the output images.
/// @param width Width of the output images in pixels.
/// @param height Height of the output images in pixels. By default = -1. If < 0, the height is set to the width to yield a square image.
/// @return A reference to the current object.
Slicer360ToPerspective & SetOutputImageSize(int width, int height = -1);
/// @brief Sets the interpolation method used to project the 360-degree image to perspective images. See Image::InterpMethod.
/// @param interpolation_method_ Interpolation method used to project the 360-degree image to perspective images.
/// @return A reference to the current object.
Slicer360ToPerspective & SetInterpolationMethod(Image::InterpMethod const& interpolation_method_);
/// @brief Sets the folder where the output images are saved. By default, the output images are saved in the current folder.
/// @param folder Folder where the output images are saved.
/// @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.
/// @return The coverage of the 360-degree image.
Image ComputeCoverage(int width, bool raw = false) const;
/// @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) 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.
/// @return A vector of generated perspective images.
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.
/// @return A vector of generated perspective images.
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:
/// - Yaw (degrees)
/// - Pitch (degrees)
/// - Roll (degrees)
/// - FOV (degrees)
/// @param filename File path of the CSV file.
/// @return A reference to the current object.
Slicer360ToPerspective & LoadCamerasFromFile(std::string const& filename);
std::vector<Camera> cameras;//!< A vector of cameras that are used to convert the 360-degree image to perspective images.
private:
std::vector<Camera> cameras;//!< A vector of cameras that are used to convert the 360-degree image to perspective images.
/// @brief Computes the projection of the 360-degree image to a perspective image using the specified camera on a single pixel of the output image.
/// @param camera The camera used to project the 360-degree image.
/// @param i The y-coordinate of the pixel of the output image (row).
/// @param j The x-coordinate of the pixel of the output image (column).
/// @return The color of the pixel of the output image.
Eigen::Vector3i ProjectToCameraPixel(Camera const& camera, int i, int j) const;
Image panorama_image; //!< The 360-degree input image.
int output_image_width; //!< Width of the output images in pixels.
int output_image_height; //!< Height of the output images in pixels.
Image::InterpMethod interpolation_method; //!< Interpolation method used to project the 360-degree image to perspective images.
std::string output_folder; //!< Folder where the output images are saved.
};
#endif

View file

@ -0,0 +1,25 @@
#ifndef H_CSVparser
#define H_CSVparser
#include <vector>
#include <string>
/**
* @brief Parses a CSV file containing floating-point numbers.
*
* @param filename The name of the CSV file to parse.
* @return A vector of vectors of doubles containing the parsed data.
* Returns an empty vector if there was an error.
*
* This function reads a CSV file containing floating-point numbers and
* returns the data as a vector of vectors of doubles. The CSV file should
* use commas as delimiters and should not contain any quotes or other
* special characters. Each row in the CSV file should have the same number
* of columns. If there is an error opening the file or parsing the data,
* this function returns an empty vector and prints an error message to
* standard error. The error message will indicate the type of error and
* the name of the file where the error occurred.
*/
std::vector<std::vector<double>> parse_csv_file(const std::string& filename, char delimiter = ',');
#endif

View file

@ -4,6 +4,38 @@
#include <Eigen/Dense>
#include <cmath>
#ifndef M_PI
#define M_PI 3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068
#endif
namespace frame_conversions {
/// @brief Computes the modulo of a number with another number, ensuring that the results lies in the interval [0;b].
/// @param a The number to compute the modulo of.
/// @param b The modulo.
/// @return The modulo of a with b, ensuring that the result lies in the interval [0;b]
template <typename T>
inline T fmod_pos(T a, T b) {
return a - b*std::floor(a/b);
}
/// @brief Maps a value x from the range (a,b) to the range (c,d).
///
/// This function takes an input value x and maps it from the input range (a,b)
/// to the output range (c,d). The resulting mapped value is returned.
///
/// @param x The value to be mapped.
/// @param a The lower bound of the input range.
/// @param b The upper bound of the input range.
/// @param c The lower bound of the output range.
/// @param d The upper bound of the output range.
///
/// @return The mapped value of x in the output range.
///
/// @note Make sure that a != b to avoid a divide-by-zero error.
double map(double x, double a, double b, double c, double d);
} // namespace frame_conversions
/// @brief Converts a spherical vector [r, theta, phi] in ISO convention (theta=0 is Z+) to a cartesian vector.
Eigen::Vector3d sph2cart(const Eigen::Vector3d & sph);
@ -16,7 +48,7 @@ Eigen::Vector3d cart2sph(const Eigen::Vector3d & cart);
/// @param width Width of the equirectangular image.
/// @param height Height of the equirectangular image.
/// @return A spherical vector [r, theta, phi] in ISO convention (theta=0 is Z+).
Eigen::Vector3d equirectangular2sph(unsigned int i, unsigned int j, unsigned int width, unsigned int height);
Eigen::Vector3d equirectangular2sph(int i, int j, unsigned int width, unsigned int height);
/// @brief Converts spherical coordinates to pixel coordinates of an equirectangular image.
/// @param p_sph A spherical vector [r, theta, phi] in ISO convention (theta=0 is Z+).

31
cpp/include/logger.hpp Normal file
View file

@ -0,0 +1,31 @@
#ifndef H_Logger
#define H_Logger
#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>
/// @brief Implementation of the Logger class to print messages to the console. It is defined as a template class to save the .cpp file.
template <int _T=1>
class Logger_impl {
public:
// static constexpr char date_format[] = "%FT%T%z";// Works on Linux but not on Windows...
static constexpr char date_format[] = "%Y-%m-%dT%H:%M:%S";
static void log(const std::string& message) {
auto now = std::chrono::system_clock::now();
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
std::cout << std::put_time(std::localtime(&now_time), date_format) << " " << message << std::endl;
}
static void error(const std::string& message) {
auto now = std::chrono::system_clock::now();
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
std::cerr << std::put_time(std::localtime(&now_time), date_format) << " ERROR: " << message << std::endl;
}
};
/// @brief Logger class to print messages to the console.
using Logger = Logger_impl<1>;
#endif

View file

@ -0,0 +1,29 @@
#ifndef H_ProgressTimer
#define H_ProgressTimer
#include <chrono>
#include <iostream>
/// @brief This class implements a progress timer that predicts how long a loop will take to complete.
class ProgressTimer {
public:
/// @brief Creates a new progress timer.
/// @param num_iterations Number of iterations to perform.
ProgressTimer(size_t num_iterations);
/// @brief Increments the iteration counter and prints the progress.
/// @return A string with the current progress.
std::string increment();
/// @brief Converts a number of seconds to a string with hours, minutes and seconds.
/// @param seconds Total number of seconds (input).
/// @return "hh:mm:ss.uuuuuu"
static std::string seconds_to_hms_str(double seconds);
private:
std::chrono::steady_clock::time_point m_start;
size_t m_num_iterations;
size_t m_current_iteration;
};
#endif

View file

@ -0,0 +1,135 @@
#ifndef h_test_framework
#define h_test_framework
#ifndef TOL_EQ_TEST
#define TOL_EQ_TEST 1e-9
#endif
static size_t num_tests = 0;
static size_t num_tests_ok = 0;
static size_t num_tests_ko = 0;
#define PRINT_VAR(x) std::cout << #x << "\t= " << (x) << std::endl
// EXPECT_EQ macro
#define EXPECT_EQ(actual, expected) EXPECT_EQ_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_EQ_IMPL(actual, expected, file, line) \
do { \
num_tests++; \
auto&& e = (expected); \
auto&& a = (actual); \
if (e != a) { \
std::cout << file << ":" << line << " : " << "Expected: " << (#expected) << " = " << e << ", actual: " << (#actual) << " = " << a << " " << std::endl; \
num_tests_ko++; \
} \
else { \
num_tests_ok++; \
} \
} while (false)
// EXPECT_DOUBLE_EQ macro
#define EXPECT_DOUBLE_EQ(actual, expected) EXPECT_DOUBLE_EQ_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_DOUBLE_EQ_IMPL(actual, expected, file, line) \
do { \
num_tests++; \
double e = (expected); \
double a = (actual); \
if (std::abs(e - a) > TOL_EQ_TEST) { \
std::cout << file << ":" << line << " : " << "Expected: " << (#expected) << " = " << e << ", actual: " << (#actual) << " = " << a << " " << std::endl; \
num_tests_ko++; \
} \
else { \
num_tests_ok++; \
} \
} while (false)
// EXPECT_LT macro
#define EXPECT_LT(actual, expected) EXPECT_LT_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_LT_IMPL(actual, expected, file, line) \
do { \
num_tests++; \
auto&& e = (expected); \
auto&& a = (actual); \
if (a < e) { \
num_tests_ok++; \
} \
else { \
std::cout << file << ":" << line << " : " << "Expected " << a << " < " << (#expected) << " = " << e << ", actual: " << (#actual) << std::endl; \
num_tests_ko++; \
} \
} while (false)
// EXPECT_LE macro
#define EXPECT_LE(actual, expected) EXPECT_LE_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_LE_IMPL(actual, expected, file, line) \
do { \
num_tests++; \
auto&& e = (expected); \
auto&& a = (actual); \
if (a <= e) { \
num_tests_ok++; \
} \
else { \
std::cout << file << ":" << line << " : " << "Expected " << a << " <= " << (#expected) << " = " << e << ", actual: " << (#actual) << std::endl; \
num_tests_ko++; \
} \
} while (false)
// EXPECT_GT macro
#define EXPECT_GT(actual, expected) EXPECT_GT_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_GT_IMPL(actual, expected, file, line) \
do { \
num_tests++; \
auto&& e = (expected); \
auto&& a = (actual); \
if (a > e) { \
num_tests_ok++; \
} \
else { \
std::cout << file << ":" << line << " : " << "Expected " << a << " > " << (#expected) << " = " << e << ", actual: " << (#actual) << std::endl; \
num_tests_ko++; \
} \
} while (false)
// EXPECT_GE macro
#define EXPECT_GE(actual, expected) EXPECT_GE_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_GE_IMPL(actual, expected, file, line) \
do { \
num_tests++; \
auto&& e = (expected); \
auto&& a = (actual); \
if (a >= e) { \
num_tests_ok++; \
} \
else { \
std::cout << file << ":" << line << " : " << "Expected " << a << " >= " << (#expected) << " = " << e << ", actual: " << (#actual) << std::endl; \
num_tests_ko++; \
} \
} while (false)
// EXPECT_VEC_EQ macro
#define EXPECT_VEC_EQ(actual, expected) EXPECT_VEC_EQ_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_VEC_EQ_IMPL(actual, expected, file, line) \
do { \
for(unsigned int i = 0 ; i < (expected).size() ; i++) { \
EXPECT_DOUBLE_EQ_IMPL((actual)[i], (expected)[i], file, line); \
} \
} while (false)
// EXPECT_MAT_EQ macro
#define EXPECT_MAT_EQ(actual, expected) EXPECT_MAT_EQ_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_MAT_EQ_IMPL(actual, expected, file, line) \
do { \
for(unsigned int i = 0 ; i < (expected).rows() ; i++) { \
for(unsigned int j = 0 ; j < (expected).rows() ; j++) { \
EXPECT_DOUBLE_EQ_IMPL((actual)(i,j), (expected)(i,j), file, line); \
} \
} \
} while (false)
#define PRINT_TESTS_SUMMARY() \
do { \
std::cout << "Tests summary: " << num_tests << " tests in total\n " << num_tests_ok << "/" << num_tests << " OK (" << ((100.0*(double)num_tests_ok)/num_tests) << " %)\n " << num_tests_ko << "/" << num_tests << " KO (" << ((100.0*(double)num_tests_ko)/num_tests) << " %)" << std::endl; \
} while (false)
#endif

View file

@ -1,7 +1,5 @@
#include <Camera.hpp>
#define _USE_MATH_DEFINES
#include <cmath>
#include <iostream>
Camera::Camera()
: fwd(Eigen::Vector3d::UnitX()), up(Eigen::Vector3d::UnitZ()), R(Eigen::Matrix3d::Identity()), R_T(Eigen::Matrix3d::Identity()), FOV(90.0*DEG2RAD), tanFOV2(tan(FOV/2.0))
@ -70,6 +68,67 @@ Camera & Camera::SetTarget(Eigen::Vector3d const& target) {
return *this;
}
Camera & Camera::SetEulerAngles(double yaw, double pitch, double roll, bool radians) {
if(!radians) {
yaw *= DEG2RAD;
pitch *= DEG2RAD;
roll *= DEG2RAD;
}
// Compute sin and cos values for the angles
const double sy = sin(yaw);
const double cy = cos(yaw);
const double sp = sin(pitch);
const double cp = cos(pitch);
const double sr = sin(roll);
const double cr = cos(roll);
// Compute the rotation matrix
R(0,0) = cy*cp;
R(0,1) = cy*sp*sr - sy*cr;
R(0,2) = cy*sp*cr + sy*sr;
R(1,0) = sy*cp;
R(1,1) = sy*sp*sr + cy*cr;
R(1,2) = sy*sp*cr - cy*sr;
R(2,0) = -sp;
R(2,1) = cp*sr;
R(2,2) = cp*cr;
// Align the axes so that -Z points forward, Y points up, and X points to the right
Eigen::Matrix3d R_Cam_Alignment; R_Cam_Alignment << 0., 0., -1.,
-1., 0., 0.,
0., 1., 0;
R = R*R_Cam_Alignment;
R_T = R.transpose();
return *this;
}
Eigen::Vector3d Camera::GetEulerAngles() const
{
// Align the axes so that -Z points forward, Y points up, and X points to the right
Eigen::Matrix3d R_Cam_Alignment; R_Cam_Alignment << 0., 0., -1.,
-1., 0., 0.,
0., 1., 0;
Eigen::Matrix3d _R = R*R_Cam_Alignment.transpose();
double sy = sqrt(_R(0,0)*_R(0,0) + _R(1,0)*_R(1,0));
double yaw, pitch, roll;
if (sy > 1e-6) {
yaw = atan2(_R(1,0), _R(0,0));
pitch = atan2(-_R(2,0), sy);
roll = atan2(_R(2,1), _R(2,2));
}
else {
yaw = atan2(-_R(0,1), _R(1,1));
pitch = atan2(-_R(2,0), sy);
roll = 0.0;
}
return Eigen::Vector3d(yaw*RAD2DEG, pitch*RAD2DEG, roll*RAD2DEG);
}
Eigen::Vector3d Camera::ComputeRayDirInCameraFrame(Eigen::Vector2d const& p_norm) const {
return Eigen::Vector3d(p_norm(0)*tanFOV2, p_norm(1)*tanFOV2, -1.0).normalized();
}
@ -101,4 +160,12 @@ bool Camera::IsPointVisibleInertialFrame(Eigen::Vector3d const& p_inertial) cons
Eigen::Vector2d Camera::PixelToNormalizedCoordinates(unsigned int i, unsigned int j, unsigned int width, unsigned int height) {
return Eigen::Vector2d(2.0*((double)j)/(width - 1) - 1.0, -2.0*((double)i)/(height - 1) + 1.0);
}
}
Camera Camera::FromYawPitchRollFOV(std::vector<double> const& yaw_pitch_roll_fov) {
if(yaw_pitch_roll_fov.size() != 4) {
std::cerr << "Error: Camera::FromYawPitchRollFOV: yaw_pitch_roll_fov must be a vector of size 4." << std::endl;
return Camera();
}
return Camera().SetEulerAngles(yaw_pitch_roll_fov[0], yaw_pitch_roll_fov[1], yaw_pitch_roll_fov[2]).SetFOV(yaw_pitch_roll_fov[3]*DEG2RAD);
}

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];
@ -9,10 +10,6 @@ Image::Image(Image const& other) : width(other.width), height(other.height), dep
std::memcpy(data, other.data, width * height * depth);
}
Image::~Image() {
delete[] data;
}
Image::Image(std::string const& filename) {
// Load image data using stb_image.h
unsigned char* image_data = stbi_load(filename.c_str(), &width, &height, &depth, 0);
@ -31,6 +28,25 @@ Image::Image(std::string const& filename) {
}
}
Image::~Image() {
delete[] data;
}
Image & Image::operator=(Image const& other) {
// Delete current data
delete[] data;
// Copy image attributes
width = other.width;
height = other.height;
depth = other.depth;
// Allocate new data and copy the data from other.data.
data = new unsigned char[width * height * depth];
std::memcpy(data, other.data, width * height * depth);
return *this;
}
int Image::Save(std::string const& filename) const {
if(filename.size() > 4 && filename.substr(filename.size() - 4) == ".png")
return stbi_write_png(filename.c_str(), width, height, depth, (const void *)data, 0);
@ -61,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);
@ -94,7 +111,9 @@ Eigen::Vector3i Image::GetPixelInterp(Eigen::Vector2d const& pos, InterpMethod c
int j = (int)std::floor(pos[1]);
double u = pos[0] - i;
double v = pos[1] - j;
Eigen::Vector3i rgb = ((1 - u) * (1 - v) * GetPixel(i, j).cast<double>() + u * (1 - v) * GetPixel(i + 1, j).cast<double>() + (1 - u) * v * GetPixel(i, j + 1).cast<double>() + u * v * GetPixel(i + 1, j + 1).cast<double>()).cast<int>();
int i_plus_1 = (i + 1) % height;
int j_plus_1 = (j + 1) % width;
Eigen::Vector3i rgb = ((1 - u) * (1 - v) * GetPixel(i, j).cast<double>() + u * (1 - v) * GetPixel(i_plus_1, j).cast<double>() + (1 - u) * v * GetPixel(i, j_plus_1).cast<double>() + u * v * GetPixel(i_plus_1, j_plus_1).cast<double>()).cast<int>();
return rgb;
}
else // By default, use nearest neighbor interpolation
@ -120,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));
@ -128,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);
@ -138,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);
@ -153,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);
@ -161,10 +184,101 @@ Image & Image::HistogramNormalize(int downsampling_factor) {
return *this;
}
Image Image::HistogramNormalized(int downsampling_factor) const {
Image normalized(*this);
normalized.HistogramNormalize(downsampling_factor);
return normalized;
}
Image & Image::Normalize() {
// First, compute the min and max values using all channels
auto [min_val, max_val] = ComputeMinMax();
// Then, normalize the image
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;
}
Image Image::Normalized() const {
Image normalized(*this);
normalized.Normalize();
return normalized;
}
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++)
colorized.SetPixelValue(i, j, c, colormap[3*GetPixelValue(i, j) + c]);
return colorized;
}
Image Image::Resized(int new_width, int new_height, InterpMethod const& interp_method) const {
Image resized(new_width, new_height, depth);
double resize_factor = (double)new_width / (double)width;
#pragma omp parallel for
for(int i = 0 ; i < resized.GetHeight() ; i++)
for(int j = 0 ; j < resized.GetWidth() ; j++)
resized.SetPixel(i, j, GetPixelInterp(Eigen::Vector2d(i / resize_factor, j / resize_factor), interp_method));
return resized;
}
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;
}
@ -174,4 +288,4 @@ std::vector<uint64_t> Image::CumulativeHistogram(std::vector<uint64_t> const& hi
for(unsigned int i = 1 ; i < histogram.size() ; i++)
cumul_hist[i] = cumul_hist[i - 1] + histogram[i];
return cumul_hist;
}
}

View file

@ -1,6 +1,117 @@
#include <Slicer360ToPerspective.hpp>
#include <frame_conversions.hpp>
#include <csv_parser.hpp>
#include <iostream>
Slicer360ToPerspective::Slicer360ToPerspective()
#include <omp.h>
Slicer360ToPerspective::Slicer360ToPerspective(Image const& panorama_image_) : panorama_image(panorama_image_), output_image_width(2), interpolation_method(Image::InterpMethod::BILINEAR), output_folder(".")
{
}
SetOutputImageSize(0);
}
Slicer360ToPerspective & Slicer360ToPerspective::SetPanoramaImage(Image const& panorama_image_) {
panorama_image = panorama_image_;
return *this;
}
Slicer360ToPerspective & Slicer360ToPerspective::SetOutputImageSize(int width, int height) {
output_image_width = (width > 0) ? width : 1024;
output_image_height = (height > 0) ? height : output_image_width;
return *this;
}
Slicer360ToPerspective & Slicer360ToPerspective::SetInterpolationMethod(Image::InterpMethod const& interpolation_method_) {
interpolation_method = interpolation_method_;
return *this;
}
Slicer360ToPerspective & Slicer360ToPerspective::SetOutputFolder(std::string const& folder) {
output_folder = folder;
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
Eigen::Vector3d ray_dir_sph = equirectangular2sph(i, j, width, height);
Eigen::Vector3d ray_dir_cart = sph2cart(ray_dir_sph);
int count = 0;
for(Camera const& camera : cameras)
{
if(camera.IsPointVisibleInertialFrame(ray_dir_cart))
count++;
}
coverage.SetPixelValue(i, j, 0, count);
}
}
if(raw)
return coverage;
else
return coverage.Normalized().Colorized(Image::Colormap::PARULA);
}
Eigen::Vector3i Slicer360ToPerspective::ProjectToCameraPixel(Camera const& camera, int i, int j) const {
// Get the ray direction in inertial frame by projecting the pixel to the sphere
// Eigen::Vector2d Camera::PixelToNormalizedCoordinates(unsigned int i, unsigned int j, unsigned int width, unsigned int height) {
Eigen::Vector2d p_sensor = Camera::PixelToNormalizedCoordinates(i, j, output_image_width, output_image_height);
p_sensor[0] *= -1;
p_sensor[1] *= ((double)output_image_height)/((double)output_image_width);// Take aspect ratio into account.
// Compute ray direction from sensor coordinates
Eigen::Vector3d ray_dir = camera.ComputeRayDirInInertialFrame(p_sensor);
Eigen::Vector3d ray_dir_sph = cart2sph(ray_dir);
Eigen::Vector2d p_equi = sph2equirectangular_d(ray_dir_sph, panorama_image.GetWidth(), panorama_image.GetHeight());
return panorama_image.GetPixelInterp(p_equi, interpolation_method);
}
Image Slicer360ToPerspective::ProjectToCamera(Camera const& camera) const {
Image image(output_image_width, output_image_height, 3);
#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_) const {
std::vector<Image> images(cameras_.size());
#pragma omp parallel for
for(unsigned int i = 0 ; i < cameras_.size(); i++)
images[i] = ProjectToCamera(cameras_[i]);
return images;
}
std::vector<Image> Slicer360ToPerspective::ProjectToAllCameras() const {
return ProjectToCameras(cameras);
}
Slicer360ToPerspective & Slicer360ToPerspective::LoadCamerasFromFile(std::string const& filename) {
std::vector<std::vector<double>> camera_specs = parse_csv_file(filename);
if(camera_specs.size() == 0) {
std::cerr << "Error: no camera specification found in file " << filename << std::endl;
return *this;
} else if(camera_specs[0].size() < 4) {
std::cerr << "Error: camera specification in file " << filename << " must have 4 columns : yaw, pitch, roll, FOV (all in degrees)." << std::endl;
return *this;
} else if(camera_specs[0].size() > 4) {
std::cerr << "Warning: camera specification in file " << filename << " has more than 4 columns. Only the first 4 columns will be used." << std::endl;
}
// Load cameras from decoded CSV file
cameras.clear();
for(auto& camera_spec : camera_specs)
cameras.push_back(Camera::FromYawPitchRollFOV(camera_spec));
return *this;
}

46
cpp/src/csv_parser.cpp Normal file
View file

@ -0,0 +1,46 @@
#include <csv_parser.hpp>
#include <iostream>
#include <fstream>
#include <sstream>
std::vector<std::vector<double>> parse_csv_file(const std::string& filename, char delimiter)
{
// Open file for reading
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "Error: could not open file " << filename << std::endl;
return {};
}
// Read file line by line
std::vector<std::vector<double>> data;
std::string line;
while (std::getline(file, line)) {
// Parse line into a vector of doubles
std::vector<double> row;
std::istringstream iss(line);
std::string token;
while (std::getline(iss, token, delimiter)) {
try {
double value = std::stod(token);
row.push_back(value);
} catch (const std::invalid_argument&) {
std::cerr << "Error: invalid floating-point number \"" << token << "\" in file " << filename << std::endl;
return {};
}
}
data.push_back(row);
}
// Check that all rows have the same size
const size_t num_columns = data[0].size();
for (const auto& row : data) {
if (row.size() != num_columns) {
std::cerr << "Error: inconsistent number of columns in file " << filename << std::endl;
return {};
}
}
return data;
}

View file

@ -1,6 +1,12 @@
#include <frame_conversions.hpp>
#define M_PI 3.14159265358979323846
namespace frame_conversions {
double map(double x, double a, double b, double c, double d) {
return c + (x - a) * (d - c) / (b - a);
}
}// namespace frame_conversions
Eigen::Vector3d sph2cart(const Eigen::Vector3d & sph) {
double r = sph(0);
@ -18,19 +24,20 @@ Eigen::Vector3d cart2sph(const Eigen::Vector3d & cart) {
double z = cart(2);
double r = sqrt(x*x + y*y + z*z);
double theta = acos(z/r);
double phi = atan2(y,x);
double phi = frame_conversions::fmod_pos(atan2(y,x), 2.0*M_PI);
return Eigen::Vector3d(r,theta,phi);
}
Eigen::Vector3d equirectangular2sph(unsigned int i, unsigned int j, unsigned int width, unsigned int height) {
return Eigen::Vector3d(1.0, M_PI*(((double)i)/(height-1)), 2.0*M_PI*(((double)j)/width - 0.5));
Eigen::Vector3d equirectangular2sph(int i, int j, unsigned int width, unsigned int height) {
return Eigen::Vector3d(1.0, std::max(0.0, std::min(M_PI*(((double)i)/(height-1)), M_PI)), frame_conversions::fmod_pos(2.0*M_PI*((double)j)/((double)width), 2.0*M_PI));
}
Eigen::Vector2d sph2equirectangular_d(Eigen::Vector3d const& p_sph, unsigned int width, unsigned int height) {
double theta = p_sph[1];
double phi = p_sph[2];
double i = std::fmod(((height - 1)*theta)/M_PI, height);
double j = std::fmod((width*(M_PI + phi))/(2.0*M_PI), width);
double theta = std::max(0.0, std::min(p_sph[1], M_PI));
double phi = frame_conversions::fmod_pos(p_sph[2], 2.0*M_PI);
double i = ((height - 1)*theta)/M_PI;
// double j = std::fmod((width*phi)/(2.0*M_PI), width);
double j = std::fmod(frame_conversions::map(phi, 0.0, 2.0*M_PI, 0.0, width), width);
return Eigen::Vector2d(i, j);
}

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

@ -0,0 +1,224 @@
#include <Slicer360ToPerspective.hpp>
#include <parser.hpp>
#include <csv_parser.hpp>
#include <filesystem>
#include <logger.hpp>
#include <progress_timer.hpp>
#include <omp.h>
namespace fs = std::filesystem;
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";
}
}
void scan_folder_for_files(const fs::path& folder_path, std::vector<fs::path>& files, bool recursive = true)
{
for(const auto& entry : fs::directory_iterator(folder_path))
{
if(fs::is_directory(entry) && recursive)
scan_folder_for_files(entry, files, recursive);
if(fs::is_regular_file(entry))
files.push_back(entry.path());
}
}
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(!fs::exists(program_config.images_folder)) {
std::cerr << "Error: images folder \"" << program_config.images_folder << "\" does not exist." << std::endl;
return false;
}
}
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.", "-f", 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);
// Create the output folder if it does not exist
if(!fs::exists(program_config.output_folder)) {
Logger::log("Creating output folder \"" + program_config.output_folder + "\"...");
fs::create_directory(program_config.output_folder);
}
// Load the cameras
std::vector<Camera> cameras;
{
Logger::log("Loading cameras from \"" + program_config.cameras_filename + "\"...");
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
Logger::log("Loaded " + std::to_string(cameras.size()) + " cameras.");
if(!cameras.size()) { // If no cameras were loaded, abort
Logger::error("No cameras have been found, aborting now.");
return 1;
}
for(unsigned int i = 0; i < cameras.size(); ++i) {
Eigen::Vector3d euler_angles = cameras[i].GetEulerAngles();
Logger::log("Camera " + std::to_string(i) + " : Euler angles (yaw, pitch, roll) = " + std::to_string(euler_angles[0]) + " " + std::to_string(euler_angles[1]) + " " + std::to_string(euler_angles[2]) + " FOV = " + std::to_string(cameras[i].GetFOV()*RAD2DEG));
}
}
// 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) {
Logger::log("Generating coverage image...");
Image coverage_image = slicer.ComputeCoverage(program_config.size, true);
Logger::log("Coverage image generated.");
auto [min_cov, max_cov, avg_cov] = coverage_image.ComputeMinMaxAvg();
Logger::log("Coverage statistics (per pixel) :\tmin = " + std::to_string(int(min_cov)) + "\tmax = " + std::to_string(int(max_cov)) + "\tavg = " + std::to_string(avg_cov));
Logger::log("Exporting coverage image...");
coverage_image.Normalized().Colorized(Image::Colormap::PARULA).Save(program_config.output_folder + "/coverage." + program_config.output_format);
}
// Generate the perspective images
if(program_config.images_folder != "") {
Logger::log("Generating perspective images...");
std::vector<fs::path> image_files;
scan_folder_for_files(program_config.images_folder, image_files);
ProgressTimer progress_timer(image_files.size());
for(fs::path const& image_file : image_files) {
std::string image_path = image_file.string();
Logger::log("Processing \"" + image_path + "\"...");
Image input_image(image_path);
if(program_config.normalize) {
Logger::log("\tNormalizing image...");
input_image = input_image.HistogramNormalized();
}
slicer.SetPanoramaImage(input_image);
Logger::log("\tProjecting " + image_file.string() + " using " + std::to_string(slicer.cameras.size()) + " cameras...");
std::vector<Image> projected_images = slicer.ProjectToAllCameras();
#pragma omp parallel for
for(unsigned int i = 0; i < projected_images.size(); ++i) {
#pragma omp critical
{Logger::log("\t\tSaving perspective image " + std::to_string(i) + "...");}
std::string output_image_path = program_config.output_folder + "/" + image_file.stem().string() + "_" + std::to_string(i) + "." + program_config.output_format;
projected_images[i].Save(output_image_path);
}
Logger::log(progress_timer.increment());
}
Logger::log(std::to_string(image_files.size()*slicer.cameras.size()) + " perspective images generated from " + std::to_string(image_files.size()) + " input images.");
}
return 0;
}

View file

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

View file

@ -0,0 +1,32 @@
#include <progress_timer.hpp>
#include <iomanip>
#include <cmath>
ProgressTimer::ProgressTimer(size_t num_iterations)
: m_start(std::chrono::steady_clock::now()), m_num_iterations(num_iterations), m_current_iteration(0)
{
}
std::string ProgressTimer::increment()
{
++m_current_iteration;
auto current_time = std::chrono::steady_clock::now();
auto elapsed_time = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - m_start).count();
double progress_percent = static_cast<double>(m_current_iteration) / m_num_iterations * 100;
double elapsed_seconds = elapsed_time / 1000.0;
double remaining_seconds = (m_num_iterations - m_current_iteration) * (elapsed_seconds / m_current_iteration);
return "Progress: " + std::to_string(progress_percent) + " %, Elapsed time: " + seconds_to_hms_str(elapsed_seconds) + ", Remaining time: " + seconds_to_hms_str(remaining_seconds);
}
std::string ProgressTimer::seconds_to_hms_str(double seconds) {
unsigned int hours = seconds / 3600;
unsigned int minutes = std::fmod(seconds, 3600.0) / 60;
unsigned int remaining_seconds = std::fmod(seconds, 60.0);
std::ostringstream oss;
oss << std::setfill('0') << std::setw(2) << hours << ":"
<< std::setfill('0') << std::setw(2) << minutes << ":"
<< std::setfill('0') << std::setw(2) << remaining_seconds << "."
<< std::setfill('0') << std::setw(6) << int(std::fmod(seconds, 1) * 1000000);
return oss.str();
}

View file

@ -1,81 +1,160 @@
#include <iostream>
#include <Camera.hpp>
#include <Image.hpp>
#include <Slicer360ToPerspective.hpp>
#include <frame_conversions.hpp>
#include <parser.hpp>
#include <csv_parser.hpp>
#define _USE_MATH_DEFINES
#include <cmath>
#include <test_framework.hpp>
constexpr double TOL_EQ_TEST = 1e-9;
constexpr double M_PI = 3.14159265358979323846;
static size_t num_tests = 0;
static size_t num_tests_ok = 0;
static size_t num_tests_ko = 0;
#define PRINT_VAR(x) std::cout << #x << "\t= " << (x) << std::endl
// EXPECT_EQ macro
#define EXPECT_EQ(actual, expected) EXPECT_EQ_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_EQ_IMPL(actual, expected, file, line) \
do { \
num_tests++; \
auto&& e = (expected); \
auto&& a = (actual); \
if (e != a) { \
std::cout << file << ":" << line << " : " << "Expected: " << (#expected) << " = " << e << ", actual: " << (#actual) << " = " << a << " " << std::endl; \
num_tests_ko++; \
} \
else { \
num_tests_ok++; \
} \
} while (false)
// EXPECT_DOUBLE_EQ macro
#define EXPECT_DOUBLE_EQ(actual, expected) EXPECT_DOUBLE_EQ_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_DOUBLE_EQ_IMPL(actual, expected, file, line) \
do { \
num_tests++; \
double e = (expected); \
double a = (actual); \
if (std::abs(e - a) > TOL_EQ_TEST) { \
std::cout << file << ":" << line << " : " << "Expected: " << (#expected) << " = " << e << ", actual: " << (#actual) << " = " << a << " " << std::endl; \
num_tests_ko++; \
} \
else { \
num_tests_ok++; \
} \
} while (false)
// EXPECT_VEC_EQ macro
#define EXPECT_VEC_EQ(actual, expected) EXPECT_VEC_EQ_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_VEC_EQ_IMPL(actual, expected, file, line) \
do { \
for(unsigned int i = 0 ; i < (expected).size() ; i++) { \
EXPECT_DOUBLE_EQ_IMPL((actual)[i], (expected)[i], file, line); \
} \
} while (false)
// EXPECT_MAT_EQ macro
#define EXPECT_MAT_EQ(actual, expected) EXPECT_MAT_EQ_IMPL(actual, expected, __FILE__, __LINE__)
#define EXPECT_MAT_EQ_IMPL(actual, expected, file, line) \
do { \
for(unsigned int i = 0 ; i < (expected).rows() ; i++) { \
for(unsigned int j = 0 ; j < (expected).rows() ; j++) { \
EXPECT_DOUBLE_EQ_IMPL((actual)(i,j), (expected)(i,j), file, line); \
} \
} \
} while (false)
#define PRINT_TESTS_SUMMARY() \
do { \
std::cout << "Tests summary: " << num_tests << " tests in total\n " << num_tests_ok << "/" << num_tests << " OK (" << ((100.0*(double)num_tests_ok)/num_tests) << " %)\n " << num_tests_ko << "/" << num_tests << " KO (" << ((100.0*(double)num_tests_ko)/num_tests) << " %)" << std::endl; \
} while (false)
int main() {
int main(int argc, char** argv) {
std::cout.precision(16);
std::cout << "Running Unit Tests\n";
{// Camera
if(0){// argument parser
std::cout << "argument parser\n";
// declare the parser
cmd_line_parser::parser parser(argc, argv);
// configure the parser
parser.add("perc", // name
"A percentage value.", // description
"-p", // shorthand
true, // required argument
false // not boolean option (default is false)
);
parser.add("input_filename", "An input file name.", "-i", true);
parser.add("output_filename", // name
"An output file name.", // description
"-o", // shorthand
false, false);
parser.add("num_trials", "Number of trials.", "-n", false, false);
parser.add("sorted", "Sort output.", "--sort", false,
true // boolean option: a value is not expected after the shorthand
);
parser.add("buffered", "Buffer input.", "--buffer", false, true);
parser.add("ram", "Amount of ram to use.", "--ram", false, false);
// parse command line and return on failure
bool success = parser.parse();
if (!success) return 1;
// now get some variables
auto perc = parser.get<float>("perc"); // deduced type is float
auto input_filename = // deduced type is std::string
parser.get<std::string>("input_filename");
auto sorted_output = parser.get<bool>("sorted"); // deduced type is bool
auto buffered_input = parser.get<bool>("buffered"); // deduced type is bool
size_t ram = 999; // some default value
if (parser.parsed("ram")) {
ram = parser.get<size_t>("ram"); // deduced type is size_t
}
std::cout << "perc: " << perc << std::endl;
std::cout << "input_filename: " << input_filename << std::endl;
std::cout << "sorted_output: " << sorted_output << std::endl;
std::cout << "buffered_input: " << buffered_input << std::endl;
std::cout << "ram: " << ram << std::endl;
try {
auto val = parser.get<int>("bar"); // fail: no name 'bar' was specified
(void)val; // shut up, compiler!
} catch (std::runtime_error const& e) {
std::cerr << e.what() << std::endl;
}
}
if(0){// frame transforms
std::cout << "frame transforms\n";
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, 0., 0.), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, 0., 1.), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, M_PI/2.0, 0.), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, M_PI/2.0, M_PI/2.0), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, M_PI/2.0, M_PI), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, M_PI/2.0, -M_PI/2.0), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, -M_PI/2.0, 0.), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, M_PI/2.0, -M_PI/2.0), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, -M_PI/2.0, -M_PI), 100, 50).transpose());
// PRINT_VAR(sph2equirectangular_d(Eigen::Vector3d(1.0, M_PI/2.0, -2.5*M_PI), 100, 50).transpose());
std::cout << "Testing equirectangular coordinates.\n";
int i = 0;
int j = 0;
int width = 10;
int height = 5;
if(1) { std::cout << "\nTesting equirectangular2sph.\n";
for(i = -2*height ; i < 2*height ; i++) {
for(j = -2*width ; j < 2*width ; j++) {
Eigen::Vector3d p_sph = equirectangular2sph(i, j, width, height);
// std::cout << "i: " << i << ", j: " << j << ", p_sph: " << p_sph.transpose() << "\n";
EXPECT_DOUBLE_EQ(p_sph[0], 1.0);
EXPECT_LT(p_sph[1], M_PI+TOL_EQ_TEST);
EXPECT_GE(p_sph[1], 0.0);
EXPECT_LT(p_sph[2], 2.0*M_PI);
EXPECT_GE(p_sph[2], 0.0);
}
}
}
if(1) { std::cout << "\nTesting roundtrip.\n";
for(i = 0 ; i < height ; i++) {
for(j = 0 ; j < width ; j++) {
Eigen::Vector3d p_sph = equirectangular2sph(i, j, width, height);
Eigen::Vector2d p_equirectangular = sph2equirectangular_d(p_sph, width, height);
// std::cout << "i: " << i << ", j: " << j << ", p_sph: " << p_sph.transpose() << ", p_equirectangular: " << p_equirectangular.transpose() << ((p_equirectangular[0] > height) ? "I OUT OF BOUNDS " : "") << ((p_equirectangular[1] > width) ? "J OUT OF BOUNDS " : "") << "\n";
EXPECT_DOUBLE_EQ(p_equirectangular[0], i);
EXPECT_DOUBLE_EQ(p_equirectangular[1], j);
EXPECT_LT(p_equirectangular[0], height);
EXPECT_GE(p_equirectangular[0], 0.0);
EXPECT_LT(p_equirectangular[1], width);
EXPECT_GE(p_equirectangular[1], 0.0);
}
}
}
if(1) { std::cout << "\nTesting sph2equirectangular_d out of bounds.\n";
width = 3600;
height = 1801;
double theta = 0.0;
double phi = M_PI/4.0;
std::cout << "\nTesting sph2equirectangular_d out of bounds in THETA.\n";
for(theta = -4.0*M_PI ; theta < 4.0*M_PI ; theta += M_PI/10.0) {
auto p_equi = sph2equirectangular_d(Eigen::Vector3d(1.0, theta, phi), width, height).transpose();
// std::cout << "theta: " << theta << ", phi: " << phi << ", p_equirectangular: " << p_equi << "\n";
EXPECT_EQ(p_equi[0] < 0, false);
EXPECT_EQ(p_equi[0] >= height, false);
}
std::cout << "\nTesting sph2equirectangular_d out of bounds in PHI.\n";
theta = M_PI/2.0;
for(phi = -4.0*M_PI ; phi < 4.0*M_PI ; phi += M_PI/10.0) {
auto p_equi = sph2equirectangular_d(Eigen::Vector3d(1.0, theta, phi), width, height).transpose();
// std::cout << "theta: " << theta << ", phi: " << phi << ",\tp_equirectangular: " << p_equi << "\n";
EXPECT_EQ(p_equi[1] < 0, false);
EXPECT_EQ(p_equi[1] >= width, false);
}
}
if(1) { std::cout << "\nTesting fmod.\n";
for(double x = -4.0*M_PI ; x < 4.0*M_PI ; x += M_PI/10.0) {
// std::cout << x << "\t" << std::fmod(x, 2.0*M_PI) << "\t" << frame_conversions::fmod_pos(x, 2.0*M_PI) << "\n";
EXPECT_EQ(frame_conversions::fmod_pos(x, 2.0*M_PI) >= 0.0, true);
EXPECT_EQ(frame_conversions::fmod_pos(x, 2.0*M_PI) < 2.0*M_PI, true);
}
}
}
if(1){// Camera
std::cout << "Camera\n";
{// Constructor, accessors and setters
std::cout << "Constructor, accessors and setters\n";
@ -125,8 +204,10 @@ int main() {
EXPECT_EQ(camera.GetUp(), up);
EXPECT_EQ(camera.GetR(), R);
EXPECT_DOUBLE_EQ(camera.GetFOV(), fov);
// Test point projections
}
{// Test point projections
std::cout << "Test point projections\n";
Camera camera;
camera.SetFOV(20*DEG2RAD);
camera.SetUp(Eigen::Vector3d(0, 0, 1));
camera.SetTarget(Eigen::Vector3d(1.,.5,.3));
@ -165,9 +246,26 @@ int main() {
EXPECT_VEC_EQ(Camera::PixelToNormalizedCoordinates(height-1, width-1, width, height), Eigen::Vector2d(1.0, -1.0));
EXPECT_VEC_EQ(Camera::PixelToNormalizedCoordinates( 0, width-1, width, height), Eigen::Vector2d(1.0, 1.0));
}
{// test Euler angles conversion
std::cout << "Euler angles\n";
PRINT_VAR(Camera().SetEulerAngles(0.0, 0.0, 0.0).GetR());
PRINT_VAR(Camera().SetEulerAngles(45.0, 0.0, 0.0).GetR());
PRINT_VAR(Camera().SetEulerAngles(45.0, 30.0, 0.0).GetR());
PRINT_VAR(Camera().SetEulerAngles(0.0, 0.0, 45.0).GetR());
EXPECT_EQ(Camera().SetEulerAngles(45.0, 0.0, 0.0).GetR(), Camera().SetEulerAngles(45.0*DEG2RAD, 0.0, 0.0, true).GetR());// input in radians vs degrees
}
{// Test Camera creation from CSV line
std::cout << "Creation from CSV line\n";
std::vector<double> yaw_pitch_roll_FOV = {45.0, 30.0, -15.0, 90.0};
Camera camera = Camera::FromYawPitchRollFOV(yaw_pitch_roll_FOV);
EXPECT_EQ(camera.GetFOV(), yaw_pitch_roll_FOV[3]*DEG2RAD);
EXPECT_EQ(camera.GetR(), Camera().SetEulerAngles(yaw_pitch_roll_FOV[0], yaw_pitch_roll_FOV[1], yaw_pitch_roll_FOV[2]).GetR());
}
}
{// Image
if(0){// Image
std::cout << "Image\n";
{
std::cout << "Constructors\n";
@ -300,6 +398,10 @@ int main() {
Image image_luma = image.LumaREC709();
image_gray.Save("venise_gray.jpg");
image_luma.Save("venise_luma.jpg");
std::cout << "Colormap application\n";
Image image_parula = image_luma.Colorized(Image::Colormap::PARULA);
image_parula.Save("venise_parula.jpg");
}
{
@ -310,6 +412,84 @@ int main() {
}
}
if(0){// Test Slicer360ToPerspective
if(0) { std::cout << "Coverage analysis\n";
Slicer360ToPerspective slicer;
double fov = 90.0*DEG2RAD;
int width = 500;
slicer.cameras.push_back(Camera().SetFOV(fov).SetTarget(Eigen::Vector3d(1., 0., 0.)));
slicer.cameras.push_back(Camera().SetFOV(fov).SetTarget(Eigen::Vector3d(-1., -1., -1.)));
slicer.cameras.push_back(Camera().SetFOV(fov/2).SetTarget(Eigen::Vector3d(1., 0.2, -0.3)));
Image coverage_raw = slicer.ComputeCoverage(width, true);
Image coverage_col = slicer.ComputeCoverage(width, false);
coverage_raw.Save("coverage_raw.png");
coverage_col.Save("coverage_col.png");
}
if(1){ std::cout << "Perspective Projection\n";
double Mpx = 1e6; // Image size in Mpx
double ratio = 16./9.; // Image ratio
int width = sqrt(Mpx)*sqrt(ratio);
int height = sqrt(Mpx)/sqrt(ratio);
Slicer360ToPerspective slicer = Slicer360ToPerspective(Image("../venise.jpg")).SetOutputImageSize(width, height).SetInterpolationMethod(Image::InterpMethod::BILINEAR);
Camera camera = Camera().SetFOV(90.0*DEG2RAD).SetTarget(Eigen::Vector3d(1., 0., 0.));
slicer.cameras.push_back(camera);
slicer.ProjectToCamera(camera, true).Save("venise_proj_01.png");
// measure performance
double t0, t1;
t0 = omp_get_wtime();
slicer.ProjectToCamera(camera, false);
t1 = omp_get_wtime();
std::cout << "Projection time single-threaded BILINEAR : " << t1-t0 << " s\n";
t0 = omp_get_wtime();
slicer.ProjectToCamera(camera, true);
t1 = omp_get_wtime();
std::cout << "Projection time multi-threaded BILINEAR : " << t1-t0 << " s\n";
slicer.SetInterpolationMethod(Image::InterpMethod::NEAREST);
t0 = omp_get_wtime();
slicer.ProjectToCamera(camera, false);
t1 = omp_get_wtime();
std::cout << "Projection time single-threaded NEAREST : " << t1-t0 << " s\n";
t0 = omp_get_wtime();
slicer.ProjectToCamera(camera, true);
t1 = omp_get_wtime();
std::cout << "Projection time multi-threaded NEAREST : " << t1-t0 << " s\n";
}
}
if(1){// CSV parser
std::cout << "CSV parser\n";
{ std::cout << "Valid CSV file\n";
auto data = parse_csv_file("../data_test_valid.csv");
EXPECT_EQ(data.empty(), false);
for(auto& row : data) {
for(auto& cell : row) {
std::cout << cell << "\t";
}
std::cout << std::endl;
}
}
{ std::cout << "Invalid CSV file (missing column on one row)\n";
auto data = parse_csv_file("../data_test_invalid.csv");
EXPECT_EQ(data.empty(), true);
}
{ std::cout << "Invalid CSV file (bad floating-point number)\n";
auto data = parse_csv_file("../data_test_invalid_floating_point.csv");
EXPECT_EQ(data.empty(), true);
}
{ std::cout << "Non-existent CSV file\n";
auto data = parse_csv_file("does_not_exist.csv");
EXPECT_EQ(data.empty(), true);
}
}
PRINT_TESTS_SUMMARY();
return 0;