feat(kg-3)
This commit is contained in:
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user