#include "glib-object.h" #include "glib.h" #include "glibconfig.h" #include #include #include #include #include #include #include #include #include #include #include #define PLUG_IN_PROC "plug-in-seam-carving" #define PLUG_IN_BINARY "seam_carving" struct _Plugin { GimpPlugIn parent_instance; }; #define PLUGIN_TYPE (plugin_get_type()) G_DECLARE_FINAL_TYPE(Plugin, plugin, plugin, , GimpPlugIn) static GList *plugin_query_procedures(GimpPlugIn *plug_in); static GimpProcedure *plugin_create_procedure(GimpPlugIn *plug_in, const gchar *name); static GimpValueArray *plugin_run(GimpProcedure *procedure, GimpRunMode run_mode, GimpImage *image, GimpDrawable **drawables, GimpProcedureConfig *config, gpointer run_data); G_DEFINE_TYPE(Plugin, plugin, GIMP_TYPE_PLUG_IN) static void plugin_class_init(PluginClass *klass) { GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS(klass); plug_in_class->query_procedures = plugin_query_procedures; plug_in_class->create_procedure = plugin_create_procedure; } static void plugin_init(Plugin *plugin) {} static GList *plugin_query_procedures(GimpPlugIn *plug_in) { return g_list_append(NULL, g_strdup(PLUG_IN_PROC)); } static GimpProcedure *plugin_create_procedure(GimpPlugIn *plug_in, const gchar *name) { GimpProcedure *procedure = NULL; if (g_strcmp0(name, PLUG_IN_PROC) == 0) { procedure = gimp_image_procedure_new( plug_in, name, GIMP_PDB_PROC_TYPE_PLUGIN, plugin_run, NULL, NULL); gimp_procedure_set_sensitivity_mask(procedure, GIMP_PROCEDURE_SENSITIVE_DRAWABLE); gimp_procedure_set_menu_label(procedure, "_Seam carving"); gimp_procedure_add_menu_path(procedure, "/Filters/"); gimp_procedure_set_documentation(procedure, "Intelligent image resizing", NULL, NULL); gimp_procedure_set_attribution(procedure, "Vlad Litvinov ", "LGPL-3.0", "2025"); gimp_procedure_add_int_argument(procedure, "blur-factor", "Blur factor", NULL, 0, 100, 3, G_PARAM_READWRITE); gimp_procedure_add_int_argument(procedure, "horizontal-resize", "Horizontal resize", NULL, 0, 100, 50, G_PARAM_READWRITE); gimp_procedure_add_boolean_argument(procedure, "show-path", "Show path", NULL, false, G_PARAM_READWRITE); gimp_procedure_add_boolean_argument(procedure, "show-energy", "Show energy", NULL, false, G_PARAM_READWRITE); } return procedure; } typedef struct { guint8 r, g, b, a; } RGBA; #define PATH_VOID -2 #define PATH_END -1 #define PATH_LEFT 0 #define PATH_BOTTOM 1 #define PATH_RIGHT 2 typedef struct { gint8 direction; guint64 energy; } PathCell; static void carve_seam(RGBA *energy, RGBA *output, PathCell *path, gint *width, gint height, gint rowstride); static void print_seam(RGBA *output, PathCell *path, gint width, gint height, gint rowstride); static void update_void_path(RGBA *energy, PathCell *path, gint width, gint height, gint rowstride); static guint64 update_path(RGBA *energy, PathCell *path, gint width, gint height, gint rowstride); static void print_state(RGBA *energy, PathCell *path, gint width, gint height, gint rowstride) { for (gint row = 0; row < height; row++) { for (gint col = 0; col < width; col++) { printf("%4hhu ", energy[row * rowstride + col].r); } printf("\n"); } printf("----\n"); for (gint row = 0; row < height; row++) { for (gint col = 0; col < width; col++) { printf("%4lu ", path[row * rowstride + col].energy); } printf("\n"); } printf("----\n"); for (gint row = 0; row < height; row++) { for (gint col = 0; col < width; col++) { switch (path[row * rowstride + col].direction) { case PATH_LEFT: printf("/ "); break; case PATH_BOTTOM: printf("| "); break; case PATH_RIGHT: printf("\\ "); break; } } printf("\n"); } } static GimpValueArray *plugin_run(GimpProcedure *procedure, GimpRunMode run_mode, GimpImage *image, GimpDrawable **drawables, GimpProcedureConfig *config, gpointer run_data) { gint blur_factor, horizontal_resize; gboolean show_path, show_energy; g_object_get(config, "blur-factor", &blur_factor, "horizontal-resize", &horizontal_resize, "show-path", &show_path, "show-energy", &show_energy, NULL); if (run_mode == GIMP_RUN_INTERACTIVE) { GtkWidget *dialog, *area, *label_bf, *label_hr, *scale_bf, *scale_hr, *toggle_sp, *toggle_se; gimp_ui_init(PLUG_IN_BINARY); dialog = gtk_dialog_new_with_buttons("Seam carving", NULL, GTK_DIALOG_MODAL, "_Ok", GTK_RESPONSE_OK, "_Cancel", GTK_RESPONSE_CANCEL, NULL); gtk_window_set_resizable(GTK_WINDOW(dialog), false); area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); gtk_widget_set_margin_start(area, 12); gtk_widget_set_margin_end(area, 12); gtk_widget_set_margin_top(area, 12); scale_bf = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 100, 1); scale_hr = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 100, 1); toggle_sp = gtk_toggle_button_new_with_label("Show path"); toggle_se = gtk_toggle_button_new_with_label("Show energy"); gtk_range_set_value(GTK_RANGE(scale_bf), 3); gtk_range_set_value(GTK_RANGE(scale_hr), 50); label_bf = gtk_label_new("Blur factor"); label_hr = gtk_label_new("Horizontal resize"); gtk_box_pack_end(GTK_BOX(area), toggle_se, FALSE, FALSE, 0); gtk_box_pack_end(GTK_BOX(area), toggle_sp, FALSE, FALSE, 0); gtk_box_pack_end(GTK_BOX(area), scale_hr, FALSE, FALSE, 0); gtk_box_pack_end(GTK_BOX(area), label_hr, FALSE, FALSE, 0); gtk_box_pack_end(GTK_BOX(area), scale_bf, FALSE, FALSE, 0); gtk_box_pack_end(GTK_BOX(area), label_bf, FALSE, FALSE, 0); gtk_widget_show_all(dialog); gint response = gtk_dialog_run(GTK_DIALOG(dialog)); switch (response) { case GTK_RESPONSE_OK: blur_factor = gtk_range_get_value(GTK_RANGE(scale_bf)); horizontal_resize = gtk_range_get_value(GTK_RANGE(scale_hr)); show_path = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle_sp)); show_energy = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle_se)); break; default: gtk_widget_destroy(dialog); return gimp_procedure_new_return_values(procedure, GIMP_PDB_CANCEL, NULL); } gtk_widget_destroy(dialog); } const Babl *rgba_u8 = babl_format("RGBA u8"); for (int i = 0; i < gimp_core_object_array_get_length((GObject **)drawables); i++) { gint width = gimp_drawable_get_width(drawables[i]), height = gimp_drawable_get_height(drawables[i]), target_width = width * horizontal_resize / 100, rowstride = width; GimpLayer *output_layer = gimp_layer_new_from_drawable(drawables[i], image); GimpLayer *energy_layer = gimp_layer_new_from_drawable(drawables[i], image); // https://gegl.org/operations/gegl-saturation.html // https://gegl.org/operations/gegl-gaussian-blur.html // https://gegl.org/operations/gegl-edge-sobel.html gimp_drawable_merge_new_filter( GIMP_DRAWABLE(energy_layer), "gegl:saturation", "Gray", GIMP_LAYER_MODE_REPLACE, 1.0, "scale", 0.0, NULL); gimp_drawable_merge_new_filter( GIMP_DRAWABLE(energy_layer), "gegl:gaussian-blur", "Blur", GIMP_LAYER_MODE_REPLACE, 1.0, "std-dev-x", blur_factor * 0.15, "std-dev-y", blur_factor * 0.15, NULL); gimp_drawable_merge_new_filter(GIMP_DRAWABLE(energy_layer), "gegl:edge-sobel", "Edge", GIMP_LAYER_MODE_REPLACE, 1.0, NULL); GeglBuffer *output_buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(output_layer)); GeglBuffer *energy_buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(energy_layer)); auto *output = (RGBA *)g_malloc(width * height * sizeof(RGBA)); auto *energy = (RGBA *)g_malloc(width * height * sizeof(RGBA)); auto *path = (PathCell *)g_malloc(width * height * sizeof(PathCell)); GeglRectangle rect = {0, 0, width, height}; gegl_buffer_get(output_buffer, &rect, 1.0, rgba_u8, output, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_BLACK); gegl_buffer_get(energy_buffer, &rect, 1.0, rgba_u8, energy, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_BLACK); guint64 max_energy = update_path(energy, path, width, height, rowstride); if (width < 40) { print_state(energy, path, width, height, rowstride); } if (show_energy) { for (gint row = 0; row < height; row++) { for (gint col = 0; col < width; col++) { guint8 e = (float)(path[row * rowstride + col].energy) / max_energy * 255; output[row * rowstride + col].r = e; output[row * rowstride + col].g = e; output[row * rowstride + col].b = e; output[row * rowstride + col].a = 255; } } } else { if (show_path) { for (gint i = 0; i < width - target_width; i++) { print_seam(output, path, width, height, rowstride); update_void_path(energy, path, width, height, rowstride); if (width < 40) { print_state(energy, path, width, height, rowstride); } } } else { while (width > target_width) { carve_seam(energy, output, path, &width, height, rowstride); update_path(energy, path, width, height, rowstride); if (width < 40) { print_state(energy, path, width, height, rowstride); } } } } gegl_buffer_set(output_buffer, &rect, 0, rgba_u8, output, GEGL_AUTO_ROWSTRIDE); gegl_buffer_flush(output_buffer); gimp_image_insert_layer( image, output_layer, NULL, gimp_image_get_item_position(image, GIMP_ITEM(drawables[i])) + 1); if (!show_energy && !show_path) { gimp_layer_resize(output_layer, width, height, 0, 0); } if (GIMP_IS_LAYER(drawables[i])) { gimp_image_remove_layer(image, GIMP_LAYER(drawables[i])); } else if (GIMP_IS_CHANNEL(drawables[i])) { gimp_image_remove_channel(image, GIMP_CHANNEL(drawables[i])); } g_free(path); g_free(energy); g_object_unref(energy_layer); } babl_exit(); return gimp_procedure_new_return_values(procedure, GIMP_PDB_SUCCESS, NULL); } static void update_void_path(RGBA *energy, PathCell *path, gint width, gint height, gint rowstride) { auto threads_count = std::thread::hardware_concurrency(); std::barrier sync_point(threads_count); auto worker = [&](int id) { for (gint row = height - 2; row >= 0; row--) { gint start = id * width / threads_count; gint end = (id + 1) * width / threads_count; for (gint col = start; col < end; col++) { PathCell *cell = &path[row * rowstride + col]; if (cell->direction == PATH_VOID) { continue; } guint64 left = 0, bottom, right = 0; if (col > 0) { gint i = col - 1; for (; i >= 0 && path[(row + 1) * rowstride + i].direction == PATH_VOID; i--) { } left = path[(row + 1) * rowstride + i].energy; } { gint i = col; for (; i >= 0 && path[(row + 1) * rowstride + i].direction == PATH_VOID; i--) { } for (; i < width && path[(row + 1) * rowstride + i].direction == PATH_VOID; i++) { } bottom = path[(row + 1) * rowstride + col].energy; } if (col < width - 1) { gint i = col + 1; for (; i < width && path[(row + 1) * rowstride + i].direction == PATH_VOID; i++) { } right = path[(row + 1) * rowstride + i].energy; } cell->energy = energy[row * rowstride + col].r; if (col > 0 && left < bottom && left < right) { cell->direction = PATH_LEFT; cell->energy += left; } else if (col < (width - 1) && right < bottom && right < left) { cell->direction = PATH_RIGHT; cell->energy += right; } else { cell->direction = PATH_BOTTOM; cell->energy += bottom; } } sync_point.arrive_and_wait(); } }; std::vector threads; for (gint i = 0; i < threads_count; i++) { threads.emplace_back(worker, i); } for (auto &t : threads) { t.join(); } } static guint64 update_path(RGBA *energy, PathCell *path, gint width, gint height, gint rowstride) { std::atomic max_energy = 0; for (gint col = 0; col < width; col++) { PathCell *cell = &path[(height - 1) * rowstride + col]; cell->direction = PATH_END; cell->energy = energy[(height - 1) * rowstride + col].r; max_energy = MAX(max_energy.load(), cell->energy); } auto threads_count = std::thread::hardware_concurrency(); std::barrier sync_point(threads_count); auto worker = [&](int id) { for (gint row = height - 2; row >= 0; row--) { gint start = id * width / threads_count; gint end = (id + 1) * width / threads_count; for (gint col = start; col < end; col++) { guint64 left = 0, bottom, right = 0; if (col > 0) { left = path[(row + 1) * rowstride + col - 1].energy; } bottom = path[(row + 1) * rowstride + col].energy; if (col < width - 1) { right = path[(row + 1) * rowstride + col + 1].energy; } PathCell *cell = &path[row * rowstride + col]; cell->energy = energy[row * rowstride + col].r; if (col > 0 && left < bottom && left < right) { cell->direction = PATH_LEFT; cell->energy += left; } else if (col < (width - 1) && right < bottom && right < left) { cell->direction = PATH_RIGHT; cell->energy += right; } else { cell->direction = PATH_BOTTOM; cell->energy += bottom; } guint64 current_energy = max_energy.load(); while ( current_energy < cell->energy && !max_energy.compare_exchange_weak(current_energy, cell->energy)) { } } sync_point.arrive_and_wait(); } }; std::vector threads; for (gint i = 0; i < threads_count; i++) { threads.emplace_back(worker, i); } for (auto &t : threads) { t.join(); } return max_energy; } static gint find_min_col(PathCell *path, gint width) { gint min_col = 0; guint64 min_energy = UINT64_MAX; for (gint col = 0; col < width; col++) { if (path[col].direction != PATH_VOID && path[col].energy < min_energy) { min_energy = path[col].energy; min_col = col; } } return MIN(width - 2, min_col); } static void print_seam(RGBA *output, PathCell *path, gint width, gint height, gint rowstride) { gint col = find_min_col(path, width), row = 0; for (; row < height - 1; row++) { RGBA *out = &output[row * rowstride + col]; out->r = 255; out->g = 0; out->b = 255; out->a = 255; gint8 dir = path[row * rowstride + col].direction; path[row * rowstride + col].direction = PATH_VOID; switch (dir) { case PATH_LEFT: do { col--; } while (col > 0 && path[(row + 1) * rowstride + col].direction == PATH_VOID); while (col < width - 1 && path[(row + 1) * rowstride + col].direction == PATH_VOID) { col++; } break; case PATH_RIGHT: do { col++; } while (col < width - 1 && path[(row + 1) * rowstride + col].direction == PATH_VOID); while (col > 0 && path[(row + 1) * rowstride + col].direction == PATH_VOID) { col--; } break; case PATH_BOTTOM: while (col < width - 1 && path[(row + 1) * rowstride + col].direction == PATH_VOID) { col++; } while (col > 0 && path[(row + 1) * rowstride + col].direction == PATH_VOID) { col--; } break; } } path[row * rowstride + col].direction = PATH_VOID; } static void carve_seam(RGBA *energy, RGBA *output, PathCell *path, gint *width, gint height, gint rowstride) { gint col = find_min_col(path, *width), row = 0; for (; row < height - 1; row++) { gint old_col = col; switch (path[row * rowstride + col].direction) { case PATH_LEFT: col = MAX(0, col - 1); break; case PATH_RIGHT: col = MIN(*width - 1, col + 1); break; } memmove(path + row * rowstride + old_col, path + row * rowstride + old_col + 1, (*width - old_col - 1) * sizeof(PathCell)); memmove(output + row * rowstride + old_col, output + row * rowstride + old_col + 1, (*width - old_col - 1) * sizeof(RGBA)); memmove(energy + row * rowstride + old_col, energy + row * rowstride + old_col + 1, (*width - old_col - 1) * sizeof(RGBA)); } memmove(path + row * rowstride + col, path + row * rowstride + col + 1, (*width - col - 1) * sizeof(PathCell)); memmove(output + row * rowstride + col, output + row * rowstride + col + 1, (*width - col - 1) * sizeof(RGBA)); memmove(energy + row * rowstride + col, energy + row * rowstride + col + 1, (*width - col - 1) * sizeof(RGBA)); (*width)--; } GIMP_MAIN(PLUGIN_TYPE)