feat(kg-3)

This commit is contained in:
2025-10-23 17:26:47 +03:00
parent 886f7f4268
commit 4a27006658
2 changed files with 428 additions and 8 deletions

View File

@ -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)

View File

@ -1,9 +1,17 @@
#include "gtk/gtk.h"
#include "glib-object.h"
#include "glib.h"
#include "glibconfig.h"
#include <barrier>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <gtk/gtk.h>
#include <libgimp/gimp.h>
#include <libgimp/gimpenums.h>
#include <libgimp/gimpui.h>
#include <libgimp/gimpuitypes.h>
#include <libgimpbase/gimpbaseenums.h>
#include <thread>
#include <vector>
#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 <vlad@sek1.ro>",
"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<std::thread> 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<guint64> 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<std::thread> 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)