Compare commits

..

5 Commits

5 changed files with 217 additions and 203 deletions

View File

@ -2,7 +2,6 @@
## References:
- [Seam carving for content-aware image resizing](https://perso.crans.org/frenoy/matlab2012/seamcarving.pdf)
- [By-example Synthesis of Architectural Textures](https://inria.hal.science/inria-00547754v1/file/paper.pdf)
## Build
@ -17,5 +16,21 @@ cd ..
## Usage
```bash
build/seam-carving --source imgs/birds.jpg --output out.jpg -n 300
```
build/seam-carving ...
```
Mandatory arguments:
- `-s FILE`: source image
- `-o FILE`: output image
Options:
- `-m FILE`: the mask. RGB image, of the same size than the source, red pixel are those to be removed, the green ones must be kept.
- `-n NUMBER`: number of seams to compute or remove (default 0)
- `--show-seams`: do not carve the image, only outputs the energy and the relevant seams
- `-f FUNCTION NAME`: the energy function to use, see the [beginning of the source file to see the available functions](src/seam-carving.cpp)
- `-u`: when a mask is provided and no seam number is provided, remove seams until the zone to remove has disappeared.
## Generation of the images of the report
A `Makefile` is provided at the root of the repo, it can be used to generate the images that are shown in the report.

View File

@ -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);

View File

@ -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<float> energy_e1(std::vector<unsigned char> 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<float> energy(width * height);
@ -158,10 +157,10 @@ std::vector<float> energy_e1(std::vector<unsigned char> source, int width,
return energy;
}
/** Given the energy value, returns the optimal seam */
template <typename T>
std::vector<int> optimal_seam(std::vector<T> 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<T> dyn_energy(width * height);
@ -171,7 +170,7 @@ std::vector<int> optimal_seam(std::vector<T> 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<int> optimal_seam(std::vector<T> energy, int width, int height,
return result;
}
/** Carves an image by one seam. Returns the optimal seam used */
template <typename T>
void remove_seam(const std::vector<T> source, std::vector<T> &output, int width,
int height, int nbChannels, bool vertical,
const std::vector<int> 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<T> source, std::vector<T> &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<unsigned char> carved_img,
std::vector<float> &output_energy,
std::vector<int> opt_seam, int width,
@ -287,6 +290,7 @@ void recompute_energy_along_seam(std::vector<unsigned char> 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 <typename T>
std::vector<int> carving_step(const std::vector<unsigned char> source_img,
std::vector<T> source_energy,
std::vector<unsigned char> &output_img,
std::vector<T> &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<int> opt_seam =
optimal_seam(source_energy, width, height, vertical);
@ -360,18 +364,15 @@ std::vector<int> carving_step(const std::vector<unsigned char> 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<unsigned char> source_img(width * height * nbChannels);
// Contains at each step the carved image
std::vector<float> 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<unsigned char> 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<int> 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");
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

View File

@ -1,6 +1,6 @@
#include <iostream>
#include <sstream>
#include <random>
#include <sstream>
#include <vector>
#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<float>::lowest()) <= b &&
std::nextafter(a, std::numeric_limits<float>::max()) >= b;
}
bool does_seam_remove_mask(std::vector<std::pair<int, float>> energy, int width, int height, int nbChannels,
std::vector<int> opt_seam, bool vertical)
{
bool does_seam_remove_mask(std::vector<std::pair<int, float>> energy, int width,
int height, int nbChannels,
std::vector<int> 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<std::pair<int, float>> mask_energy(std::vector<float> energy,
int width, int height, unsigned char* mask) {
std::vector<std::pair<int, float>> output(width*height);
int width, int height,
unsigned char *mask) {
/* transform the computed energy to the energy associated to the mask*/
std::vector<std::pair<int, float>> 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<std::pair<int, float>> mask_energy(std::vector<float> energy,
return output;
}
std::pair<int, float> operator+(std::pair<int, float>& p1, std::pair<int, float>& 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<int, float> operator+(std::pair<int, float> &p1,
std::pair<int, float> &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<int, float>& p1, std::pair<int, float>& p2) {
p1 = p1+p2;
void operator+=(std::pair<int, float> &p1, std::pair<int, float> &p2) {
p1 = p1 + p2;
}
bool operator==(std::pair<int, float>& p1, std::pair<int, float>& p2) {
return (p1.first==p2.first && p1.second == p2.second);
bool operator==(std::pair<int, float> &p1, std::pair<int, float> &p2) {
return (p1.first == p2.first && p1.second == p2.second);
}
std::string str_of_e(std::pair<int, float> energy) {
@ -66,4 +70,4 @@ std::string str_of_e(float energy) {
std::stringstream ss;
ss << "(" << energy << ")";
return ss.str();
}
}

View File

@ -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<std::pair<int, float>> energy, int width, int height, int nbChannels,
std::vector<int> opt_seam, bool vertical);
bool does_seam_remove_mask(std::vector<std::pair<int, float>> energy, int width,
int height, int nbChannels,
std::vector<int> opt_seam, bool vertical);
std::vector<std::pair<int, float>> mask_energy(std::vector<float> energy,
int width, int height, unsigned char* mask);
int width, int height,
unsigned char *mask);
//* Operators overwrites
std::pair<int, float> operator+(std::pair<int, float>& p1, std::pair<int, float>& p2);
std::pair<int, float> operator+(std::pair<int, float> &p1,
std::pair<int, float> &p2);
void operator+=(std::pair<int, float>& p1, std::pair<int, float>& p2);
void operator+=(std::pair<int, float> &p1, std::pair<int, float> &p2);
bool operator==(std::pair<int, float>& p1, std::pair<int, float>& p2);
bool operator==(std::pair<int, float> &p1, std::pair<int, float> &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<int, float> energy);
std::string str_of_e(float energy);
namespace limits {
struct max_energy {
template<class T> operator T() {
return std::numeric_limits<T>::max();
}
operator std::pair<int, float>() {
return {3, std::numeric_limits<float>::max()};
}
};
struct max_energy {
template <class T> operator T() { return std::numeric_limits<T>::max(); }
operator std::pair<int, float>() {
return {3, std::numeric_limits<float>::max()};
}
};
struct null_energy {
template<class T> operator T() {
return 0.;
}
operator std::pair<int, float>() {
return {0, limits::null_energy()};
}
};
}
struct null_energy {
template <class T> operator T() { return 0.; }
operator std::pair<int, float>() { return {0, limits::null_energy()}; }
};
} // namespace limits
template <typename T>
void compute_seam_tot(std::vector<int> opt_seam, std::vector<T> energy, int width, int height, bool vertical) {
void compute_seam_tot(std::vector<int> opt_seam, std::vector<T> 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;
}
}