diff --git a/src/seam-carving.cpp b/src/seam-carving.cpp index e08fce6..7e13418 100644 --- a/src/seam-carving.cpp +++ b/src/seam-carving.cpp @@ -1,103 +1,112 @@ #include -#include #include +#include #include -//Command-line parsing +// Command-line parsing #include -//Image filtering and I/O +// Image filtering and I/O #define STB_IMAGE_IMPLEMENTATION #include #define STB_IMAGE_WRITE_IMPLEMENTATION -#include #include +#include -//Global flag to silent verbose messages +// Global flag to silent verbose messages bool silent; bool test_energy; +bool energy_recompute_all = false; - -// Get index for any table indexed by [width*(i : height) + (j : width)], but a : dim_long, b : dim_large -#define im_index(a, b) \ - (vertical ? (width*a + b) : (width*b + a)) - +// Get index for any table indexed by [width*(i : height) + (j : width)], but a +// : dim_long, b : dim_large +#define im_index(a, b) (vertical ? (width * a + b) : (width * b + a)) bool nearly_equal(float a, float b) { - return std::nextafter(a, std::numeric_limits::lowest()) <= b - && std::nextafter(a, std::numeric_limits::max()) >= b; + return std::nextafter(a, std::numeric_limits::lowest()) <= b && + std::nextafter(a, std::numeric_limits::max()) >= b; } -void export_image(const char* filename, const void* data, int width, int height, int nbChannels) { - if (!silent) std::cout << "Exporting to \"" << filename << "\".." << std::endl; - int errcode = stbi_write_png(filename, width, height, nbChannels, data, nbChannels*width); +void export_image(const char *filename, const void *data, int width, int height, + int nbChannels) { + if (!silent) + std::cout << "Exporting to \"" << filename << "\".." << std::endl; + int errcode = stbi_write_png(filename, width, height, nbChannels, data, + nbChannels * width); if (!errcode) { std::cerr << "Error while exporting the resulting image." << std::endl; exit(errcode); } } -/** e_1 energy, energy is always normalized between 0 and 1 */ -std::vector energy_e1(std::vector source, int width, int height, int nbChannels) { - int nbColorChannels = nbChannels > 3 ? 3 : nbChannels; // nombre de canaux, excepté le alpha - std::vector energy(width*height); +#define compute_energy_for_pixel(source, width, height, i, j, nbChannels, \ + nbColorChannels, energy) \ + 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; \ + energy[width * j + i] = 0; \ + 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])); \ + } \ +// 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 +// longer... +/** e_1 energy, energy is always normalized between 0 and 1 */ +std::vector energy_e1(std::vector source, int width, + int height, int nbChannels) { + int nbColorChannels = + nbChannels > 3 ? 3 : nbChannels; // nombre de canaux, excepté le alpha + std::vector energy(width * height); float max_energy = 0; - for(auto i=0 ; i < width; ++i) { - for(auto j=0; j < height; ++j) { - 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; - - energy[width*j+i] = 0; - for (auto ch=0; ch < nbColorChannels; ch++) { // Le alpha n'est pas pris en compte dans l'énergie - 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]) - ); - } - max_energy = std::max(max_energy, energy[width*j+i]); + for (auto i = 0; i < width; i++) { + for (auto j = 0; j < height; j++) { + compute_energy_for_pixel(source, width, height, i, j, nbChannels, + nbColorChannels, energy); } } - - if (max_energy == 0) { return energy; } - for (auto k=0; k < width*height; k++) { - energy[k] = energy[k]/max_energy; - } - return energy; } -/** Given the energy value, returns the optimal seam */ -std::vector optimal_seam(std::vector energy, int width, int height, bool vertical, int max_step=1) { +std::vector optimal_seam(std::vector energy, int width, int height, + bool vertical, int max_step = 1) { + /** Given the energy value, returns the optimal seam */ + // dyn_energy is indexed by [dim_large*(i : dim_long) + (j : dim_large)] - std::vector dyn_energy(width*height); + std::vector dyn_energy(width * height); int dim_large = vertical ? width : height; - int dim_long = vertical ? height : width; // Number of elements in the seam + int dim_long = vertical ? height : width; // Number of elements in the seam //* Find an end of the minimal connected vertical/horizontal seam - for (auto i=0; i < dim_large; i++) { + for (auto i = 0; i < dim_large; i++) { dyn_energy[i] = energy[i]; } - for (auto i=1; i < dim_long; i++) { // Propagate dyn_energy - for (auto j=0; j < dim_large; j++) { - dyn_energy[dim_large*i+j] = __FLT_MAX__; - + for (auto i = 1; i < dim_long; i++) { // Propagate dyn_energy + for (auto j = 0; j < dim_large; j++) { + dyn_energy[dim_large * i + j] = __FLT_MAX__; int lower_bound = std::max(j - max_step, 0); - int upper_bound = std::min(j+max_step, dim_large-1); + int upper_bound = std::min(j + max_step, dim_large - 1); - for (auto k=lower_bound; k <= upper_bound; k++) { // Compute energy based on predecessors - dyn_energy[dim_large*i+j] = std::min( - dyn_energy[dim_large*i+j], - dyn_energy[dim_large*(i-1)+k] - ); + for (auto k = lower_bound; k <= upper_bound; + k++) { // Compute energy based on predecessors + dyn_energy[dim_large * i + j] = std::min( + dyn_energy[dim_large * i + j], dyn_energy[dim_large * (i - 1) + k]); } - dyn_energy[dim_large*i+j] += energy[im_index(i, j)]; + dyn_energy[dim_large * i + j] += energy[im_index(i, j)]; } } @@ -105,180 +114,271 @@ std::vector optimal_seam(std::vector energy, int width, int height, // Find the seam end int min_idx = -1; float min_val = __FLT_MAX__; - for (auto j=0; j < dim_large; j++) { - if (min_val > dyn_energy[dim_large*(dim_long-1) + j]) { + for (auto j = 0; j < dim_large; j++) { + if (min_val > dyn_energy[dim_large * (dim_long - 1) + j]) { min_idx = j; - min_val = dyn_energy[dim_large*(dim_long-1) + j]; + min_val = dyn_energy[dim_large * (dim_long - 1) + j]; } } - result[dim_long-1] = min_idx; + result[dim_long - 1] = min_idx; //* Backtracking to find the path - for (auto i=dim_long-1; i > 0; i--) { - // We want to find either (bot_l, bot_c, bot_r) with dyn_energy[.] = min_val - energy[cur] - - //Idea : float next_energy = min_val - energy[width*i + min_idx]; - //! With floats, we don't always have x + y - y == x, so we check is x+y == x+y - // This define is a bit ugly but 200x faster than using a lambda function - #define is_next_idx(idx) \ - (dyn_energy[(i-1)*dim_large + idx]+energy[im_index(i, min_idx)] == min_val) + for (auto i = dim_long - 1; i > 0; i--) { +// We want to find either (bot_l, bot_c, bot_r) with dyn_energy[.] = min_val - +// energy[cur] - // This is not the nicest way to do thiss but we want to check in priority at the center to have straight seams +// Idea : float next_energy = min_val - energy[width*i + min_idx]; +//! With floats, we don't always have x + y - y == x, so we check is x+y == x+y +// This define is a bit ugly but 200x faster than using a lambda function +#define is_next_idx(idx) \ + (dyn_energy[(i - 1) * dim_large + idx] + energy[im_index(i, min_idx)] == \ + min_val) + + // This is not the nicest way to do thiss but we want to check in priority + // at the center to have straight seams bool found = false; if (is_next_idx(min_idx)) { - min_val = dyn_energy[(i-1)*dim_large+min_idx]; + + min_val = dyn_energy[(i - 1) * dim_large + min_idx]; found = true; } - for (auto k=1; !found && k <= max_step; k++) { - if (min_idx+k < dim_large && is_next_idx(min_idx+k)) { - min_val = dyn_energy[(i-1)*dim_large+min_idx+k]; - min_idx = min_idx+k; - + for (auto k = 1; !found && k <= max_step; k++) { + if (min_idx + k < dim_large && is_next_idx(min_idx + k)) { + min_val = dyn_energy[(i - 1) * dim_large + min_idx + k]; + min_idx = min_idx + k; + found = true; - } else if (min_idx-k >= 0 && is_next_idx(min_idx-k)) { - min_val = dyn_energy[(i-1)*dim_large+min_idx-k]; - min_idx = min_idx-k; - + } else if (min_idx - k >= 0 && is_next_idx(min_idx - k)) { + min_val = dyn_energy[(i - 1) * dim_large + min_idx - k]; + min_idx = min_idx - k; + found = true; } } if (!found) { + std::cerr << "Unable to backtrack path !" << std::endl; exit(1); } - result[i-1] = min_idx; + result[i - 1] = min_idx; } return result; } - /** Carves an image by one seam. Returns the optimal seam used */ -std::vector carving_step(const std::vector source, std::vector &output, int width, int height, int nbChannels, bool vertical, int max_step=1) { - std::vector energy = energy_e1(source, width, height, nbChannels); - std::vector opt_seam = optimal_seam(energy, width, height, vertical, max_step=max_step); - std::vector blacklist(width*height); +template +void remove_seam(const std::vector source, std::vector &output, int width, + int height, int nbChannels, bool vertical, + const std::vector seam) { + // 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; - int dim_long = vertical ? height : width; - - for (auto k=0; k < width*height; k++) { blacklist[k] = false; } - for (auto i=0; i < dim_long; i++) { - int index = vertical ? opt_seam[i]+i*width : i+width*opt_seam[i]; - blacklist[index] = true; - } - - for (auto i=0; i < dim_long; i++) { + int dim_long = vertical ? height : width; + for (auto i = 0; i < dim_long; i++) { int cur_j = 0; - for (auto j=0; cur_j < dim_large-1 && j < dim_large; j++) { - if (!blacklist[im_index(i, j)]) { - int out_pixelIndex = nbChannels*(vertical ? ((width-1)*i + cur_j) : (width*cur_j + i)); - int src_pixelIndex = nbChannels*im_index(i, j); - - for (auto ch=0; ch < nbChannels; ch++) - output[out_pixelIndex+ch] = source[src_pixelIndex+ch]; + for (auto j = 0; cur_j < dim_large - 1 && j < dim_large; j++) { + if (seam[i] != j) { + int out_pixelIndex = nbChannels * (vertical ? ((width - 1) * i + cur_j) + : (width * cur_j + i)); + int src_pixelIndex = nbChannels * im_index(i, j); + for (auto ch = 0; ch < nbChannels; ch++) + output[out_pixelIndex + ch] = source[src_pixelIndex + ch]; cur_j++; } } } +} + +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); + 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 + if (energy_recompute_all) { + std::vector energy = + energy_e1(output_img, vertical ? width - 1 : width, + vertical ? height : height - 1, nbChannels); + std::copy(energy.begin(), energy.end(), output_energy.begin()); + } else { + + // ASSUME WE ARE DOING A VERTICAL SEAM + if (vertical) { + for (auto j = 0; j < height; j++) { + for (auto i = -1; i < 2; i++) { + if ((0 < (opt_seam[j] + i)) && ((opt_seam[j] + i) < width - 1)) { + compute_energy_for_pixel(output_img, (width - 1), height, + (opt_seam[j] + i), j, nbChannels, + nbColorChannels, output_energy); + } + } + } + } else { + for (auto i = 0; i < width; i++) { + for (auto j = -1; j < 2; j++) { + if ((0 < (opt_seam[i] + j)) && ((opt_seam[i] + j) < height - 1)) { + compute_energy_for_pixel(output_img, width, height - 1, i, + (opt_seam[i] + j), nbChannels, + nbColorChannels, output_energy); + } + } + } + } + } return opt_seam; } - -void seam_carving(unsigned char* source, int width, int height, int nbChannels, const char* out_filename, int nbSeams, bool vertical, bool test_energy=false, int max_step=1) { +void seam_carving(unsigned char *source, int width, int height, int nbChannels, + const char *out_filename, int nbSeams, bool vertical, + bool test_energy = false, int max_step = 1) { int nbColorChannels = nbChannels > 3 ? 3 : nbChannels; int curWidth = width; int curHeight = height; int dim_large = vertical ? width : height; - int dim_long = vertical ? height : width; + int dim_long = vertical ? height : width; + // dim_long=longueur des seam - std::vector carve_output(width*height*nbChannels); // Receives at each step the newly carved image - std::vector source_img(width*height*nbChannels); // Contains at each step the carved image - std::vector complete_blacklist(width*height); // Contains all removed pixels, for "test_energy" - std::vector ini_energy; // Contains the initial energy, only for "test_energy" - std::vector test_energy_output(width*height*nbChannels); // Final output for "test_energy" + std::vector source_img(width * height * nbChannels); + // Contains at each step the carved image + std::vector source_energy(width * height); + // Contains at each step the carved energy + std::vector output_img(width * height * nbChannels); + // Receives at each step the newly carved image + std::vector output_energy(width * height); + // Contains at each step the carved energy + std::vector complete_blacklist(width * height); + // Contains all removed pixels, for "test_energy" + std::vector ini_energy; + // 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]; + } - for (auto i=0; i < width*height*nbChannels; i++) { source_img[i] = source[i]; } + source_energy = energy_e1(source_img, width, height, nbChannels); if (test_energy) { ini_energy = energy_e1(source_img, width, height, nbChannels); - for (auto k=0; k < width*height; k++) { complete_blacklist[k] = false; } + for (auto k = 0; k < width * height; k++) { + complete_blacklist[k] = false; + } //* Prepare final output - for (auto k=0; k < width*height; k++) { - //for (auto i=0; i < nbColorChannels; i++) //* Uncomment if you prefer to see darkened source image - // output[nbChannels*k+i] = source_img[nbChannels*k+i]/nbChannels; - for (auto i=0; i < nbColorChannels; i++) - test_energy_output[nbChannels*k+i] = ini_energy[k]*255; - if (nbChannels==4) - test_energy_output[nbChannels*k+3] = source_img[nbChannels*k+3]; + float max_energy = __FLT_MAX__; + for (auto k = 0; k < width * height; k++) { + max_energy = fmax(max_energy, ini_energy[k]); + } + if (max_energy != 0) { + for (auto k = 0; k < width * height; k++) { + ini_energy[k] /= max_energy; + } + } + for (auto k = 0; k < width * height; k++) { + // for (auto i=0; i < nbColorChannels; i++) //* Uncomment if you prefer to + // see darkened source image + // output[nbChannels*k+i] = source_img[nbChannels*k+i]/nbChannels; + + for (auto i = 0; i < nbColorChannels; i++) + test_energy_output[nbChannels * k + i] = ini_energy[k] * 255; + + if (nbChannels == 4) + test_energy_output[nbChannels * k + 3] = source_img[nbChannels * k + 3]; } } SimpleProgressBar::ProgressBar bar(nbSeams); - for (auto seam=0; seam < nbSeams; seam++) { - std::vector opt_seam = carving_step(source_img, carve_output, curWidth, curHeight, nbChannels, vertical, max_step=max_step); - std::copy(carve_output.begin(), carve_output.end(), source_img.begin()); - if (vertical) // We just reduced the dimension - curWidth--; - else - curHeight--; + for (auto seam_index = 0; seam_index < nbSeams; seam_index++) { + std::vector opt_seam = carving_step( + source_img, source_energy, output_img, output_energy, curWidth, + curHeight, nbChannels, nbColorChannels, vertical); + std::copy(output_img.begin(), output_img.end(), source_img.begin()); + std::copy(output_energy.begin(), output_energy.end(), + source_energy.begin()); + + vertical ? curWidth-- : curHeight--; // We just reduced the dimension if (test_energy) { // Update blacklist - for (auto i=0; i < dim_long; i++) { - int j, cur_j = 0; // cur_j is the index relative to the current carved image. j is absolute in the source image - for (j=0; j < dim_large && (cur_j < opt_seam[i] || complete_blacklist[im_index(i, j)]); j++) { - if (!complete_blacklist[im_index(i, j)]) { cur_j++; } + for (auto i = 0; i < dim_long; i++) { + int j, cur_j = 0; // cur_j is the index relative to the current carved + // image. j is absolute in the source image + for (j = 0; j < dim_large && + (cur_j < opt_seam[i] || complete_blacklist[im_index(i, j)]); + j++) { + if (!complete_blacklist[im_index(i, j)]) { + cur_j++; + } } - assert(cur_j == opt_seam[i]); // Else, j == width and cur_j is not in the source image.. - + assert(cur_j == opt_seam[i]); // Else, j == width and cur_j is not in + // the source image.. complete_blacklist[im_index(i, j)] = true; - test_energy_output[nbChannels*im_index(i, j)] = 255; // Set carved pixel to red + test_energy_output[nbChannels * im_index(i, j)] = + 255; // Set carved pixel to red } } bar.increment(); bar.print(); } - std::cout << std::endl; if (test_energy) { - export_image(out_filename, test_energy_output.data(), width, height, nbChannels); + export_image(out_filename, test_energy_output.data(), width, height, + nbChannels); } else { - export_image(out_filename, source_img.data(), curWidth, curHeight, nbChannels); + export_image(out_filename, source_img.data(), curWidth, curHeight, + nbChannels); } } - int main(int argc, char **argv) { CLI::App app{"seam-carving"}; std::string sourceImage; - app.add_option("-s,--source", sourceImage, "Source image")->required()->check(CLI::ExistingFile);; - std::string outputImage= "output.png"; + app.add_option("-s,--source", sourceImage, "Source image") + ->required() + ->check(CLI::ExistingFile); + ; + std::string outputImage = "output.png"; app.add_option("-o,--output", outputImage, "Output image")->required(); int nbSeams = 1; app.add_option("-n,--nb-seams", nbSeams, "Number of seams"); int max_step = 1; app.add_option("--max-step", max_step, "Max width of step to find a seam"); bool vertical = false; - app.add_flag("--vertical", vertical, "Vertical carving"); + app.add_flag("--vertical", vertical, + "Vertical carving (remove vertical seams)"); silent = false; app.add_flag("--silent", silent, "No verbose messages"); test_energy = false; - app.add_flag("--test-energy", test_energy, "Don't resize image, just try the specified energy function"); + app.add_flag("--test-energy", test_energy, + "Don't resize image, just try the specified energy function"); + app.add_flag("--energy-recompute-all", energy_recompute_all, + "recompute the whole energy at each step"); CLI11_PARSE(app, argc, argv); - //Image loading + // Image loading int width, height, nbChannels; - unsigned char *source = stbi_load(sourceImage.c_str(), &width, &height, &nbChannels, 0); + unsigned char *source = + stbi_load(sourceImage.c_str(), &width, &height, &nbChannels, 0); nbSeams = std::min(nbSeams, width); - //std::cout << "channels: " << nbChannels << std::endl; - - seam_carving(source, width, height, nbChannels, outputImage.c_str(), nbSeams, vertical, test_energy=test_energy, max_step=max_step); + seam_carving(source, width, height, nbChannels, outputImage.c_str(), nbSeams, + vertical, test_energy = test_energy, max_step = max_step); stbi_image_free(source); exit(0); -} \ No newline at end of file +}