#include #include #include #include #include // Command-line parsing #include // Image filtering and I/O #include "image.h" #include "utils.hpp" #include #define DEFAULT_SEAMS 1 // Global flags bool silent = false; bool show_seams = false; bool until_mask_removal = false; int max_step = 1; std::string function = "grad"; 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); } } #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) { 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++) { compute_energy_for_pixel(source, width, height, i, j, nbChannels, nbColorChannels, energy[width * j + i]); } } return energy; } /** Given the energy value, returns the optimal seam */ template std::vector optimal_seam(std::vector energy, int width, int height, bool vertical) { // dyn_energy is indexed by [dim_large*(i : dim_long) + (j : dim_large)] std::vector dyn_energy(width * height); int dim_large = vertical ? width : height; 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++) { dyn_energy[i] = energy[vertical ? i : i*width]; } 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] = limits::max_energy(); int lower_bound = std::max(j - max_step, 0); int upper_bound = std::min(j + max_step, dim_large - 1); // Compute energy based on predecessors for (auto k = lower_bound; k <= upper_bound; k++) { if (dyn_energy[dim_large * (i - 1) + k] < dyn_energy[dim_large * i + j]) { dyn_energy[dim_large * i + j] = dyn_energy[dim_large * (i - 1) + k]; } } dyn_energy[dim_large * i + j] += energy[im_index(i, j)]; } } std::vector result(dim_long); // Find the seam end int min_idx = -1; T min_val = limits::max_energy(); for (auto j = 0; j < dim_large; j++) { if (dyn_energy[dim_large * (dim_long - 1) + j] < min_val) { min_idx = j; min_val = dyn_energy[dim_large * (dim_long - 1) + j]; } } 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) // This is not the nicest way to do this 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]; 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; 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; found = true; } } if (!found) { std::cerr << "Unable to backtrack path !" << std::endl; exit(1); } result[i - 1] = min_idx; } 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) { // 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 i = 0; i < dim_long; i++) { int cur_j = 0; 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++; } } } } // 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, int height, int nbChannels, int nbColorChannels, bool vertical) { int dim_large = vertical ? width : height; int dim_long = vertical ? height : width; int newWidth = vertical ? width - 1 : width; int newHeight = vertical ? height : height - 1; for (auto j0 = 0; j0 < dim_long; j0++) { auto i0 = opt_seam[j0]; 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++) { int x = vertical ? (i0 + i_offset) : j0 + j_offset; int y = vertical ? j0 + j_offset : (i0 + i_offset); if (((0 <= (i0 + i_offset)) && ((i0 + i_offset) < dim_large - 1)) && (((0 <= j0 + j_offset) && (j0 + j_offset < dim_long)))) { compute_energy_for_pixel(carved_img, newWidth, newHeight, x, y, nbChannels, nbColorChannels, output_energy[width * y + x]); } } } } } void recompute_energy_along_seam( std::vector carved_img, std::vector> &output_energy, std::vector opt_seam, int width, int height, int nbChannels, int nbColorChannels, bool vertical) { int dim_large = vertical ? width : height; int dim_long = vertical ? height : width; int newWidth = vertical ? width - 1 : width; int newHeight = vertical ? height : height - 1; for (auto j0 = 0; j0 < dim_long; j0++) { auto i0 = opt_seam[j0]; 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++) { int x = vertical ? (i0 + i_offset) : j0 + j_offset; int y = vertical ? j0 + j_offset : (i0 + i_offset); if (((0 < (i0 + i_offset)) && ((i0 + i_offset) < dim_large - 1)) && (((0 < j0 + j_offset) && (j0 + j_offset < dim_long)))) { // 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); } } } } } /** 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) { 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, we need a separate function for // templating recompute_energy_along_seam(source_img, output_energy, opt_seam, width, height, nbChannels, nbColorChannels, vertical); return opt_seam; } auto seam_carving(unsigned char *source, int width, int height, int nbChannels, int nbSeams, bool vertical, unsigned char *mask = nullptr) { 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); // Contains at each step the carved energy std::vector> masked_energy; std::vector> output_masked_energy(width * height); // Source energy with (-1, 0, 1) on first element according to mask value 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]; } source_energy = energy_e1(source_img, width, height, nbChannels); if (mask) masked_energy = mask_energy(source_energy, width, height, mask); if (show_seams) { ini_energy = energy_e1(source_img, width, height, nbChannels); for (auto k = 0; k < width * height; k++) { complete_blacklist[k] = false; } //* Prepare final output float max_energy = __FLT_MIN__; for (auto k = 0; k < width * height; k++) { max_energy = std::max(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++) { //* Uncomment if you prefer to see darkened source image // for (auto i=0; i < nbColorChannels; i++) // output[nbChannels*k+i] = source_img[nbChannels*k+i]/nbChannels; for (auto ch = 0; ch < nbColorChannels; ch++) { test_energy_output[nbChannels * k + ch] = ini_energy[k] * 255; } if (nbChannels == 4) test_energy_output[nbChannels * k + 3] = source_img[nbChannels * k + 3]; if (mask) { if (mask[k] == 2) // Green test_energy_output[nbChannels * k + 1] = 125; else if (mask[k] == 0) { // Red test_energy_output[nbChannels * k] = 125; } } } } SimpleProgressBar::ProgressBar bar(until_mask_removal ? dim_large : nbSeams); bar.print(); auto seam_index = 0; while (seam_index++ < nbSeams || until_mask_removal) { std::vector opt_seam; if (mask) { opt_seam = carving_step(source_img, masked_energy, output_img, output_masked_energy, curWidth, curHeight, nbChannels, nbColorChannels, vertical); if (until_mask_removal && !does_seam_remove_mask(masked_energy, curWidth, curHeight, nbChannels, opt_seam, vertical)) break; } else { 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()); if (mask) std::copy(output_masked_energy.begin(), output_masked_energy.end(), masked_energy.begin()); vertical ? curWidth-- : curHeight--; // We just reduced the dimension if (show_seams) { // 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++; } } 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 } } bar.increment(); bar.print(); } std::cout << std::endl; // Add newline after ProgressBar if (show_seams) { return std::make_tuple(test_energy_output, width, height, nbChannels); } else { return std::make_tuple(source_img, curWidth, curHeight, nbChannels); } } int main(int argc, char **argv) { CLI::App app{"seam-carving"}; std::string maskImage; std::string sourceImage; std::string outputImage = "output.png"; int nbSeams = DEFAULT_SEAMS; bool vertical = false; app.add_option("-s,--source", sourceImage, "Source image") ->required() ->check(CLI::ExistingFile); app.add_option("-o,--output", outputImage, "Output image")->required(); app.add_option("-m,--mask", maskImage, "Source image") ->check(CLI::ExistingFile); app.add_option("-n,--nb-seams", nbSeams, "Number of seams") ->check(CLI::Number); app.add_option("--max-step", max_step, "Max width of step to find a seam") ->check(CLI::Number); app.add_flag("--vertical", vertical, "Vertical carving (remove vertical seams)"); 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"); CLI11_PARSE(app, argc, argv); // Image loading int width, height, nbChannels; unsigned char *source = stbi_load(sourceImage.c_str(), &width, &height, &nbChannels, 0); unsigned char *mask = nullptr; if (!maskImage.empty()) { int maskWidth, maskHeight, maskChannels; mask = stbi_load(maskImage.c_str(), &maskWidth, &maskHeight, &maskChannels, 0); if (maskWidth != width || maskHeight != height) { std::cerr << maskImage << " and " << sourceImage << " differ in dimension. Please provide a valid mask." << std::endl; exit(1); } if (maskChannels < 3) { std::cerr << maskImage << " needs to be RGB." << std::endl; exit(1); } unsigned char r, g, b; for (auto i = 0; i < width * height; i++) { r = mask[maskChannels * i]; g = mask[maskChannels * i + 1]; b = mask[maskChannels * i + 2]; bool positive = (g > r && g > b && g > 100); // Mask images are not always the cleanest bool negative = (r > g && r > b && r > 100); 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 //* . (0) we want to remove the pixel } if (until_mask_removal && maskImage.empty()) { std::cerr << "Flag --until-mask-removal but no mask provided." << std::endl; until_mask_removal = false; } if (until_mask_removal && nbSeams != DEFAULT_SEAMS) { std::cerr << "Flag --nb-seams specified but --until-mask-removal provided." << std::endl; nbSeams = DEFAULT_SEAMS; } nbSeams = std::min( nbSeams, vertical ? width - 1 : height - 1); // We want to keep at least one row or column auto result = seam_carving(source, width, height, nbChannels, nbSeams, 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); exit(0); }