diff --git a/kg/25-1/3/Makefile b/kg/25-1/3/Makefile index 849a91a..979c923 100644 --- a/kg/25-1/3/Makefile +++ b/kg/25-1/3/Makefile @@ -1,5 +1,5 @@ DIRS=dist build $(PLUGIN_DIR) -CXX=clang++ -std=c++23 +CXX=clang++ -std=c++23 -O3 NAME=seam_carving PLUGIN_DIR=$(HOME)/.config/GIMP/3.0/plug-ins/$(NAME) diff --git a/kg/25-1/3/plugin.cpp b/kg/25-1/3/plugin.cpp index e195126..28f01b4 100644 --- a/kg/25-1/3/plugin.cpp +++ b/kg/25-1/3/plugin.cpp @@ -1,9 +1,17 @@ -#include "gtk/gtk.h" +#include "glib-object.h" +#include "glib.h" +#include "glibconfig.h" +#include #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" @@ -56,15 +64,79 @@ static GimpProcedure *plugin_create_procedure(GimpPlugIn *plug_in, gimp_procedure_set_attribution(procedure, "Vlad Litvinov ", "LGPL-3.0", "2025"); gimp_procedure_add_int_argument(procedure, "blur-factor", "Blur factor", - NULL, 0, 100, 20, G_PARAM_READWRITE); + 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, @@ -72,11 +144,14 @@ static GimpValueArray *plugin_run(GimpProcedure *procedure, 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, NULL); + &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; + GtkWidget *dialog, *area, *label_bf, *label_hr, *scale_bf, *scale_hr, + *toggle_sp, *toggle_se; gimp_ui_init(PLUG_IN_BINARY); @@ -92,13 +167,17 @@ static GimpValueArray *plugin_run(GimpProcedure *procedure, 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), blur_factor); - gtk_range_set_value(GTK_RANGE(scale_hr), horizontal_resize); + 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); @@ -112,14 +191,355 @@ static GimpValueArray *plugin_run(GimpProcedure *procedure, 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)