summaryrefslogtreecommitdiffstats
path: root/app/paint/gimpheal.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:23:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:23:22 +0000
commite42129241681dde7adae7d20697e7b421682fbb4 (patch)
treeaf1fe815a5e639e68e59fabd8395ec69458b3e5e /app/paint/gimpheal.c
parentInitial commit. (diff)
downloadgimp-e42129241681dde7adae7d20697e7b421682fbb4.tar.xz
gimp-e42129241681dde7adae7d20697e7b421682fbb4.zip
Adding upstream version 2.10.22.upstream/2.10.22upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'app/paint/gimpheal.c')
-rw-r--r--app/paint/gimpheal.c642
1 files changed, 642 insertions, 0 deletions
diff --git a/app/paint/gimpheal.c b/app/paint/gimpheal.c
new file mode 100644
index 0000000..814fd26
--- /dev/null
+++ b/app/paint/gimpheal.c
@@ -0,0 +1,642 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpheal.c
+ * Copyright (C) Jean-Yves Couleaud <cjyves@free.fr>
+ * Copyright (C) 2013 Loren Merritt
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdint.h>
+#include <string.h>
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gegl.h>
+
+#include "libgimpbase/gimpbase.h"
+#include "libgimpmath/gimpmath.h"
+
+#include "paint-types.h"
+
+#include "gegl/gimp-gegl-apply-operation.h"
+#include "gegl/gimp-gegl-loops.h"
+
+#include "core/gimpbrush.h"
+#include "core/gimpdrawable.h"
+#include "core/gimpdynamics.h"
+#include "core/gimperror.h"
+#include "core/gimpimage.h"
+#include "core/gimppickable.h"
+#include "core/gimptempbuf.h"
+
+#include "gimpheal.h"
+#include "gimpsourceoptions.h"
+
+#include "gimp-intl.h"
+
+
+
+/* NOTES
+ *
+ * The method used here is similar to the lighting invariant correction
+ * method but slightly different: we do not divide the RGB components,
+ * but subtract them I2 = I0 - I1, where I0 is the sample image to be
+ * corrected, I1 is the reference pattern. Then we solve DeltaI=0
+ * (Laplace) with I2 Dirichlet conditions at the borders of the
+ * mask. The solver is a red/black checker Gauss-Seidel with over-relaxation.
+ * It could benefit from a multi-grid evaluation of an initial solution
+ * before the main iteration loop.
+ *
+ * I reduced the convergence criteria to 0.1% (0.001) as we are
+ * dealing here with RGB integer components, more is overkill.
+ *
+ * Jean-Yves Couleaud cjyves@free.fr
+ */
+
+static gboolean gimp_heal_start (GimpPaintCore *paint_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ const GimpCoords *coords,
+ GError **error);
+static GeglBuffer * gimp_heal_get_paint_buffer (GimpPaintCore *core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ GimpLayerMode paint_mode,
+ const GimpCoords *coords,
+ gint *paint_buffer_x,
+ gint *paint_buffer_y,
+ gint *paint_width,
+ gint *paint_height);
+
+static void gimp_heal_motion (GimpSourceCore *source_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ const GimpCoords *coords,
+ GeglNode *op,
+ gdouble opacity,
+ GimpPickable *src_pickable,
+ GeglBuffer *src_buffer,
+ GeglRectangle *src_rect,
+ gint src_offset_x,
+ gint src_offset_y,
+ GeglBuffer *paint_buffer,
+ gint paint_buffer_x,
+ gint paint_buffer_y,
+ gint paint_area_offset_x,
+ gint paint_area_offset_y,
+ gint paint_area_width,
+ gint paint_area_height);
+
+
+G_DEFINE_TYPE (GimpHeal, gimp_heal, GIMP_TYPE_SOURCE_CORE)
+
+#define parent_class gimp_heal_parent_class
+
+
+void
+gimp_heal_register (Gimp *gimp,
+ GimpPaintRegisterCallback callback)
+{
+ (* callback) (gimp,
+ GIMP_TYPE_HEAL,
+ GIMP_TYPE_SOURCE_OPTIONS,
+ "gimp-heal",
+ _("Healing"),
+ "gimp-tool-heal");
+}
+
+static void
+gimp_heal_class_init (GimpHealClass *klass)
+{
+ GimpPaintCoreClass *paint_core_class = GIMP_PAINT_CORE_CLASS (klass);
+ GimpSourceCoreClass *source_core_class = GIMP_SOURCE_CORE_CLASS (klass);
+
+ paint_core_class->start = gimp_heal_start;
+ paint_core_class->get_paint_buffer = gimp_heal_get_paint_buffer;
+
+ source_core_class->motion = gimp_heal_motion;
+}
+
+static void
+gimp_heal_init (GimpHeal *heal)
+{
+}
+
+static gboolean
+gimp_heal_start (GimpPaintCore *paint_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ const GimpCoords *coords,
+ GError **error)
+{
+ GimpSourceCore *source_core = GIMP_SOURCE_CORE (paint_core);
+
+ if (! GIMP_PAINT_CORE_CLASS (parent_class)->start (paint_core, drawable,
+ paint_options, coords,
+ error))
+ {
+ return FALSE;
+ }
+
+ if (! source_core->set_source && gimp_drawable_is_indexed (drawable))
+ {
+ g_set_error_literal (error, GIMP_ERROR, GIMP_FAILED,
+ _("Healing does not operate on indexed layers."));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static GeglBuffer *
+gimp_heal_get_paint_buffer (GimpPaintCore *core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ GimpLayerMode paint_mode,
+ const GimpCoords *coords,
+ gint *paint_buffer_x,
+ gint *paint_buffer_y,
+ gint *paint_width,
+ gint *paint_height)
+{
+ return GIMP_PAINT_CORE_CLASS (parent_class)->get_paint_buffer (core,
+ drawable,
+ paint_options,
+ GIMP_LAYER_MODE_NORMAL,
+ coords,
+ paint_buffer_x,
+ paint_buffer_y,
+ paint_width,
+ paint_height);
+}
+
+/* Subtract bottom from top and store in result as a float
+ */
+static void
+gimp_heal_sub (GeglBuffer *top_buffer,
+ const GeglRectangle *top_rect,
+ GeglBuffer *bottom_buffer,
+ const GeglRectangle *bottom_rect,
+ GeglBuffer *result_buffer,
+ const GeglRectangle *result_rect)
+{
+ GeglBufferIterator *iter;
+ const Babl *format = gegl_buffer_get_format (top_buffer);
+ gint n_components = babl_format_get_n_components (format);
+
+ if (n_components == 2)
+ format = babl_format ("Y'A float");
+ else if (n_components == 4)
+ format = babl_format ("R'G'B'A float");
+ else
+ g_return_if_reached ();
+
+ iter = gegl_buffer_iterator_new (top_buffer, top_rect, 0, format,
+ GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 3);
+
+ gegl_buffer_iterator_add (iter, bottom_buffer, bottom_rect, 0, format,
+ GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
+
+ gegl_buffer_iterator_add (iter, result_buffer, result_rect, 0,
+ babl_format_n (babl_type ("float"), n_components),
+ GEGL_ACCESS_WRITE, GEGL_ABYSS_NONE);
+
+ while (gegl_buffer_iterator_next (iter))
+ {
+ gfloat *t = iter->items[0].data;
+ gfloat *b = iter->items[1].data;
+ gfloat *r = iter->items[2].data;
+ gint length = iter->length * n_components;
+
+ while (length--)
+ *r++ = *t++ - *b++;
+ }
+}
+
+/* Add first to second and store in result
+ */
+static void
+gimp_heal_add (GeglBuffer *first_buffer,
+ const GeglRectangle *first_rect,
+ GeglBuffer *second_buffer,
+ const GeglRectangle *second_rect,
+ GeglBuffer *result_buffer,
+ const GeglRectangle *result_rect)
+{
+ GeglBufferIterator *iter;
+ const Babl *format = gegl_buffer_get_format (result_buffer);
+ gint n_components = babl_format_get_n_components (format);
+
+ if (n_components == 2)
+ format = babl_format ("Y'A float");
+ else if (n_components == 4)
+ format = babl_format ("R'G'B'A float");
+ else
+ g_return_if_reached ();
+
+ iter = gegl_buffer_iterator_new (first_buffer, first_rect, 0,
+ babl_format_n (babl_type ("float"),
+ n_components),
+ GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 3);
+
+ gegl_buffer_iterator_add (iter, second_buffer, second_rect, 0, format,
+ GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
+
+ gegl_buffer_iterator_add (iter, result_buffer, result_rect, 0, format,
+ GEGL_ACCESS_WRITE, GEGL_ABYSS_NONE);
+
+ while (gegl_buffer_iterator_next (iter))
+ {
+ gfloat *f = iter->items[0].data;
+ gfloat *s = iter->items[1].data;
+ gfloat *r = iter->items[2].data;
+ gint length = iter->length * n_components;
+
+ while (length--)
+ *r++ = *f++ + *s++;
+ }
+}
+
+#if defined(__SSE__) && defined(__GNUC__) && __GNUC__ >= 4
+static float
+gimp_heal_laplace_iteration_sse (gfloat *pixels,
+ gfloat *Adiag,
+ gint *Aidx,
+ gfloat w,
+ gint nmask)
+{
+ typedef float v4sf __attribute__((vector_size(16)));
+ gint i;
+ v4sf wv = { w, w, w, w };
+ v4sf err = { 0, 0, 0, 0 };
+ union { v4sf v; float f[4]; } erru;
+
+#define Xv(j) (*(v4sf*)&pixels[Aidx[i * 5 + j]])
+
+ for (i = 0; i < nmask; i++)
+ {
+ v4sf a = { Adiag[i], Adiag[i], Adiag[i], Adiag[i] };
+ v4sf diff = a * Xv(0) - wv * (Xv(1) + Xv(2) + Xv(3) + Xv(4));
+
+ Xv(0) -= diff;
+ err += diff * diff;
+ }
+
+ erru.v = err;
+
+ return erru.f[0] + erru.f[1] + erru.f[2] + erru.f[3];
+}
+#endif
+
+/* Perform one iteration of Gauss-Seidel, and return the sum squared residual.
+ */
+static float
+gimp_heal_laplace_iteration (gfloat *pixels,
+ gfloat *Adiag,
+ gint *Aidx,
+ gfloat w,
+ gint nmask,
+ gint depth)
+{
+ gint i, k;
+ gfloat err = 0;
+
+#if defined(__SSE__) && defined(__GNUC__) && __GNUC__ >= 4
+ if (depth == 4)
+ return gimp_heal_laplace_iteration_sse (pixels, Adiag, Aidx, w, nmask);
+#endif
+
+ for (i = 0; i < nmask; i++)
+ {
+ gint j0 = Aidx[i * 5 + 0];
+ gint j1 = Aidx[i * 5 + 1];
+ gint j2 = Aidx[i * 5 + 2];
+ gint j3 = Aidx[i * 5 + 3];
+ gint j4 = Aidx[i * 5 + 4];
+ gfloat a = Adiag[i];
+
+ for (k = 0; k < depth; k++)
+ {
+ gfloat diff = (a * pixels[j0 + k] -
+ w * (pixels[j1 + k] +
+ pixels[j2 + k] +
+ pixels[j3 + k] +
+ pixels[j4 + k]));
+
+ pixels[j0 + k] -= diff;
+ err += diff * diff;
+ }
+ }
+
+ return err;
+}
+
+/* Solve the laplace equation for pixels and store the result in-place.
+ */
+static void
+gimp_heal_laplace_loop (gfloat *pixels,
+ gint height,
+ gint depth,
+ gint width,
+ guchar *mask)
+{
+ /* Tolerate a total deviation-from-smoothness of 0.1 LSBs at 8bit depth. */
+#define EPSILON (0.1/255)
+#define MAX_ITER 500
+
+ gint i, j, iter, parity, nmask, zero;
+ gfloat *Adiag;
+ gint *Aidx;
+ gfloat w;
+
+ Adiag = g_new (gfloat, width * height);
+ Aidx = g_new (gint, 5 * width * height);
+
+ /* All off-diagonal elements of A are either -1 or 0. We could store it as a
+ * general-purpose sparse matrix, but that adds some unnecessary overhead to
+ * the inner loop. Instead, assume exactly 4 off-diagonal elements in each
+ * row, all of which have value -1. Any row that in fact wants less than 4
+ * coefs can put them in a dummy column to be multiplied by an empty pixel.
+ */
+ zero = depth * width * height;
+ memset (pixels + zero, 0, depth * sizeof (gfloat));
+
+ /* Construct the system of equations.
+ * Arrange Aidx in checkerboard order, so that a single linear pass over that
+ * array results updating all of the red cells and then all of the black cells.
+ */
+ nmask = 0;
+ for (parity = 0; parity < 2; parity++)
+ for (i = 0; i < height; i++)
+ for (j = (i&1)^parity; j < width; j+=2)
+ if (mask[j + i * width])
+ {
+#define A_NEIGHBOR(o,di,dj) \
+ if ((dj<0 && j==0) || (dj>0 && j==width-1) || (di<0 && i==0) || (di>0 && i==height-1)) \
+ Aidx[o + nmask * 5] = zero; \
+ else \
+ Aidx[o + nmask * 5] = ((i + di) * width + (j + dj)) * depth;
+
+ /* Omit Dirichlet conditions for any neighbors off the
+ * edge of the canvas.
+ */
+ Adiag[nmask] = 4 - (i==0) - (j==0) - (i==height-1) - (j==width-1);
+ A_NEIGHBOR (0, 0, 0);
+ A_NEIGHBOR (1, 0, 1);
+ A_NEIGHBOR (2, 1, 0);
+ A_NEIGHBOR (3, 0, -1);
+ A_NEIGHBOR (4, -1, 0);
+ nmask++;
+ }
+
+ /* Empirically optimal over-relaxation factor. (Benchmarked on
+ * round brushes, at least. I don't know whether aspect ratio
+ * affects it.)
+ */
+ w = 2.0 - 1.0 / (0.1575 * sqrt (nmask) + 0.8);
+ w *= 0.25;
+ for (i = 0; i < nmask; i++)
+ Adiag[i] *= w;
+
+ /* Gauss-Seidel with successive over-relaxation */
+ for (iter = 0; iter < MAX_ITER; iter++)
+ {
+ gfloat err = gimp_heal_laplace_iteration (pixels, Adiag, Aidx,
+ w, nmask, depth);
+ if (err < EPSILON * EPSILON * w * w)
+ break;
+ }
+
+ g_free (Adiag);
+ g_free (Aidx);
+}
+
+/* Original Algorithm Design:
+ *
+ * T. Georgiev, "Photoshop Healing Brush: a Tool for Seamless Cloning
+ * http://www.tgeorgiev.net/Photoshop_Healing.pdf
+ */
+static void
+gimp_heal (GeglBuffer *src_buffer,
+ const GeglRectangle *src_rect,
+ GeglBuffer *dest_buffer,
+ const GeglRectangle *dest_rect,
+ GeglBuffer *mask_buffer,
+ const GeglRectangle *mask_rect)
+{
+ const Babl *src_format;
+ const Babl *dest_format;
+ gint src_components;
+ gint dest_components;
+ gint width;
+ gint height;
+ gfloat *diff, *diff_alloc;
+ GeglBuffer *diff_buffer;
+ guchar *mask;
+
+ src_format = gegl_buffer_get_format (src_buffer);
+ dest_format = gegl_buffer_get_format (dest_buffer);
+
+ src_components = babl_format_get_n_components (src_format);
+ dest_components = babl_format_get_n_components (dest_format);
+
+ width = gegl_buffer_get_width (src_buffer);
+ height = gegl_buffer_get_height (src_buffer);
+
+ g_return_if_fail (src_components == dest_components);
+
+ diff_alloc = g_new (gfloat, 4 + (width * height + 1) * src_components);
+ diff = (gfloat*)(((uintptr_t)diff_alloc + 15) & ~15);
+
+ diff_buffer =
+ gegl_buffer_linear_new_from_data (diff,
+ babl_format_n (babl_type ("float"),
+ src_components),
+ GEGL_RECTANGLE (0, 0, width, height),
+ GEGL_AUTO_ROWSTRIDE,
+ (GDestroyNotify) g_free, diff_alloc);
+
+ /* subtract pattern from image and store the result as a float in diff */
+ gimp_heal_sub (dest_buffer, dest_rect,
+ src_buffer, src_rect,
+ diff_buffer, GEGL_RECTANGLE (0, 0, width, height));
+
+ mask = g_new (guchar, mask_rect->width * mask_rect->height);
+
+ gegl_buffer_get (mask_buffer, mask_rect, 1.0, babl_format ("Y u8"),
+ mask, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
+
+ gimp_heal_laplace_loop (diff, height, src_components, width, mask);
+
+ g_free (mask);
+
+ /* add solution to original image and store in dest */
+ gimp_heal_add (diff_buffer, GEGL_RECTANGLE (0, 0, width, height),
+ src_buffer, src_rect,
+ dest_buffer, dest_rect);
+
+ g_object_unref (diff_buffer);
+}
+
+static void
+gimp_heal_motion (GimpSourceCore *source_core,
+ GimpDrawable *drawable,
+ GimpPaintOptions *paint_options,
+ const GimpCoords *coords,
+ GeglNode *op,
+ gdouble opacity,
+ GimpPickable *src_pickable,
+ GeglBuffer *src_buffer,
+ GeglRectangle *src_rect,
+ gint src_offset_x,
+ gint src_offset_y,
+ GeglBuffer *paint_buffer,
+ gint paint_buffer_x,
+ gint paint_buffer_y,
+ gint paint_area_offset_x,
+ gint paint_area_offset_y,
+ gint paint_area_width,
+ gint paint_area_height)
+{
+ GimpPaintCore *paint_core = GIMP_PAINT_CORE (source_core);
+ GimpContext *context = GIMP_CONTEXT (paint_options);
+ GimpSourceOptions *src_options = GIMP_SOURCE_OPTIONS (paint_options);
+ GimpDynamics *dynamics = GIMP_BRUSH_CORE (paint_core)->dynamics;
+ GimpImage *image = gimp_item_get_image (GIMP_ITEM (drawable));
+ GeglBuffer *src_copy;
+ GeglBuffer *mask_buffer;
+ GimpPickable *dest_pickable;
+ const GimpTempBuf *mask_buf;
+ gdouble fade_point;
+ gdouble force;
+ gint mask_off_x;
+ gint mask_off_y;
+ gint dest_pickable_off_x;
+ gint dest_pickable_off_y;
+
+ fade_point = gimp_paint_options_get_fade (paint_options, image,
+ paint_core->pixel_dist);
+
+ if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
+ force = gimp_dynamics_get_linear_value (dynamics,
+ GIMP_DYNAMICS_OUTPUT_FORCE,
+ coords,
+ paint_options,
+ fade_point);
+ else
+ force = paint_options->brush_force;
+
+ mask_buf = gimp_brush_core_get_brush_mask (GIMP_BRUSH_CORE (source_core),
+ coords,
+ GIMP_BRUSH_HARD,
+ force);
+
+ if (! mask_buf)
+ return;
+
+ /* check that all buffers are of the same size */
+ if (src_rect->width != gegl_buffer_get_width (paint_buffer) ||
+ src_rect->height != gegl_buffer_get_height (paint_buffer))
+ {
+ /* this generally means that the source point has hit the edge
+ * of the layer, so it is not an error and we should not
+ * complain, just don't do anything
+ */
+ return;
+ }
+
+ /* heal should work in perceptual space, use R'G'B' instead of RGB */
+ src_copy = gegl_buffer_new (GEGL_RECTANGLE (paint_area_offset_x,
+ paint_area_offset_y,
+ src_rect->width,
+ src_rect->height),
+ babl_format ("R'G'B'A float"));
+
+ if (! op)
+ {
+ gimp_gegl_buffer_copy (src_buffer, src_rect, GEGL_ABYSS_NONE,
+ src_copy, gegl_buffer_get_extent (src_copy));
+ }
+ else
+ {
+ gimp_gegl_apply_operation (src_buffer, NULL, NULL, op,
+ src_copy, gegl_buffer_get_extent (src_copy),
+ FALSE);
+ }
+
+ if (src_options->sample_merged)
+ {
+ dest_pickable = GIMP_PICKABLE (image);
+
+ gimp_item_get_offset (GIMP_ITEM (drawable),
+ &dest_pickable_off_x,
+ &dest_pickable_off_y);
+ }
+ else
+ {
+ dest_pickable = GIMP_PICKABLE (drawable);
+
+ dest_pickable_off_x = 0;
+ dest_pickable_off_y = 0;
+ }
+
+ gimp_gegl_buffer_copy (gimp_pickable_get_buffer (dest_pickable),
+ GEGL_RECTANGLE (paint_buffer_x + dest_pickable_off_x,
+ paint_buffer_y + dest_pickable_off_y,
+ gegl_buffer_get_width (paint_buffer),
+ gegl_buffer_get_height (paint_buffer)),
+ GEGL_ABYSS_NONE,
+ paint_buffer,
+ GEGL_RECTANGLE (paint_area_offset_x,
+ paint_area_offset_y,
+ paint_area_width,
+ paint_area_height));
+
+ mask_buffer = gimp_temp_buf_create_buffer ((GimpTempBuf *) mask_buf);
+
+ /* find the offset of the brush mask's rect */
+ {
+ gint x = (gint) floor (coords->x) - (gegl_buffer_get_width (mask_buffer) >> 1);
+ gint y = (gint) floor (coords->y) - (gegl_buffer_get_height (mask_buffer) >> 1);
+
+ mask_off_x = (x < 0) ? -x : 0;
+ mask_off_y = (y < 0) ? -y : 0;
+ }
+
+ gimp_heal (src_copy, gegl_buffer_get_extent (src_copy),
+ paint_buffer,
+ GEGL_RECTANGLE (paint_area_offset_x,
+ paint_area_offset_y,
+ paint_area_width,
+ paint_area_height),
+ mask_buffer,
+ GEGL_RECTANGLE (mask_off_x, mask_off_y,
+ paint_area_width,
+ paint_area_height));
+
+ g_object_unref (src_copy);
+ g_object_unref (mask_buffer);
+
+ /* replace the canvas with our healed data */
+ gimp_brush_core_replace_canvas (GIMP_BRUSH_CORE (paint_core), drawable,
+ coords,
+ MIN (opacity, GIMP_OPACITY_OPAQUE),
+ gimp_context_get_opacity (context),
+ gimp_paint_options_get_brush_mode (paint_options),
+ force,
+ GIMP_PAINT_INCREMENTAL);
+}