546 lines
18 KiB
C++
546 lines
18 KiB
C++
#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 <libgimpbase/gimpbaseenums.h>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#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, "<Image>/Filters/");
|
|
gimp_procedure_set_documentation(procedure, "Intelligent image resizing",
|
|
NULL, NULL);
|
|
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, 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<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)
|