From d4e09bf957366fc2956ed5ea606cb2e4426d6730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Colin=20de=20Verdi=C3=A8re?= Date: Fri, 4 Apr 2025 14:53:45 +0200 Subject: [PATCH] comments the code and adds information --- src/image.h | 14 ++- src/seam-carving.cpp | 269 ++++++++++++++++++++++--------------------- src/utils.cpp | 56 ++++----- src/utils.hpp | 60 +++++----- 4 files changed, 199 insertions(+), 200 deletions(-) diff --git a/src/image.h b/src/image.h index d123c54..eba6d74 100644 --- a/src/image.h +++ b/src/image.h @@ -1,19 +1,21 @@ #ifndef STBIDEF #ifdef STB_IMAGE_WRITE_STATIC -#define STBIDEF static +#define STBIDEF static #else #ifdef __cplusplus -#define STBIDEF extern "C" +#define STBIDEF extern "C" #else -#define STBIDEF extern +#define STBIDEF extern #endif #endif #endif typedef unsigned char stbi_uc; -STBIDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIDEF int stbi_write_png(char const *filename, int w, int h, int comp, + const void *data, int stride_in_bytes); -STBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp); +STBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, + int req_comp); -STBIDEF void stbi_image_free (void *retval_from_stbi_load); +STBIDEF void stbi_image_free(void *retval_from_stbi_load); diff --git a/src/seam-carving.cpp b/src/seam-carving.cpp index 78ae877..88522ed 100644 --- a/src/seam-carving.cpp +++ b/src/seam-carving.cpp @@ -20,6 +20,113 @@ bool until_mask_removal = false; int max_step = 1; std::string function = "grad"; +/* The next macro is ugly, but speeds up the computation. It computes the + * energy at one pixel of the image, depending on the energy function used */ +#define compute_energy_for_pixel( \ + source, width, height, i, j, \ + nbChannels, /* computes the energy at pixel i,j, i.e. energy[width*j+i]*/ \ + nbColorChannels, dest) \ + { \ + auto indexPixel = (nbChannels) * (width * (j) + (i)); \ + auto indexPixel_up = \ + ((j) - 1 > 0) ? (nbChannels) * (width * ((j) - 1) + (i)) : indexPixel; \ + auto indexPixel_down = ((j) + 1 < height) \ + ? (nbChannels) * (width * ((j) + 1) + (i)) \ + : indexPixel; \ + auto indexPixel_left = \ + ((i) - 1 > 0) ? (nbChannels) * (width * (j) + ((i) - 1)) : indexPixel; \ + auto indexPixel_right = ((i) + 1 < width) \ + ? (nbChannels) * (width * (j) + ((i) + 1)) \ + : indexPixel; \ + dest = 0; \ + if (function == "grad") { \ + for (auto ch = 0; ch < (nbColorChannels); ch++) { \ + dest += ((fabs((float)source[indexPixel_up + ch] - \ + source[indexPixel + ch])) + \ + \ + (fabs((float)source[indexPixel_down + ch] - \ + source[indexPixel + ch])) + \ + \ + (fabs((float)source[indexPixel_left + ch] - \ + source[indexPixel + ch])) + \ + (fabs((float)source[indexPixel_right + ch] - \ + source[indexPixel + ch]))); \ + } \ + } else if (function == "gradnorm") { \ + for (auto ch = 0; ch < (nbColorChannels); ch++) { \ + dest += (std::pow(fabs((float)source[indexPixel_up + ch] - \ + source[indexPixel + ch]), \ + 2) + \ + \ + std::pow(fabs((float)source[indexPixel_down + ch] - \ + source[indexPixel + ch]), \ + 2) + \ + \ + std::pow(fabs((float)source[indexPixel_left + ch] - \ + source[indexPixel + ch]), \ + 2) + \ + std::pow(fabs((float)source[indexPixel_right + ch] - \ + source[indexPixel + ch]), \ + 2)); \ + } \ + } else if (function == "gradhoriz") { \ + /* take the gradient along the horizontal only*/ \ + for (auto ch = 0; ch < (nbColorChannels); ch++) { \ + dest += ((fabs((float)source[indexPixel_left + ch] - \ + source[indexPixel + ch])) + \ + (fabs((float)source[indexPixel_right + ch] - \ + source[indexPixel + ch]))); \ + } \ + } else if (function == "gradvertic") { \ + /* take the gradient along the vertical only*/ \ + for (auto ch = 0; ch < (nbColorChannels); ch++) { \ + dest += ((fabs((float)source[indexPixel_up + ch] - \ + source[indexPixel + ch])) + \ + (fabs((float)source[indexPixel_down + ch] - \ + source[indexPixel + ch]))); \ + } \ + } else if (function == "gradnorminf") { \ + for (auto ch = 0; ch < (nbColorChannels); ch++) { \ + dest += std::max(std::max((fabs((float)source[indexPixel_up + ch] - \ + source[indexPixel + ch])), \ + \ + (fabs((float)source[indexPixel_down + ch] - \ + source[indexPixel + ch]))), \ + std::max( \ + \ + (fabs((float)source[indexPixel_left + ch] - \ + source[indexPixel + ch])), \ + (fabs((float)source[indexPixel_right + ch] - \ + source[indexPixel + ch])))); \ + } \ + } else if (function == "sumexpgradcomp") { \ + \ + for (auto ch = 0; ch < (nbColorChannels); ch++) { \ + dest += (std::exp(fabs((float)source[indexPixel_up + ch] - \ + source[indexPixel + ch]) / \ + 255) + \ + \ + std::exp(fabs((float)source[indexPixel_down + ch] - \ + source[indexPixel + ch]) / \ + 255) + \ + \ + std::exp(fabs((float)source[indexPixel_left + ch] - \ + source[indexPixel + ch]) / \ + 255) + \ + std::exp(fabs((float)source[indexPixel_right + ch] - \ + source[indexPixel + ch]) / \ + 255)); \ + } \ + } else { \ + std::cerr << "no implementation found for function \"" << function \ + << "\"" << std::endl; \ + exit(1); \ + } \ + } + +// Le alpha n'est pas pris en compte dans l'énergie +// Here, we use this /ugly/ macro to avoid defining a function that would be way +// slower... void export_image(const char *filename, const void *data, int width, int height, int nbChannels) { @@ -33,117 +140,9 @@ void export_image(const char *filename, const void *data, int width, int height, } } - -#define compute_energy_for_pixel( \ - source, width, height, i, j, \ - nbChannels, /* computes the energy at pixel i,j, i.e. energy[width*j+i]*/ \ - nbColorChannels, dest) { \ - auto indexPixel = (nbChannels) * (width * (j) + (i)); \ - auto indexPixel_up = \ - ((j) - 1 > 0) ? (nbChannels) * (width * ((j) - 1) + (i)) : indexPixel; \ - auto indexPixel_down = ((j) + 1 < height) \ - ? (nbChannels) * (width * ((j) + 1) + (i)) \ - : indexPixel; \ - auto indexPixel_left = \ - ((i) - 1 > 0) ? (nbChannels) * (width * (j) + ((i) - 1)) : indexPixel; \ - auto indexPixel_right = ((i) + 1 < width) \ - ? (nbChannels) * (width * (j) + ((i) + 1)) \ - : indexPixel; \ - dest = 0; \ - if (function == "grad") { \ - for (auto ch = 0; ch < (nbColorChannels); ch++) { \ - dest += ((fabs((float)source[indexPixel_up + ch] - \ - source[indexPixel + ch])) + \ - \ - (fabs((float)source[indexPixel_down + ch] - \ - source[indexPixel + ch])) + \ - \ - (fabs((float)source[indexPixel_left + ch] - \ - source[indexPixel + ch])) + \ - (fabs((float)source[indexPixel_right + ch] - \ - source[indexPixel + ch]))); \ - } \ - } else if (function == "gradnorm") { \ - for (auto ch = 0; ch < (nbColorChannels); ch++) { \ - dest += (std::pow(fabs((float)source[indexPixel_up + ch] - \ - source[indexPixel + ch]), \ - 2) + \ - \ - std::pow(fabs((float)source[indexPixel_down + ch] - \ - source[indexPixel + ch]), \ - 2) + \ - \ - std::pow(fabs((float)source[indexPixel_left + ch] - \ - source[indexPixel + ch]), \ - 2) + \ - std::pow(fabs((float)source[indexPixel_right + ch] - \ - source[indexPixel + ch]), \ - 2)); \ - } \ - } else if (function == "gradhoriz") { \ - /* take the gradient along the horizontal only*/ \ - for (auto ch = 0; ch < (nbColorChannels); ch++) { \ - dest += ((fabs((float)source[indexPixel_left + ch] - \ - source[indexPixel + ch])) + \ - (fabs((float)source[indexPixel_right + ch] - \ - source[indexPixel + ch]))); \ - } \ - } else if (function == "gradvertic") { \ - /* take the gradient along the vertical only*/ \ - for (auto ch = 0; ch < (nbColorChannels); ch++) { \ - dest += ((fabs((float)source[indexPixel_up + ch] - \ - source[indexPixel + ch])) + \ - (fabs((float)source[indexPixel_down + ch] - \ - source[indexPixel + ch]))); \ - } \ - } else if (function == "gradnorminf") { \ - for (auto ch = 0; ch < (nbColorChannels); ch++) { \ - dest += std::max(std::max((fabs((float)source[indexPixel_up + ch] - \ - source[indexPixel + ch])), \ - \ - (fabs((float)source[indexPixel_down + ch] - \ - source[indexPixel + ch]))), \ - std::max( \ - \ - (fabs((float)source[indexPixel_left + ch] - \ - source[indexPixel + ch])), \ - (fabs((float)source[indexPixel_right + ch] - \ - source[indexPixel + ch])))); \ - } \ - } else if (function == "sumexpgradcomp") { \ - \ - for (auto ch = 0; ch < (nbColorChannels); ch++) { \ - dest += (std::exp(fabs((float)source[indexPixel_up + ch] - \ - source[indexPixel + ch]) / \ - 255) + \ - \ - std::exp(fabs((float)source[indexPixel_down + ch] - \ - source[indexPixel + ch]) / \ - 255) + \ - \ - std::exp(fabs((float)source[indexPixel_left + ch] - \ - source[indexPixel + ch]) / \ - 255) + \ - std::exp(fabs((float)source[indexPixel_right + ch] - \ - source[indexPixel + ch]) / \ - 255)); \ - } \ - } else { \ - std::cerr << "no implementation found for function \"" << function << "\"" \ - << std::endl; \ - exit(1); \ - }} - - - -// Le alpha n'est pas pris en compte dans l'énergie -// Here, we use this /ugly/ macro to avoid defining a function that would be way -// slower... - - -/** e_1 energy*/ std::vector energy_e1(std::vector source, int width, int height, int nbChannels) { + /* Compute the energy of an image, using the previous macro. */ int nbColorChannels = nbChannels > 3 ? 3 : nbChannels; // nombre de canaux, excepté le alpha std::vector energy(width * height); @@ -158,10 +157,10 @@ std::vector energy_e1(std::vector source, int width, return energy; } -/** Given the energy value, returns the optimal seam */ template std::vector optimal_seam(std::vector energy, int width, int height, bool vertical) { + /* Performs dynamic programming to find the optimal seam given the energy. */ // dyn_energy is indexed by [dim_large*(i : dim_long) + (j : dim_large)] std::vector dyn_energy(width * height); @@ -171,7 +170,7 @@ std::vector optimal_seam(std::vector energy, int width, int height, //* Find an end of the minimal connected vertical/horizontal seam for (auto i = 0; i < dim_large; i++) { - dyn_energy[i] = energy[vertical ? i : i*width]; + dyn_energy[i] = energy[vertical ? i : i * width]; } for (auto i = 1; i < dim_long; i++) { // Propagate dyn_energy @@ -248,11 +247,12 @@ std::vector optimal_seam(std::vector energy, int width, int height, return result; } -/** Carves an image by one seam. Returns the optimal seam used */ template void remove_seam(const std::vector source, std::vector &output, int width, int height, int nbChannels, bool vertical, const std::vector seam) { + /* Carves an image by one seam. (the image can be a real image, or an energy + * vector). */ // remove the given seam from the image, the result is in output // the output must have at least the right size! int dim_large = vertical ? width : height; @@ -272,8 +272,11 @@ void remove_seam(const std::vector source, std::vector &output, int width, } } -// It would be preferable to use templates only for the value assignation but -// this is in fact far less efficient +// The two next functions are used to update the energy after one seamcarving +// step on the image. It recomputes the energy along the seam. It would be +// preferable to use templates only for the value assignation but this is in +// fact far less efficient + void recompute_energy_along_seam(std::vector carved_img, std::vector &output_energy, std::vector opt_seam, int width, @@ -287,6 +290,7 @@ void recompute_energy_along_seam(std::vector carved_img, for (auto j0 = 0; j0 < dim_long; j0++) { auto i0 = opt_seam[j0]; + // recompute the energy on a square around (i0,j0) that was removed. for (auto i_offset = -max_step - 1; i_offset <= max_step + 1; i_offset++) { for (auto j_offset = -max_step - 1; j_offset <= max_step + 1; j_offset++) { @@ -317,6 +321,7 @@ void recompute_energy_along_seam( for (auto j0 = 0; j0 < dim_long; j0++) { auto i0 = opt_seam[j0]; + // recompute the energy on a square around (i0,j0) that was removed. for (auto i_offset = -max_step - 1; i_offset <= max_step + 1; i_offset++) { for (auto j_offset = -max_step - 1; j_offset <= max_step + 1; j_offset++) { @@ -327,24 +332,23 @@ void recompute_energy_along_seam( // if the pixel is to be removed, we keep the energy at 0. if (output_energy[width * y + x].first != 0) compute_energy_for_pixel(carved_img, newWidth, newHeight, x, y, - nbChannels, nbColorChannels, - output_energy[width * y + x].second); + nbChannels, nbColorChannels, + output_energy[width * y + x].second); } } } } } -/** Carves an image and its energy by one seam, and recomputes the energy. - Returns the optimal seam used */ template - std::vector carving_step(const std::vector source_img, std::vector source_energy, std::vector &output_img, std::vector &output_energy, int width, int height, int nbChannels, int nbColorChannels, bool vertical) { + /** Carves an image and its energy by one seam, and recomputes the energy. + Returns the optimal seam used */ std::vector opt_seam = optimal_seam(source_energy, width, height, vertical); @@ -360,18 +364,15 @@ std::vector carving_step(const std::vector source_img, return opt_seam; } - auto seam_carving(unsigned char *source, int width, int height, int nbChannels, int nbSeams, bool vertical, unsigned char *mask = nullptr) { - + /* Performs seam Carving */ int nbColorChannels = nbChannels > 3 ? 3 : nbChannels; int curWidth = width; int curHeight = height; - int dim_large = vertical ? width : height; int dim_long = vertical ? height : width; // dim_long=longueur des seam - std::vector source_img(width * height * nbChannels); // Contains at each step the carved image std::vector source_energy(width * height); @@ -389,11 +390,9 @@ auto seam_carving(unsigned char *source, int width, int height, int nbChannels, // Contains the initial energy, only for "test_energy" std::vector test_energy_output(width * height * nbChannels); // Final output for "test_energy" - for (auto i = 0; i < width * height * nbChannels; i++) { source_img[i] = source[i]; } - source_energy = energy_e1(source_img, width, height, nbChannels); if (mask) masked_energy = mask_energy(source_energy, width, height, mask); @@ -441,6 +440,7 @@ auto seam_carving(unsigned char *source, int width, int height, int nbChannels, auto seam_index = 0; while (seam_index++ < nbSeams || until_mask_removal) { + // Main loop of the program: we remove a seam until the end. std::vector opt_seam; if (mask) { opt_seam = carving_step(source_img, masked_energy, output_img, @@ -448,8 +448,8 @@ auto seam_carving(unsigned char *source, int width, int height, int nbChannels, nbChannels, nbColorChannels, vertical); if (until_mask_removal && - !does_seam_remove_mask(masked_energy, curWidth, curHeight, nbChannels, opt_seam, - vertical)) + !does_seam_remove_mask(masked_energy, curWidth, curHeight, nbChannels, + opt_seam, vertical)) break; } else { @@ -503,7 +503,6 @@ int main(int argc, char **argv) { std::string outputImage = "output.png"; int nbSeams = DEFAULT_SEAMS; bool vertical = false; - app.add_option("-s,--source", sourceImage, "Source image") ->required() ->check(CLI::ExistingFile); @@ -513,6 +512,10 @@ int main(int argc, char **argv) { app.add_option("-n,--nb-seams", nbSeams, "Number of seams") ->check(CLI::Number); + app.add_flag( + "-u,--until-mask-removal", until_mask_removal, + "Carve the image until there are no more red pixels in the mask"); + app.add_option("--max-step", max_step, "Max width of step to find a seam") ->check(CLI::Number); @@ -521,11 +524,9 @@ int main(int argc, char **argv) { app.add_flag("--silent", silent, "No verbose messages"); app.add_flag("--show-seams", show_seams, "Don't resize image, just try the specified energy function"); - app.add_option("-f,--function", function, - "The function to apply to compute the energy (default: gradient)"); - app.add_flag( - "-u,--until-mask-removal", until_mask_removal, - "Carve the image until there are no more red pixels in the mask"); + app.add_option( + "-f,--function", function, + "The function to apply to compute the energy (default: gradient)"); CLI11_PARSE(app, argc, argv); // Image loading @@ -561,7 +562,7 @@ int main(int argc, char **argv) { mask[i] = positive ? 2 : (negative ? 0 : 1); } //* "mask" has the same dimensions as source and one single channel - + //* The values are: //* . (2) we want to keep the pixel //* . (1) nothing in particular diff --git a/src/utils.cpp b/src/utils.cpp index 62a3f54..05ffc7f 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,6 +1,6 @@ #include -#include #include +#include #include #define STB_IMAGE_IMPLEMENTATION @@ -10,28 +10,29 @@ #include "utils.hpp" -bool nearly_equal(float a, float b) { - return std::nextafter(a, std::numeric_limits::lowest()) <= b && - std::nextafter(a, std::numeric_limits::max()) >= b; -} - -bool does_seam_remove_mask(std::vector> energy, int width, int height, int nbChannels, - std::vector opt_seam, bool vertical) - { +bool does_seam_remove_mask(std::vector> energy, int width, + int height, int nbChannels, + std::vector opt_seam, bool vertical) +/* returns true if and only if the given seam removes one pixel to remove given + by the mask*/ +{ int dim_large = vertical ? width : height; int dim_long = vertical ? height : width; - for (int i=0; i < dim_long; i++) { - if (energy[im_index(i, opt_seam[i])].first == 0) return true; + for (int i = 0; i < dim_long; i++) { + if (energy[im_index(i, opt_seam[i])].first == 0) + return true; } return false; } std::vector> mask_energy(std::vector energy, - int width, int height, unsigned char* mask) { - std::vector> output(width*height); + int width, int height, + unsigned char *mask) { + /* transform the computed energy to the energy associated to the mask*/ + std::vector> output(width * height); - for (auto i=0; i < width*height; i++) { + for (auto i = 0; i < width * height; i++) { if (mask[i] == 0) // We want to remove the pixel energy[i] = 0.; @@ -41,20 +42,23 @@ std::vector> mask_energy(std::vector energy, return output; } -std::pair operator+(std::pair& p1, std::pair& p2) { - return { - // If one of the two pixels is "protected" (ie 2), we want to prevent this line removing - // Else, we want to keep the information of "is there a pixel to remove in this seam" (ie 0) - (p1.first==2) || (p2.first==2) ? 2 : std::min(p1.first, p2.first), - p1.second+p2.second - }; +/* Here we overload the operators on pairs of int and float, to ease the + * readability of the code for removing a part of an image using a mask. */ +std::pair operator+(std::pair &p1, + std::pair &p2) { + return {// If one of the two pixels is "protected" (ie 2), we want to prevent + // this line removing + // Else, we want to keep the information of "is there a pixel to + // remove in this seam" (ie 0) + (p1.first == 2) || (p2.first == 2) ? 2 : std::min(p1.first, p2.first), + p1.second + p2.second}; } -void operator+=(std::pair& p1, std::pair& p2) { - p1 = p1+p2; +void operator+=(std::pair &p1, std::pair &p2) { + p1 = p1 + p2; } -bool operator==(std::pair& p1, std::pair& p2) { - return (p1.first==p2.first && p1.second == p2.second); +bool operator==(std::pair &p1, std::pair &p2) { + return (p1.first == p2.first && p1.second == p2.second); } std::string str_of_e(std::pair energy) { @@ -66,4 +70,4 @@ std::string str_of_e(float energy) { std::stringstream ss; ss << "(" << energy << ")"; return ss.str(); -} \ No newline at end of file +} diff --git a/src/utils.hpp b/src/utils.hpp index 588661a..267f976 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -4,59 +4,51 @@ // : dim_long, b : dim_large #define im_index(a, b) (vertical ? (width * a + b) : (width * b + a)) -// Check if two floats are nearly equal -bool nearly_equal(float a, float b); - -bool does_seam_remove_mask(std::vector> energy, int width, int height, int nbChannels, - std::vector opt_seam, bool vertical); +bool does_seam_remove_mask(std::vector> energy, int width, + int height, int nbChannels, + std::vector opt_seam, bool vertical); std::vector> mask_energy(std::vector energy, - int width, int height, unsigned char* mask); - + int width, int height, + unsigned char *mask); //* Operators overwrites -std::pair operator+(std::pair& p1, std::pair& p2); +std::pair operator+(std::pair &p1, + std::pair &p2); -void operator+=(std::pair& p1, std::pair& p2); +void operator+=(std::pair &p1, std::pair &p2); -bool operator==(std::pair& p1, std::pair& p2); - +bool operator==(std::pair &p1, std::pair &p2); -// Unfortunately, std::cout << (std::pair<>) does not work and can't be rewritten +// Unfortunately, std::cout << (std::pair<>) does not work and can't be +// rewritten std::string str_of_e(std::pair energy); std::string str_of_e(float energy); - namespace limits { - struct max_energy { - template operator T() { - return std::numeric_limits::max(); - } - operator std::pair() { - return {3, std::numeric_limits::max()}; - } - }; +struct max_energy { + template operator T() { return std::numeric_limits::max(); } + operator std::pair() { + return {3, std::numeric_limits::max()}; + } +}; - struct null_energy { - template operator T() { - return 0.; - } - operator std::pair() { - return {0, limits::null_energy()}; - } - }; -} +struct null_energy { + template operator T() { return 0.; } + operator std::pair() { return {0, limits::null_energy()}; } +}; +} // namespace limits template -void compute_seam_tot(std::vector opt_seam, std::vector energy, int width, int height, bool vertical) { +void compute_seam_tot(std::vector opt_seam, std::vector energy, + int width, int height, bool vertical) { int dim_long = vertical ? height : width; - T sum = limits::null_energy(); - for (int i=0; i < dim_long; i++) { + for (int i = 0; i < dim_long; i++) { sum += energy[im_index(i, opt_seam[i])]; // std::cout << str_of_e(sum); } std::cout << "Computed sum: " << str_of_e(sum) << std::endl; -} \ No newline at end of file +}