several improvements, including the ability to change the energy function

This commit is contained in:
François Colin de Verdière 2025-04-03 11:42:23 +02:00
parent 4ee23bb442
commit 3e96a3df05

View File

@ -39,8 +39,11 @@ void export_image(const char *filename, const void *data, int width, int height,
} }
} }
std::string function = "grad";
#define compute_energy_for_pixel(source, width, height, i, j, nbChannels, \ #define compute_energy_for_pixel(source, width, height, i, j, nbChannels, \
nbColorChannels, energy) \ nbColorChannels, energy) \
/* computes the energy at pixel i,j, i.e. energy[width*j+i]*/ \
auto indexPixel = (nbChannels) * (width * (j) + (i)); \ auto indexPixel = (nbChannels) * (width * (j) + (i)); \
auto indexPixel_up = \ auto indexPixel_up = \
((j) - 1 > 0) ? (nbChannels) * (width * ((j) - 1) + (i)) : indexPixel; \ ((j) - 1 > 0) ? (nbChannels) * (width * ((j) - 1) + (i)) : indexPixel; \
@ -53,29 +56,75 @@ void export_image(const char *filename, const void *data, int width, int height,
? (nbChannels) * (width * (j) + ((i) + 1)) \ ? (nbChannels) * (width * (j) + ((i) + 1)) \
: indexPixel; \ : indexPixel; \
energy[width * j + i] = 0; \ energy[width * j + i] = 0; \
for (auto ch = 0; ch < (nbColorChannels); ch++) { \ if (function == "gradnorm") { \
energy[(width) * (j) + (i)] += \ for (auto ch = 0; ch < (nbColorChannels); ch++) { \
(fabs((float)source[indexPixel_up + ch] - source[indexPixel + ch]) + \ energy[(width) * (j) + (i)] += \
fabs((float)source[indexPixel_down + ch] - source[indexPixel + ch]) + \ (std::pow(fabs((float)source[indexPixel_up + ch] - \
fabs((float)source[indexPixel_left + ch] - source[indexPixel + ch]) + \ source[indexPixel + ch]), \
fabs((float)source[indexPixel_right + ch] - \ 2) + \
source[indexPixel + ch])); \ \
} \ 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 == "grad") { \
for (auto ch = 0; ch < (nbColorChannels); ch++) { \
energy[(width) * (j) + (i)] += \
((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 == "gradnorminf") { \
for (auto ch = 0; ch < (nbColorChannels); ch++) { \
energy[(width) * (j) + (i)] += \
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 { \
std::cerr << "function " << function << " not available" << std::endl; \
exit(1); \
};
// Le alpha n'est pas pris en compte dans l'énergie // 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 // Here, we use this /ugly/ macro to avoid defining a function that would be way
// longer... // slower...
/** e_1 energy, energy is always normalized between 0 and 1 */ /** e_1 energy*/
std::vector<float> energy_e1(std::vector<unsigned char> source, int width, std::vector<float> energy_e1(std::vector<unsigned char> source, int width,
int height, int nbChannels) { int height, int nbChannels) {
int nbColorChannels = int nbColorChannels =
nbChannels > 3 ? 3 : nbChannels; // nombre de canaux, excepté le alpha nbChannels > 3 ? 3 : nbChannels; // nombre de canaux, excepté le alpha
std::vector<float> energy(width * height); std::vector<float> energy(width * height);
float max_energy = 0; float max_energy = 0;
for (auto i = 0; i < width; i++) { for (auto i = 0; i < width; i++) {
for (auto j = 0; j < height; j++) { for (auto j = 0; j < height; j++) {
compute_energy_for_pixel(source, width, height, i, j, nbChannels, compute_energy_for_pixel(source, width, height, i, j, nbChannels,
nbColorChannels, energy); nbColorChannels, energy);
energy[width * j + i] = (energy[width * j + i]);
} }
} }
return energy; return energy;
@ -190,23 +239,11 @@ void remove_seam(const std::vector<T> source, std::vector<T> &output, int width,
} }
} }
} }
void update_energy_along_seam(std::vector<int> opt_seam,
std::vector<int> carving_step(const std::vector<unsigned char> source_img, std::vector<unsigned char> output_img,
std::vector<float> source_energy, std::vector<float> &output_energy, int height,
std::vector<unsigned char> &output_img, int width, int nbChannels, bool vertical,
std::vector<float> &output_energy, int width, int nbColorChannels) {
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);
remove_seam(source_img, output_img, width, height, nbChannels, vertical,
opt_seam);
remove_seam(source_energy, output_energy, width, height, 1, vertical,
opt_seam);
// Recompute the energy along the seam // Recompute the energy along the seam
if (energy_recompute_all) { if (energy_recompute_all) {
std::vector<float> energy = std::vector<float> energy =
@ -214,9 +251,7 @@ std::vector<int> carving_step(const std::vector<unsigned char> source_img,
vertical ? height : height - 1, nbChannels); vertical ? height : height - 1, nbChannels);
std::copy(energy.begin(), energy.end(), output_energy.begin()); std::copy(energy.begin(), energy.end(), output_energy.begin());
} else { } else {
if (vertical) { // Vertical seam
// ASSUME WE ARE DOING A VERTICAL SEAM
if (vertical) {
for (auto j = 0; j < height; j++) { for (auto j = 0; j < height; j++) {
for (auto i = -1; i < 2; i++) { for (auto i = -1; i < 2; i++) {
if ((0 < (opt_seam[j] + i)) && ((opt_seam[j] + i) < width - 1)) { if ((0 < (opt_seam[j] + i)) && ((opt_seam[j] + i) < width - 1)) {
@ -238,11 +273,28 @@ std::vector<int> carving_step(const std::vector<unsigned char> source_img,
} }
} }
} }
}
std::vector<int> carving_step(const std::vector<unsigned char> source_img,
std::vector<float> source_energy,
std::vector<unsigned char> &output_img,
std::vector<float> &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);
remove_seam(source_img, output_img, width, height, nbChannels, vertical,
opt_seam);
remove_seam(source_energy, output_energy, width, height, 1, vertical,
opt_seam);
update_energy_along_seam(opt_seam, output_img, output_energy, height, width,
nbChannels, vertical, nbColorChannels);
return opt_seam; return opt_seam;
} }
void seam_carving(unsigned char *source, int width, int height, int nbChannels, auto seam_carving(unsigned char *source, int width, int height, int nbChannels,
const char *out_filename, int nbSeams, bool vertical, unsigned char* mask=nullptr) { int nbSeams, bool vertical, unsigned char *mask = nullptr) {
int nbColorChannels = nbChannels > 3 ? 3 : nbChannels; int nbColorChannels = nbChannels > 3 ? 3 : nbChannels;
int curWidth = width; int curWidth = width;
int curHeight = height; int curHeight = height;
@ -337,11 +389,11 @@ void seam_carving(unsigned char *source, int width, int height, int nbChannels,
} }
if (test_energy) { if (test_energy) {
export_image(out_filename, test_energy_output.data(), width, height,
nbChannels); return std::make_tuple(test_energy_output, width, height, nbChannels);
} else { } else {
export_image(out_filename, source_img.data(), curWidth, curHeight, return std::make_tuple(source_img, curWidth, curHeight, nbChannels);
nbChannels);
} }
} }
@ -372,6 +424,8 @@ int main(int argc, char **argv) {
"Don't resize image, just try the specified energy function"); "Don't resize image, just try the specified energy function");
app.add_flag("--energy-recompute-all", energy_recompute_all, app.add_flag("--energy-recompute-all", energy_recompute_all,
"recompute the whole energy at each step"); "recompute the whole energy at each step");
app.add_option("-f,--function", function,
"The function to apply to compute the energy");
CLI11_PARSE(app, argc, argv); CLI11_PARSE(app, argc, argv);
// Image loading // Image loading
@ -379,38 +433,48 @@ int main(int argc, char **argv) {
unsigned char *source = unsigned char *source =
stbi_load(sourceImage.c_str(), &width, &height, &nbChannels, 0); stbi_load(sourceImage.c_str(), &width, &height, &nbChannels, 0);
unsigned char* mask = nullptr; unsigned char *mask = nullptr;
if (!maskImage.empty()) { if (!maskImage.empty()) {
int maskWidth, maskHeight, maskChannels; int maskWidth, maskHeight, maskChannels;
mask = mask =
stbi_load(maskImage.c_str(), &maskWidth, &maskHeight, &maskChannels, 0); stbi_load(maskImage.c_str(), &maskWidth, &maskHeight, &maskChannels, 0);
if (maskWidth != width || maskHeight != height) { if (maskWidth != width || maskHeight != height) {
std::cerr << maskImage << " and " << sourceImage std::cerr << maskImage << " and " << sourceImage
<< " differ in dimension. Please provide a valid mask." << " differ in dimension. Please provide a valid mask."
<< std::endl; << std::endl;
exit(1); exit(1);
} }
if (maskChannels < 3) { if (maskChannels < 3) {
std::cerr << maskImage << " needs to be RGB." << std::endl; std::cerr << maskImage << " needs to be RGB." << std::endl;
exit(1); exit(1);
} }
unsigned char r, g, b; unsigned char r, g, b;
for (auto i=0; i < width*height; i++) { for (auto i = 0; i < width * height; i++) {
r = mask[maskChannels*i]; r = mask[maskChannels * i];
g = mask[maskChannels*i]; g = mask[maskChannels * i];
b = mask[maskChannels*i]; b = mask[maskChannels * i];
mask[2*i] = (r == 0) && (g == 255) && (b == 0); mask[2 * i] = (r == 0) && (g == 255) && (b == 0);
mask[2*i+1] = (r == 255) && (g == 0) && (b == 0); mask[2 * i + 1] = (r == 255) && (g == 0) && (b == 0);
} }
//* From now on, mask has the same dimensions as source and exactly 2 channels //* From now on, mask has the same dimensions as source and exactly 2
//* The first channel is positive, the second one negative. // channels
//* The first channel is positive, the second one negative.
} }
nbSeams = std::min(nbSeams, vertical ? width-1 : height-1); // We want to keep at least one row or column nbSeams = std::min(
nbSeams, vertical
? width - 1
: height - 1); // We want to keep at least one row or column
seam_carving(source, width, height, nbChannels, outputImage.c_str(), auto result = seam_carving(source, width, height, nbChannels, nbSeams,
nbSeams, vertical, mask=mask); vertical, mask = mask);
auto content = std::get<0>(result);
int width_output = std::get<1>(result);
int height_output = std::get<2>(result);
int nbChannels_output = std::get<3>(result);
export_image(outputImage.c_str(), content.data(), width_output, height_output,
nbChannels_output);
stbi_image_free(source); stbi_image_free(source);
exit(0); exit(0);