Version 1.0
This commit is contained in:
commit
cfc1cf81dd
10 changed files with 825 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
obj/*
|
||||
.vscode/*
|
||||
input/*
|
||||
output/*
|
||||
27
Makefile
Normal file
27
Makefile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
CXX = g++
|
||||
CXXFLAGS = -std=c++17 -Wall -Wextra -pedantic -O3
|
||||
INCLUDES = -I./include -ID:/Users/Jerome/Documents/Ingenierie/Programmation/eigen-3.4.0 -I../360toPerspective/cpp/stb-master -ID:/Users/Jerome/Documents/Ingenierie/Programmation/cmd_line_parser-master/include
|
||||
LDFLAGS = -fopenmp
|
||||
|
||||
SRC_DIR = src
|
||||
OBJ_DIR = obj
|
||||
|
||||
SRCS = $(wildcard $(SRC_DIR)/*.cpp)
|
||||
OBJS = $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS))
|
||||
EXEC = $(OBJ_DIR)/image_normalizer
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(EXEC)
|
||||
|
||||
$(EXEC): $(OBJS)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $^
|
||||
|
||||
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR)
|
||||
$(CXX) $(CXXFLAGS) $(INCLUDES) $(LDFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJ_DIR):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJ_DIR)
|
||||
189
include/Image.hpp
Normal file
189
include/Image.hpp
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
#ifndef H_Image
|
||||
#define H_Image
|
||||
|
||||
#include <vector>
|
||||
#include <Eigen/Dense>
|
||||
#include <stb_image.h>
|
||||
#include <stb_image_write.h>
|
||||
|
||||
/// @brief This class encapsulates the raw data of an image.
|
||||
/// The Image class has a constructor that takes the width, height, and optionally depth of the image, and dynamically allocates a buffer of size width * height * depth to store the image data. The destructor deallocates the buffer to avoid memory leaks.
|
||||
/// The class provides three accessor methods GetWidth(), GetHeight(), and GetDepth() to query the image dimensions.
|
||||
/// The GetPixel() method takes the x, y, and z coordinates of the pixel (in row-major order) and returns the corresponding value from the buffer.
|
||||
/// The SetPixel() method takes the x, y, and z coordinates of the pixel and a value, and sets the corresponding pixel in the buffer to the given value.
|
||||
/// Note that the GetPixel() and SetPixel() methods use the formula z * width * height + y * width + x to compute the index of the pixel in the buffer. This formula assumes row-major order, where the x coordinate varies fastest, followed by the y coordinate, and then the z coordinate. If the image had a different ordering, the formula would need to be adjusted accordingly.
|
||||
class Image {
|
||||
public:
|
||||
/// @brief Interpolation methods.
|
||||
enum class InterpMethod {
|
||||
NEAREST, ///< Nearest neighbor interpolation.
|
||||
BILINEAR, ///< Bilinear interpolation.
|
||||
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_ = 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 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.
|
||||
int Save(std::string const& filename) const;
|
||||
|
||||
int GetWidth() const;
|
||||
int GetHeight() const;
|
||||
int GetDepth() const;
|
||||
|
||||
/// @brief Fills the image with the given value. All channels are set.
|
||||
/// @param value Value to fill the image with.
|
||||
Image & Fill(unsigned char value);
|
||||
|
||||
/// @brief Fills the image with the given rgb value.
|
||||
/// @param rgb RGB value to fill the image with.
|
||||
Image & Fill(Eigen::Vector3i const& rgb);
|
||||
|
||||
/// @brief Returns the value of the chosen channel of the given pixel.
|
||||
/// @param i Row index.
|
||||
/// @param j Column index.
|
||||
/// @param c Channel index.
|
||||
/// @return The value of the chosen channel of the given pixel.
|
||||
unsigned char GetPixelValue(int i, int j, int c = 0) const;
|
||||
|
||||
/// @brief Sets the value of the chosen channel of the given pixel.
|
||||
/// @param i Row index.
|
||||
/// @param j Column index.
|
||||
/// @param c Channel index.
|
||||
/// @param value Value of the chosen channel of the given pixel.
|
||||
void SetPixelValue(int i, int j, int c, unsigned char value);
|
||||
|
||||
/// @brief Returns a mutable reference to the chosen channel of the given pixel.
|
||||
/// @param i Row index.
|
||||
/// @param j Column index.
|
||||
/// @param c Channel index.
|
||||
/// @return A mutable reference to the value of the chosen channel of the given pixel.
|
||||
unsigned char & PixelValue(int i, int j, int c = 0);
|
||||
|
||||
/// @brief Returns the given pixel as a 3D-vector containing the RGB values.
|
||||
/// @param i Row index.
|
||||
/// @param j Column index.
|
||||
/// @return Given pixel as a 3D-vector containing the RGB values.
|
||||
Eigen::Vector3i GetPixel(int i, int j) const;
|
||||
|
||||
/// @brief Returns the given pixel as a 3D-vector containing the RGB values.
|
||||
/// @param pos 2D vector containing the row and column indices.
|
||||
/// @return Given pixel as a 3D-vector containing the RGB values.
|
||||
Eigen::Vector3i GetPixel(Eigen::Vector2i const& pos) const;
|
||||
|
||||
/// @brief Returns the given pixel as a 3D-vector containing the RGB values, using the selected interpolation method.
|
||||
/// @param pos 2D vector containing the row and column indices. Floating point values are used.
|
||||
/// @param interp_method Interpolation method to use.
|
||||
/// @return Interpolated pixel as a 3D-vector containing the RGB values.
|
||||
Eigen::Vector3i GetPixelInterp(Eigen::Vector2d const& pos, InterpMethod const& interp_method) const;
|
||||
|
||||
/// @brief Sets the given pixel to the given RGB values.
|
||||
/// @param i Row index.
|
||||
/// @param j Column index.
|
||||
/// @param rgb 3D-vector containing the RGB values.
|
||||
/// @return Reference to the image.
|
||||
Image & SetPixel(int i, int j, Eigen::Vector3i const& rgb);
|
||||
|
||||
/// @brief Sets the given pixel to the given RGB values.
|
||||
/// @param pos 2D vector containing the row and column indices.
|
||||
/// @param rgb 3D-vector containing the RGB values.
|
||||
/// @return Reference to the image.
|
||||
Image & SetPixel(Eigen::Vector2i const& pos, Eigen::Vector3i const& rgb);
|
||||
|
||||
/// @brief Accesses the value at the given index of the raw image data.
|
||||
/// @param i Index of the element. Max is (width * height * depth) - 1.
|
||||
/// @return The value at the given index of the raw image data.
|
||||
unsigned char operator[](int i) const;
|
||||
|
||||
/// @brief Returns a new image that has been downsampled by a given factor.
|
||||
/// @param factor Factor by which the image is downsampled. A factor of 2 will halve the width and height, thus reducing the pixel count by a factor of 2^2 = 4.
|
||||
Image Downsampled(int factor) const;
|
||||
|
||||
/// @brief Returns a new image that represents the grayscale version of the current image.
|
||||
/// @return A new image that represents the grayscale version of the current image (average of RGB components).
|
||||
Image Grayscale() const;
|
||||
|
||||
/// @brief Returns a new image that represents the Luma version of the current image, following the REC709 standard.
|
||||
/// @return A new image that represents the Luma version of the current image, following the REC709 standard.
|
||||
Image LumaREC709() const;
|
||||
|
||||
/// @brief Performs histrogram normalization on the image (in place). 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 & 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.
|
||||
/// @param downscale_factor Factor by which the image is downsampled before computing the histogram.
|
||||
/// @return The histogram of the image.
|
||||
std::vector<uint64_t> ComputeHistogram(int downscale_factor = 1) const;
|
||||
|
||||
/// @brief Computes the cumulative histogram of the image from its histogram.
|
||||
static std::vector<uint64_t> CumulativeHistogram(std::vector<uint64_t> const& histogram);
|
||||
|
||||
private:
|
||||
int width;
|
||||
int height;
|
||||
int depth;
|
||||
unsigned char* data;
|
||||
};
|
||||
|
||||
#endif
|
||||
31
include/logger.hpp
Normal file
31
include/logger.hpp
Normal 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
|
||||
29
include/progress_timer.hpp
Normal file
29
include/progress_timer.hpp
Normal 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
|
||||
291
src/Image.cpp
Normal file
291
src/Image.cpp
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
#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];
|
||||
}
|
||||
|
||||
Image::Image(Image const& other) : width(other.width), height(other.height), depth(other.depth) {
|
||||
data = new unsigned char[width * height * depth];
|
||||
std::memcpy(data, other.data, width * height * depth);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Allocate memory using new[] and copy the data from image.data. This avoids allocating memory with malloc and freeing it with delete.
|
||||
if (image_data != nullptr) {
|
||||
data = new unsigned char[width * height * depth];
|
||||
std::memcpy(data, image_data, width * height * depth);
|
||||
stbi_image_free(image_data);
|
||||
} else { // If the image loading process has failed
|
||||
width = 1;
|
||||
height = 1;
|
||||
depth = 1;
|
||||
data = new unsigned char[width * height * depth];
|
||||
data[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if(filename.size() > 4 && filename.substr(filename.size() - 4) == ".jpg")
|
||||
return stbi_write_jpg(filename.c_str(), width, height, depth, (const void *)data, 100);
|
||||
if(filename.size() > 4 && filename.substr(filename.size() - 4) == ".bmp")
|
||||
return stbi_write_bmp(filename.c_str(), width, height, depth, (const void *)data);
|
||||
if(filename.size() > 4 && filename.substr(filename.size() - 4) == ".tga")
|
||||
return stbi_write_tga(filename.c_str(), width, height, depth, (const void *)data);
|
||||
return -1;// If the file extension is not supported
|
||||
}
|
||||
|
||||
int Image::GetWidth() const {
|
||||
return width;
|
||||
}
|
||||
|
||||
int Image::GetHeight() const {
|
||||
return height;
|
||||
}
|
||||
|
||||
int Image::GetDepth() const {
|
||||
return depth;
|
||||
}
|
||||
|
||||
Image & Image::Fill(unsigned char value) {
|
||||
std::memset(data, value, width * height * depth);
|
||||
return *this;
|
||||
}
|
||||
|
||||
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);
|
||||
return *this;
|
||||
}
|
||||
|
||||
unsigned char Image::GetPixelValue(int i, int j, int c) const {
|
||||
return data[i*width*depth + j*depth + c];
|
||||
}
|
||||
|
||||
void Image::SetPixelValue(int i, int j, int c, unsigned char value) {
|
||||
data[i*width*depth + j*depth + c] = value;
|
||||
}
|
||||
|
||||
unsigned char & Image::PixelValue(int i, int j, int c) {
|
||||
return data[i*width*depth + j*depth + c];
|
||||
}
|
||||
|
||||
Eigen::Vector3i Image::GetPixel(int i, int j) const {
|
||||
int index = i*width*depth + j*depth;
|
||||
return Eigen::Vector3i((int)data[index], (int)data[index + 1], (int)data[index + 2]);
|
||||
}
|
||||
|
||||
Eigen::Vector3i Image::GetPixel(Eigen::Vector2i const& pos) const {
|
||||
return GetPixel(pos[0], pos[1]);
|
||||
}
|
||||
|
||||
Eigen::Vector3i Image::GetPixelInterp(Eigen::Vector2d const& pos, InterpMethod const& interp_method) const {
|
||||
if(interp_method == Image::InterpMethod::BILINEAR) {
|
||||
int i = (int)std::floor(pos[0]);
|
||||
int j = (int)std::floor(pos[1]);
|
||||
double u = pos[0] - i;
|
||||
double v = pos[1] - j;
|
||||
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
|
||||
return GetPixel((int)std::round(pos[0]), (int)std::round(pos[1]));
|
||||
}
|
||||
|
||||
Image & Image::SetPixel(int i, int j, Eigen::Vector3i const& rgb) {
|
||||
SetPixelValue(i, j, 0, (unsigned char)rgb[0]);
|
||||
SetPixelValue(i, j, 1, (unsigned char)rgb[1]);
|
||||
SetPixelValue(i, j, 2, (unsigned char)rgb[2]);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Image & Image::SetPixel(Eigen::Vector2i const& pos, Eigen::Vector3i const& rgb) {
|
||||
SetPixel(pos[0], pos[1], rgb);
|
||||
return *this;
|
||||
}
|
||||
|
||||
unsigned char Image::operator[](int i) const {
|
||||
return data[i];
|
||||
}
|
||||
|
||||
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));
|
||||
return downsampled;
|
||||
}
|
||||
|
||||
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);
|
||||
grayscale.SetPixelValue(i, j, 0, ((int)rgb[0] + (int)rgb[1] + (int)rgb[2]) / 3);
|
||||
}
|
||||
return grayscale;
|
||||
}
|
||||
|
||||
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);
|
||||
grayscale.SetPixelValue(i, j, 0, ((double)rgb[0]*0.2126 + (double)rgb[1]*0.7152 + (double)rgb[2]*0.0722));
|
||||
}
|
||||
return grayscale;
|
||||
}
|
||||
|
||||
Image & Image::HistogramNormalize(int downsampling_factor) {
|
||||
// First, compute the histogram
|
||||
std::vector<uint64_t> hist = ComputeHistogram(downsampling_factor);
|
||||
// Then, compute the cumulative histogram
|
||||
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);
|
||||
SetPixel(i, j, Eigen::Vector3i(cumul_hist[rgb[0]] * 255 / max_cum_hist_val, cumul_hist[rgb[1]] * 255 / max_cum_hist_val, cumul_hist[rgb[2]] * 255 / max_cum_hist_val));
|
||||
}
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
std::vector<uint64_t> Image::CumulativeHistogram(std::vector<uint64_t> const& histogram) {
|
||||
std::vector<uint64_t> cumul_hist(histogram.size(), 0);
|
||||
cumul_hist[0] = histogram[0];
|
||||
for(unsigned int i = 1 ; i < histogram.size() ; i++)
|
||||
cumul_hist[i] = cumul_hist[i - 1] + histogram[i];
|
||||
return cumul_hist;
|
||||
}
|
||||
218
src/main.cpp
Normal file
218
src/main.cpp
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <parser.hpp>
|
||||
#include <Image.hpp>
|
||||
#include <logger.hpp>
|
||||
#include <progress_timer.hpp>
|
||||
#include <omp.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::string to_lower(const std::string& str)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(str.size());
|
||||
|
||||
std::transform(str.begin(), str.end(), std::back_inserter(result),
|
||||
[](char c){ return std::tolower(c); });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
bool is_file_an_image(fs::path const& image_file) {
|
||||
std::string extension = to_lower(image_file.extension().string());
|
||||
return (extension == ".jpg" || extension == ".png" || extension == ".bmp" || extension == ".tga");
|
||||
}
|
||||
|
||||
struct ProgramConfiguration {
|
||||
std::string images_folder = "";
|
||||
std::string output_folder = ".";
|
||||
std::string output_format = "jpg";
|
||||
Image::InterpMethod interpolation_mode = Image::InterpMethod::BILINEAR;
|
||||
bool normalize = false;
|
||||
bool resize = 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 << "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 << "Resize input images " << program_config.resize << std::endl;
|
||||
std::cout << "Output image width " << 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("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("resize")) {
|
||||
program_config.resize = parser.get<bool>("resize");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if(!program_config.resize && !program_config.normalize) {
|
||||
std::cerr << "Warning: No action selected ! The images will only be transcoded to the selected output format." << std::endl;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void configure_parser(cmd_line_parser::parser & parser) {
|
||||
// name, description, shorthand, required, is a boolean or not (false by default)
|
||||
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", true);
|
||||
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("resize", "Resize the imagesa after normalization. Specify the width of the output images using -s", "-r", false, true);
|
||||
parser.add("normalize", "If set, the images are histogram-normalized. This equalizes the utilization of all intensity values available.", "-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);
|
||||
}
|
||||
|
||||
// Process the images
|
||||
Logger::log("Processing images...");
|
||||
std::vector<fs::path> image_files;
|
||||
scan_folder_for_files(program_config.images_folder, image_files);
|
||||
|
||||
ProgressTimer progress_timer(image_files.size());
|
||||
omp_lock_t lock_logger;// OpenMP lock for the logger to avoid mangled output
|
||||
omp_init_lock(&lock_logger);
|
||||
|
||||
#pragma omp parallel for
|
||||
for(fs::path const& image_file : image_files) {
|
||||
if(!is_file_an_image(image_file))
|
||||
continue;
|
||||
std::string image_path = image_file.string();
|
||||
{
|
||||
omp_set_lock(&lock_logger);// Lock the logger to avoid mangled output
|
||||
Logger::log("Processing \"" + image_path + "\"...");
|
||||
omp_unset_lock(&lock_logger);// Unlock the logger
|
||||
}
|
||||
Image input_image(image_path);
|
||||
if(program_config.normalize) {
|
||||
{
|
||||
omp_set_lock(&lock_logger);// Lock the logger to avoid mangled output
|
||||
Logger::log("Normalizing \"" + image_path + "\"...");
|
||||
omp_unset_lock(&lock_logger);// Unlock the logger
|
||||
}
|
||||
input_image = input_image.HistogramNormalized();
|
||||
}
|
||||
if(program_config.resize) {
|
||||
{
|
||||
omp_set_lock(&lock_logger);// Lock the logger to avoid mangled output
|
||||
Logger::log("Resizing \"" + image_path + "\"...");
|
||||
omp_unset_lock(&lock_logger);// Unlock the logger
|
||||
}
|
||||
double resize_factor = (double)program_config.size / (double)input_image.GetWidth();
|
||||
input_image = input_image.Resized(input_image.GetWidth()*resize_factor, input_image.GetHeight()*resize_factor);
|
||||
}
|
||||
input_image.Save(program_config.output_folder + "/" + image_file.stem().string() + "." + program_config.output_format);
|
||||
{
|
||||
omp_set_lock(&lock_logger);// Lock the logger to avoid mangled output
|
||||
Logger::log(progress_timer.increment());
|
||||
omp_unset_lock(&lock_logger);// Unlock the logger
|
||||
}
|
||||
}
|
||||
Logger::log(std::to_string(image_files.size()) + " images processed.");
|
||||
|
||||
omp_destroy_lock(&lock_logger);// Destroy the OpenMP lock for the logger
|
||||
return 0;
|
||||
}
|
||||
32
src/progress_timer.cpp
Normal file
32
src/progress_timer.cpp
Normal 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();
|
||||
}
|
||||
2
src/stb_image.cpp
Normal file
2
src/stb_image.cpp
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include <stb_image.h>
|
||||
2
src/stb_image_write.cpp
Normal file
2
src/stb_image_write.cpp
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include <stb_image_write.h>
|
||||
Loading…
Add table
Reference in a new issue