diff options
Diffstat (limited to 'app/core/gimplineart.c')
-rw-r--r-- | app/core/gimplineart.c | 2979 |
1 files changed, 2979 insertions, 0 deletions
diff --git a/app/core/gimplineart.c b/app/core/gimplineart.c new file mode 100644 index 0000000..bf8460c --- /dev/null +++ b/app/core/gimplineart.c @@ -0,0 +1,2979 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * Copyright (C) 2017 Sébastien Fourey & David Tchumperlé + * Copyright (C) 2018 Jehan + * + * 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 <gdk-pixbuf/gdk-pixbuf.h> +#include <gegl.h> + +#include "libgimpmath/gimpmath.h" + +#include "core-types.h" + +#include "gegl/gimp-gegl-loops.h" +#include "gegl/gimp-gegl-utils.h" + +#include "gimp-parallel.h" +#include "gimp-priorities.h" +#include "gimp-utils.h" /* GIMP_TIMER */ +#include "gimpasync.h" +#include "gimpcancelable.h" +#include "gimpdrawable.h" +#include "gimpimage.h" +#include "gimplineart.h" +#include "gimpmarshal.h" +#include "gimppickable.h" +#include "gimpprojection.h" +#include "gimpviewable.h" +#include "gimpwaitable.h" + +#include "gimp-intl.h" + +enum +{ + COMPUTING_START, + COMPUTING_END, + LAST_SIGNAL, +}; + +enum +{ + PROP_0, + PROP_SELECT_TRANSPARENT, + PROP_MAX_GROW, + PROP_THRESHOLD, + PROP_SPLINE_MAX_LEN, + PROP_SEGMENT_MAX_LEN, +}; + +typedef struct _GimpLineArtPrivate GimpLineArtPrivate; + +struct _GimpLineArtPrivate +{ + gboolean frozen; + gboolean compute_after_thaw; + + GimpAsync *async; + + gint idle_id; + + GimpPickable *input; + GeglBuffer *closed; + gfloat *distmap; + + /* Used in the closing step. */ + gboolean select_transparent; + gdouble threshold; + gint spline_max_len; + gint segment_max_len; + gboolean max_len_bound; + + /* Used in the grow step. */ + gint max_grow; +}; + +typedef struct +{ + GeglBuffer *buffer; + + gboolean select_transparent; + gdouble threshold; + gint spline_max_len; + gint segment_max_len; +} LineArtData; + +typedef struct +{ + GeglBuffer *closed; + gfloat *distmap; +} LineArtResult; + +static int DeltaX[4] = {+1, -1, 0, 0}; +static int DeltaY[4] = {0, 0, +1, -1}; + +static const GimpVector2 Direction2Normal[4] = +{ + { 1.0f, 0.0f }, + { -1.0f, 0.0f }, + { 0.0f, 1.0f }, + { 0.0f, -1.0f } +}; + +typedef enum _Direction +{ + XPlusDirection = 0, + XMinusDirection = 1, + YPlusDirection = 2, + YMinusDirection = 3 +} Direction; + +typedef GimpVector2 Pixel; + +typedef struct _SplineCandidate +{ + Pixel p1; + Pixel p2; + float quality; +} SplineCandidate; + +typedef struct _Edgel +{ + gint x, y; + Direction direction; + + gfloat x_normal; + gfloat y_normal; + gfloat curvature; + guint next, previous; +} Edgel; + + +static void gimp_line_art_finalize (GObject *object); +static void gimp_line_art_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); +static void gimp_line_art_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); + +/* Functions for asynchronous computation. */ + +static void gimp_line_art_compute (GimpLineArt *line_art); +static void gimp_line_art_compute_cb (GimpAsync *async, + GimpLineArt *line_art); + +static GimpAsync * gimp_line_art_prepare_async (GimpLineArt *line_art, + gint priority); +static void gimp_line_art_prepare_async_func (GimpAsync *async, + LineArtData *data); +static LineArtData * line_art_data_new (GeglBuffer *buffer, + GimpLineArt *line_art); +static void line_art_data_free (LineArtData *data); +static LineArtResult * line_art_result_new (GeglBuffer *line_art, + gfloat *distmap); +static void line_art_result_free (LineArtResult *result); + +static gboolean gimp_line_art_idle (GimpLineArt *line_art); +static void gimp_line_art_input_invalidate_preview (GimpViewable *viewable, + GimpLineArt *line_art); + + +/* All actual computation functions. */ + +static GeglBuffer * gimp_line_art_close (GeglBuffer *buffer, + gboolean select_transparent, + gdouble stroke_threshold, + gint spline_max_length, + gint segment_max_length, + gint minimal_lineart_area, + gint normal_estimate_mask_size, + gfloat end_point_rate, + gfloat spline_max_angle, + gint end_point_connectivity, + gfloat spline_roundness, + gboolean allow_self_intersections, + gint created_regions_significant_area, + gint created_regions_minimum_area, + gboolean small_segments_from_spline_sources, + gfloat **lineart_distmap, + GimpAsync *async); + +static void gimp_lineart_denoise (GeglBuffer *buffer, + int size, + GimpAsync *async); +static void gimp_lineart_compute_normals_curvatures (GeglBuffer *mask, + gfloat *normals, + gfloat *curvatures, + gfloat *smoothed_curvatures, + int normal_estimate_mask_size, + GimpAsync *async); +static gfloat * gimp_lineart_get_smooth_curvatures (GArray *edgelset, + GimpAsync *async); +static GArray * gimp_lineart_curvature_extremums (gfloat *curvatures, + gfloat *smoothed_curvatures, + gint curvatures_width, + gint curvatures_height, + GimpAsync *async); +static gint gimp_spline_candidate_cmp (const SplineCandidate *a, + const SplineCandidate *b, + gpointer user_data); +static GList * gimp_lineart_find_spline_candidates (GArray *max_positions, + gfloat *normals, + gint width, + gint distance_threshold, + gfloat max_angle_deg, + GimpAsync *async); + +static GArray * gimp_lineart_discrete_spline (Pixel p0, + GimpVector2 n0, + Pixel p1, + GimpVector2 n1); + +static gint gimp_number_of_transitions (GArray *pixels, + GeglBuffer *buffer); +static gboolean gimp_line_art_allow_closure (GeglBuffer *mask, + GArray *pixels, + GList **fill_pixels, + int significant_size, + int minimum_size); +static GArray * gimp_lineart_line_segment_until_hit (const GeglBuffer *buffer, + Pixel start, + GimpVector2 direction, + int size); +static gfloat * gimp_lineart_estimate_strokes_radii (GeglBuffer *mask, + GimpAsync *async); +static void gimp_line_art_simple_fill (GeglBuffer *buffer, + gint x, + gint y, + gint *counter); + +/* Some callback-type functions. */ + +static guint visited_hash_fun (Pixel *key); +static gboolean visited_equal_fun (Pixel *e1, + Pixel *e2); + +static inline gboolean border_in_direction (GeglBuffer *mask, + Pixel p, + int direction); +static inline GimpVector2 pair2normal (Pixel p, + gfloat *normals, + gint width); + +/* Edgel */ + +static Edgel * gimp_edgel_new (int x, + int y, + Direction direction); +static void gimp_edgel_init (Edgel *edgel); +static void gimp_edgel_clear (Edgel **edgel); +static int gimp_edgel_cmp (const Edgel *e1, + const Edgel *e2); +static guint edgel2index_hash_fun (Edgel *key); +static gboolean edgel2index_equal_fun (Edgel *e1, + Edgel *e2); + +static glong gimp_edgel_track_mark (GeglBuffer *mask, + Edgel edgel, + long size_limit); +static glong gimp_edgel_region_area (const GeglBuffer *mask, + Edgel start_edgel); + +/* Edgel set */ + +static GArray * gimp_edgelset_new (GeglBuffer *buffer, + GimpAsync *async); +static void gimp_edgelset_add (GArray *set, + int x, + int y, + Direction direction, + GHashTable *edgel2index); +static void gimp_edgelset_init_normals (GArray *set); +static void gimp_edgelset_smooth_normals (GArray *set, + int mask_size, + GimpAsync *async); +static void gimp_edgelset_compute_curvature (GArray *set, + GimpAsync *async); + +static void gimp_edgelset_build_graph (GArray *set, + GeglBuffer *buffer, + GHashTable *edgel2index, + GimpAsync *async); +static void gimp_edgelset_next8 (const GeglBuffer *buffer, + Edgel *it, + Edgel *n); + +G_DEFINE_TYPE_WITH_CODE (GimpLineArt, gimp_line_art, GIMP_TYPE_OBJECT, + G_ADD_PRIVATE (GimpLineArt)) + +static guint gimp_line_art_signals[LAST_SIGNAL] = { 0 }; + +static void +gimp_line_art_class_init (GimpLineArtClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + gimp_line_art_signals[COMPUTING_START] = + g_signal_new ("computing-start", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (GimpLineArtClass, computing_start), + NULL, NULL, + gimp_marshal_VOID__VOID, + G_TYPE_NONE, 0); + gimp_line_art_signals[COMPUTING_END] = + g_signal_new ("computing-end", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (GimpLineArtClass, computing_end), + NULL, NULL, + gimp_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + object_class->finalize = gimp_line_art_finalize; + object_class->set_property = gimp_line_art_set_property; + object_class->get_property = gimp_line_art_get_property; + + g_object_class_install_property (object_class, PROP_SELECT_TRANSPARENT, + g_param_spec_boolean ("select-transparent", + _("Select transparent pixels instead of gray ones"), + _("Select transparent pixels instead of gray ones"), + TRUE, + G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE)); + + g_object_class_install_property (object_class, PROP_THRESHOLD, + g_param_spec_double ("threshold", + _("Line art detection threshold"), + _("Threshold to detect contour (higher values will include more pixels)"), + 0.0, 1.0, 0.92, + G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE)); + + g_object_class_install_property (object_class, PROP_MAX_GROW, + g_param_spec_int ("max-grow", + _("Maximum growing size"), + _("Maximum number of pixels grown under the line art"), + 1, 100, 3, + G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE)); + + g_object_class_install_property (object_class, PROP_SPLINE_MAX_LEN, + g_param_spec_int ("spline-max-length", + _("Maximum curved closing length"), + _("Maximum curved length (in pixels) to close the line art"), + 0, 1000, 100, + G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE)); + + g_object_class_install_property (object_class, PROP_SEGMENT_MAX_LEN, + g_param_spec_int ("segment-max-length", + _("Maximum straight closing length"), + _("Maximum straight length (in pixels) to close the line art"), + 0, 1000, 100, + G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE)); +} + +static void +gimp_line_art_init (GimpLineArt *line_art) +{ + line_art->priv = gimp_line_art_get_instance_private (line_art); +} + +static void +gimp_line_art_finalize (GObject *object) +{ + GimpLineArt *line_art = GIMP_LINE_ART (object); + + line_art->priv->frozen = FALSE; + + gimp_line_art_set_input (line_art, NULL); +} + +static void +gimp_line_art_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GimpLineArt *line_art = GIMP_LINE_ART (object); + + switch (property_id) + { + case PROP_SELECT_TRANSPARENT: + if (line_art->priv->select_transparent != g_value_get_boolean (value)) + { + line_art->priv->select_transparent = g_value_get_boolean (value); + gimp_line_art_compute (line_art); + } + break; + case PROP_MAX_GROW: + line_art->priv->max_grow = g_value_get_int (value); + break; + case PROP_THRESHOLD: + if (line_art->priv->threshold != g_value_get_double (value)) + { + line_art->priv->threshold = g_value_get_double (value); + gimp_line_art_compute (line_art); + } + break; + case PROP_SPLINE_MAX_LEN: + if (line_art->priv->spline_max_len != g_value_get_int (value)) + { + line_art->priv->spline_max_len = g_value_get_int (value); + if (line_art->priv->max_len_bound) + line_art->priv->segment_max_len = line_art->priv->spline_max_len; + gimp_line_art_compute (line_art); + } + break; + case PROP_SEGMENT_MAX_LEN: + if (line_art->priv->segment_max_len != g_value_get_int (value)) + { + line_art->priv->segment_max_len = g_value_get_int (value); + if (line_art->priv->max_len_bound) + line_art->priv->spline_max_len = line_art->priv->segment_max_len; + gimp_line_art_compute (line_art); + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gimp_line_art_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GimpLineArt *line_art = GIMP_LINE_ART (object); + + switch (property_id) + { + case PROP_SELECT_TRANSPARENT: + g_value_set_boolean (value, line_art->priv->select_transparent); + break; + case PROP_MAX_GROW: + g_value_set_int (value, line_art->priv->max_grow); + break; + case PROP_THRESHOLD: + g_value_set_double (value, line_art->priv->threshold); + break; + case PROP_SPLINE_MAX_LEN: + g_value_set_int (value, line_art->priv->spline_max_len); + break; + case PROP_SEGMENT_MAX_LEN: + g_value_set_int (value, line_art->priv->segment_max_len); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +/* Public functions */ + +GimpLineArt * +gimp_line_art_new (void) +{ + return g_object_new (GIMP_TYPE_LINE_ART, + NULL); +} + +void +gimp_line_art_bind_gap_length (GimpLineArt *line_art, + gboolean bound) +{ + line_art->priv->max_len_bound = bound; +} + +void +gimp_line_art_set_input (GimpLineArt *line_art, + GimpPickable *pickable) +{ + g_return_if_fail (pickable == NULL || GIMP_IS_VIEWABLE (pickable)); + + if (pickable != line_art->priv->input) + { + if (line_art->priv->input) + g_signal_handlers_disconnect_by_data (line_art->priv->input, line_art); + + g_set_object (&line_art->priv->input, pickable); + + gimp_line_art_compute (line_art); + + if (pickable) + { + g_signal_connect (pickable, "invalidate-preview", + G_CALLBACK (gimp_line_art_input_invalidate_preview), + line_art); + } + } +} + +GimpPickable * +gimp_line_art_get_input (GimpLineArt *line_art) +{ + return line_art->priv->input; +} + +void +gimp_line_art_freeze (GimpLineArt *line_art) +{ + g_return_if_fail (! line_art->priv->frozen); + + line_art->priv->frozen = TRUE; + line_art->priv->compute_after_thaw = FALSE; +} + +void +gimp_line_art_thaw (GimpLineArt *line_art) +{ + g_return_if_fail (line_art->priv->frozen); + + line_art->priv->frozen = FALSE; + if (line_art->priv->compute_after_thaw) + { + gimp_line_art_compute (line_art); + line_art->priv->compute_after_thaw = FALSE; + } +} + +gboolean +gimp_line_art_is_frozen (GimpLineArt *line_art) +{ + return line_art->priv->frozen; +} + +GeglBuffer * +gimp_line_art_get (GimpLineArt *line_art, + gfloat **distmap) +{ + g_return_val_if_fail (line_art->priv->input, NULL); + + if (line_art->priv->async) + { + gimp_waitable_wait (GIMP_WAITABLE (line_art->priv->async)); + } + else if (! line_art->priv->closed) + { + gimp_line_art_compute (line_art); + if (line_art->priv->async) + gimp_waitable_wait (GIMP_WAITABLE (line_art->priv->async)); + } + + g_return_val_if_fail (line_art->priv->closed, NULL); + + if (distmap) + *distmap = line_art->priv->distmap; + + return line_art->priv->closed; +} + +/* Functions for asynchronous computation. */ + +static void +gimp_line_art_compute (GimpLineArt *line_art) +{ + if (line_art->priv->frozen) + { + line_art->priv->compute_after_thaw = TRUE; + return; + } + + if (line_art->priv->async) + { + /* we cancel the async, but don't wait for it to finish, since + * it might take a while to respond. instead gimp_line_art_compute_cb() + * bails if the async has been canceled, to avoid accessing the line art. + */ + g_signal_emit (line_art, gimp_line_art_signals[COMPUTING_END], 0); + gimp_cancelable_cancel (GIMP_CANCELABLE (line_art->priv->async)); + g_clear_object (&line_art->priv->async); + } + + if (line_art->priv->idle_id) + { + g_source_remove (line_art->priv->idle_id); + line_art->priv->idle_id = 0; + } + + g_clear_object (&line_art->priv->closed); + g_clear_pointer (&line_art->priv->distmap, g_free); + + if (line_art->priv->input) + { + /* gimp_line_art_prepare_async() will flush the pickable, which + * may trigger this signal handler, and will leak a line art (as + * line_art->priv->async has not been set yet). + */ + g_signal_handlers_block_by_func ( + line_art->priv->input, + G_CALLBACK (gimp_line_art_input_invalidate_preview), + line_art); + line_art->priv->async = gimp_line_art_prepare_async (line_art, +1); + g_signal_emit (line_art, gimp_line_art_signals[COMPUTING_START], 0); + g_signal_handlers_unblock_by_func ( + line_art->priv->input, + G_CALLBACK (gimp_line_art_input_invalidate_preview), + line_art); + + gimp_async_add_callback_for_object (line_art->priv->async, + (GimpAsyncCallback) gimp_line_art_compute_cb, + line_art, line_art); + } +} + +static void +gimp_line_art_compute_cb (GimpAsync *async, + GimpLineArt *line_art) +{ + if (gimp_async_is_canceled (async)) + return; + + if (gimp_async_is_finished (async)) + { + LineArtResult *result; + + result = gimp_async_get_result (async); + + line_art->priv->closed = g_object_ref (result->closed); + line_art->priv->distmap = result->distmap; + result->distmap = NULL; + g_signal_emit (line_art, gimp_line_art_signals[COMPUTING_END], 0); + } + + g_clear_object (&line_art->priv->async); +} + +static GimpAsync * +gimp_line_art_prepare_async (GimpLineArt *line_art, + gint priority) +{ + GeglBuffer *buffer; + GimpAsync *async; + LineArtData *data; + + g_return_val_if_fail (GIMP_IS_PICKABLE (line_art->priv->input), NULL); + + gimp_pickable_flush (line_art->priv->input); + + buffer = gimp_gegl_buffer_dup ( + gimp_pickable_get_buffer (line_art->priv->input)); + + data = line_art_data_new (buffer, line_art); + + g_object_unref (buffer); + + async = gimp_parallel_run_async_full ( + priority, + (GimpRunAsyncFunc) gimp_line_art_prepare_async_func, + data, (GDestroyNotify) line_art_data_free); + + return async; +} + +static void +gimp_line_art_prepare_async_func (GimpAsync *async, + LineArtData *data) +{ + GeglBuffer *buffer; + GeglBuffer *closed = NULL; + gfloat *distmap = NULL; + gint buffer_x; + gint buffer_y; + gboolean has_alpha; + gboolean select_transparent = FALSE; + + has_alpha = babl_format_has_alpha (gegl_buffer_get_format (data->buffer)); + + if (has_alpha) + { + if (data->select_transparent) + { + /* don't select transparent regions if there are no fully + * transparent pixels. + */ + GeglBufferIterator *gi; + + gi = gegl_buffer_iterator_new (data->buffer, NULL, 0, + babl_format ("A u8"), + GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 3); + while (gegl_buffer_iterator_next (gi)) + { + guint8 *p = (guint8*) gi->items[0].data; + gint k; + + if (gimp_async_is_canceled (async)) + { + gegl_buffer_iterator_stop (gi); + + gimp_async_abort (async); + + line_art_data_free (data); + + return; + } + + for (k = 0; k < gi->length; k++) + { + if (! *p) + { + select_transparent = TRUE; + break; + } + p++; + } + if (select_transparent) + break; + } + if (select_transparent) + gegl_buffer_iterator_stop (gi); + } + } + + buffer = data->buffer; + buffer_x = gegl_buffer_get_x (data->buffer); + buffer_y = gegl_buffer_get_y (data->buffer); + + if (buffer_x != 0 || buffer_y != 0) + { + buffer = g_object_new (GEGL_TYPE_BUFFER, + "source", buffer, + "shift-x", buffer_x, + "shift-y", buffer_y, + NULL); + } + + /* For smart selection, we generate a binarized image with close + * regions, then run a composite selection with no threshold on + * this intermediate buffer. + */ + GIMP_TIMER_START(); + + closed = gimp_line_art_close (buffer, + select_transparent, + data->threshold, + data->spline_max_len, + data->segment_max_len, + /*minimal_lineart_area,*/ + 5, + /*normal_estimate_mask_size,*/ + 5, + /*end_point_rate,*/ + 0.85, + /*spline_max_angle,*/ + 90.0, + /*end_point_connectivity,*/ + 2, + /*spline_roundness,*/ + 1.0, + /*allow_self_intersections,*/ + TRUE, + /*created_regions_significant_area,*/ + 4, + /*created_regions_minimum_area,*/ + 100, + /*small_segments_from_spline_sources,*/ + TRUE, + &distmap, + async); + + GIMP_TIMER_END("close line-art"); + + if (buffer != data->buffer) + g_object_unref (buffer); + + if (! gimp_async_is_stopped (async)) + { + if (buffer_x != 0 || buffer_y != 0) + { + buffer = g_object_new (GEGL_TYPE_BUFFER, + "source", closed, + "shift-x", -buffer_x, + "shift-y", -buffer_y, + NULL); + + g_object_unref (closed); + + closed = buffer; + } + + gimp_async_finish_full (async, + line_art_result_new (closed, distmap), + (GDestroyNotify) line_art_result_free); + } + + line_art_data_free (data); +} + +static LineArtData * +line_art_data_new (GeglBuffer *buffer, + GimpLineArt *line_art) +{ + LineArtData *data = g_slice_new (LineArtData); + + data->buffer = g_object_ref (buffer); + data->select_transparent = line_art->priv->select_transparent; + data->threshold = line_art->priv->threshold; + data->spline_max_len = line_art->priv->spline_max_len; + data->segment_max_len = line_art->priv->segment_max_len; + + return data; +} + +static void +line_art_data_free (LineArtData *data) +{ + g_object_unref (data->buffer); + + g_slice_free (LineArtData, data); +} + +static LineArtResult * +line_art_result_new (GeglBuffer *closed, + gfloat *distmap) +{ + LineArtResult *data; + + data = g_slice_new (LineArtResult); + data->closed = closed; + data->distmap = distmap; + + return data; +} + +static void +line_art_result_free (LineArtResult *data) +{ + g_object_unref (data->closed); + g_clear_pointer (&data->distmap, g_free); + + g_slice_free (LineArtResult, data); +} + +static gboolean +gimp_line_art_idle (GimpLineArt *line_art) +{ + line_art->priv->idle_id = 0; + + gimp_line_art_compute (line_art); + + return G_SOURCE_REMOVE; +} + +static void +gimp_line_art_input_invalidate_preview (GimpViewable *viewable, + GimpLineArt *line_art) +{ + if (! line_art->priv->idle_id) + { + line_art->priv->idle_id = g_idle_add_full ( + GIMP_PRIORITY_VIEWABLE_IDLE, + (GSourceFunc) gimp_line_art_idle, + line_art, NULL); + } +} + +/* All actual computation functions. */ + +/** + * gimp_line_art_close: + * @buffer: the input #GeglBuffer. + * @select_transparent: whether we binarize the alpha channel or the + * luminosity. + * @stroke_threshold: [0-1] threshold value for detecting stroke pixels + * (higher values will detect more stroke pixels). + * @spline_max_length: the maximum length for creating splines between + * end points. + * @segment_max_length: the maximum length for creating segments + * between end points. Unlike splines, segments + * are straight lines. + * @minimal_lineart_area: the minimum size in number pixels for area to + * be considered as line art. + * @normal_estimate_mask_size: + * @end_point_rate: threshold to estimate if a curvature is an end-point + * in [0-1] range value. + * @spline_max_angle: the maximum angle between end point normals for + * creating splines between them. + * @end_point_connectivity: + * @spline_roundness: + * @allow_self_intersections: whether to allow created splines and + * segments to intersect. + * @created_regions_significant_area: + * @created_regions_minimum_area: + * @small_segments_from_spline_sources: + * @closed_distmap: a distance map of the closed line art pixels. + * @async: the #GimpAsync associated with the computation + * + * Creates a binarized version of the strokes of @buffer, detected either + * with luminosity (light means background) or alpha values depending on + * @select_transparent. This binary version of the strokes will have closed + * regions allowing adequate selection of "nearly closed regions". + * This algorithm is meant for digital painting (and in particular on the + * sketch-only step), and therefore will likely produce unexpected results on + * other types of input. + * + * The algorithm is the first step from the research paper "A Fast and + * Efficient Semi-guided Algorithm for Flat Coloring Line-arts", by Sébastian + * Fourey, David Tschumperlé, David Revoy. + * https://hal.archives-ouvertes.fr/hal-01891876 + * + * Returns: a new #GeglBuffer of format "Y u8" representing the + * binarized @line_art. If @lineart_distmap is not #NULL, a + * newly allocated float buffer is returned, which can be used + * for overflowing created masks later. + */ +static GeglBuffer * +gimp_line_art_close (GeglBuffer *buffer, + gboolean select_transparent, + gdouble stroke_threshold, + gint spline_max_length, + gint segment_max_length, + gint minimal_lineart_area, + gint normal_estimate_mask_size, + gfloat end_point_rate, + gfloat spline_max_angle, + gint end_point_connectivity, + gfloat spline_roundness, + gboolean allow_self_intersections, + gint created_regions_significant_area, + gint created_regions_minimum_area, + gboolean small_segments_from_spline_sources, + gfloat **closed_distmap, + GimpAsync *async) +{ + const Babl *gray_format; + GeglBufferIterator *gi; + GeglBuffer *closed = NULL; + GeglBuffer *strokes = NULL; + guchar max_value = 0; + gint width = gegl_buffer_get_width (buffer); + gint height = gegl_buffer_get_height (buffer); + gint i; + + if (select_transparent) + /* Keep alpha channel as gray levels */ + gray_format = babl_format ("A u8"); + else + /* Keep luminance */ + gray_format = babl_format ("Y' u8"); + + /* Transform the line art from any format to gray. */ + strokes = gegl_buffer_new (gegl_buffer_get_extent (buffer), + gray_format); + gimp_gegl_buffer_copy (buffer, NULL, GEGL_ABYSS_NONE, strokes, NULL); + gegl_buffer_set_format (strokes, babl_format ("Y' u8")); + + if (! select_transparent) + { + /* Compute the biggest value */ + gi = gegl_buffer_iterator_new (strokes, NULL, 0, NULL, + GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1); + while (gegl_buffer_iterator_next (gi)) + { + guchar *data = (guchar*) gi->items[0].data; + gint k; + + if (gimp_async_is_canceled (async)) + { + gegl_buffer_iterator_stop (gi); + + gimp_async_abort (async); + + goto end1; + } + + for (k = 0; k < gi->length; k++) + { + if (*data > max_value) + max_value = *data; + data++; + } + } + } + + /* Make the image binary: 1 is stroke, 0 background */ + gi = gegl_buffer_iterator_new (strokes, NULL, 0, NULL, + GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1); + while (gegl_buffer_iterator_next (gi)) + { + guchar *data = (guchar*) gi->items[0].data; + gint k; + + if (gimp_async_is_canceled (async)) + { + gegl_buffer_iterator_stop (gi); + + gimp_async_abort (async); + + goto end1; + } + + for (k = 0; k < gi->length; k++) + { + if (! select_transparent) + /* Negate the value. */ + *data = max_value - *data; + /* Apply a threshold. */ + if (*data > (guchar) (255.0f * (1.0f - stroke_threshold))) + *data = 1; + else + *data = 0; + data++; + } + } + + /* Denoise (remove small connected components) */ + gimp_lineart_denoise (strokes, minimal_lineart_area, async); + if (gimp_async_is_stopped (async)) + goto end1; + + closed = g_object_ref (strokes); + + if (spline_max_length > 0 || segment_max_length > 0) + { + GArray *keypoints = NULL; + GHashTable *visited = NULL; + gfloat *radii = NULL; + gfloat *normals = NULL; + gfloat *curvatures = NULL; + gfloat *smoothed_curvatures = NULL; + gfloat threshold; + gfloat clamped_threshold; + GList *fill_pixels = NULL; + GList *iter; + + normals = g_new0 (gfloat, width * height * 2); + curvatures = g_new0 (gfloat, width * height); + smoothed_curvatures = g_new0 (gfloat, width * height); + + /* Estimate normals & curvature */ + gimp_lineart_compute_normals_curvatures (strokes, normals, curvatures, + smoothed_curvatures, + normal_estimate_mask_size, + async); + if (gimp_async_is_stopped (async)) + goto end2; + + radii = gimp_lineart_estimate_strokes_radii (strokes, async); + if (gimp_async_is_stopped (async)) + goto end2; + threshold = 1.0f - end_point_rate; + clamped_threshold = MAX (0.25f, threshold); + for (i = 0; i < width; i++) + { + gint j; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end2; + } + + for (j = 0; j < height; j++) + { + if (smoothed_curvatures[i + j * width] >= (threshold / MAX (1.0f, radii[i + j * width])) || + curvatures[i + j * width] >= clamped_threshold) + curvatures[i + j * width] = 1.0; + else + curvatures[i + j * width] = 0.0; + } + } + g_clear_pointer (&radii, g_free); + + keypoints = gimp_lineart_curvature_extremums (curvatures, smoothed_curvatures, + width, height, async); + if (gimp_async_is_stopped (async)) + goto end2; + + visited = g_hash_table_new_full ((GHashFunc) visited_hash_fun, + (GEqualFunc) visited_equal_fun, + (GDestroyNotify) g_free, NULL); + + if (spline_max_length > 0) + { + GList *candidates; + SplineCandidate *candidate; + + candidates = gimp_lineart_find_spline_candidates (keypoints, normals, width, + spline_max_length, + spline_max_angle, + async); + if (gimp_async_is_stopped (async)) + goto end3; + + g_object_unref (closed); + closed = gimp_gegl_buffer_dup (strokes); + + /* Draw splines */ + while (candidates) + { + Pixel *p1; + Pixel *p2; + gboolean inserted = FALSE; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end3; + } + + p1 = g_new (Pixel, 1); + p2 = g_new (Pixel, 1); + + candidate = (SplineCandidate *) candidates->data; + p1->x = candidate->p1.x; + p1->y = candidate->p1.y; + p2->x = candidate->p2.x; + p2->y = candidate->p2.y; + + g_free (candidate); + candidates = g_list_delete_link (candidates, candidates); + + if ((! g_hash_table_contains (visited, p1) || + GPOINTER_TO_INT (g_hash_table_lookup (visited, p1)) < end_point_connectivity) && + (! g_hash_table_contains (visited, p2) || + GPOINTER_TO_INT (g_hash_table_lookup (visited, p2)) < end_point_connectivity)) + { + GArray *discrete_curve; + GimpVector2 vect1 = pair2normal (*p1, normals, width); + GimpVector2 vect2 = pair2normal (*p2, normals, width); + gfloat distance = gimp_vector2_length_val (gimp_vector2_sub_val (*p1, *p2)); + gint transitions; + + gimp_vector2_mul (&vect1, distance); + gimp_vector2_mul (&vect1, spline_roundness); + gimp_vector2_mul (&vect2, distance); + gimp_vector2_mul (&vect2, spline_roundness); + + discrete_curve = gimp_lineart_discrete_spline (*p1, vect1, *p2, vect2); + + transitions = allow_self_intersections ? + gimp_number_of_transitions (discrete_curve, strokes) : + gimp_number_of_transitions (discrete_curve, closed); + + if (transitions == 2 && + gimp_line_art_allow_closure (closed, discrete_curve, + &fill_pixels, + created_regions_significant_area, + created_regions_minimum_area)) + { + for (i = 0; i < discrete_curve->len; i++) + { + Pixel p = g_array_index (discrete_curve, Pixel, i); + + if (p.x >= 0 && p.x < gegl_buffer_get_width (closed) && + p.y >= 0 && p.y < gegl_buffer_get_height (closed)) + { + guchar val = 2; + + gegl_buffer_set (closed, GEGL_RECTANGLE ((gint) p.x, (gint) p.y, 1, 1), 0, + NULL, &val, GEGL_AUTO_ROWSTRIDE); + } + } + g_hash_table_replace (visited, p1, + GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p1)) + 1)); + g_hash_table_replace (visited, p2, + GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p2)) + 1)); + inserted = TRUE; + } + g_array_free (discrete_curve, TRUE); + } + if (! inserted) + { + g_free (p1); + g_free (p2); + } + } + + end3: + g_list_free_full (candidates, g_free); + + if (gimp_async_is_stopped (async)) + goto end2; + } + + g_clear_object (&strokes); + + /* Draw straight line segments */ + if (segment_max_length > 0) + { + Pixel *point; + + point = (Pixel *) keypoints->data; + for (i = 0; i < keypoints->len; i++) + { + Pixel *p; + gboolean inserted = FALSE; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end2; + } + + p = g_new (Pixel, 1); + *p = *point; + + if (! g_hash_table_contains (visited, p) || + (small_segments_from_spline_sources && + GPOINTER_TO_INT (g_hash_table_lookup (visited, p)) < end_point_connectivity)) + { + GArray *segment = gimp_lineart_line_segment_until_hit (closed, *point, + pair2normal (*point, normals, width), + segment_max_length); + + if (segment->len && + gimp_line_art_allow_closure (closed, segment, &fill_pixels, + created_regions_significant_area, + created_regions_minimum_area)) + { + gint j; + + for (j = 0; j < segment->len; j++) + { + Pixel p2 = g_array_index (segment, Pixel, j); + guchar val = 2; + + gegl_buffer_set (closed, GEGL_RECTANGLE ((gint) p2.x, (gint) p2.y, 1, 1), 0, + NULL, &val, GEGL_AUTO_ROWSTRIDE); + } + g_hash_table_replace (visited, p, + GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p)) + 1)); + inserted = TRUE; + } + g_array_free (segment, TRUE); + } + if (! inserted) + g_free (p); + point++; + } + } + + for (iter = fill_pixels; iter; iter = iter->next) + { + Pixel *p = iter->data; + gint fill_max = created_regions_significant_area - 1; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end2; + } + + /* XXX A best approach would be to generalize + * gimp_drawable_bucket_fill() to work on any buffer (the code + * is already mostly there) rather than reimplementing a naive + * bucket fill. + * This is mostly a quick'n dirty first implementation which I + * will improve later. + */ + gimp_line_art_simple_fill (closed, (gint) p->x, (gint) p->y, &fill_max); + } + + end2: + g_list_free_full (fill_pixels, g_free); + g_free (normals); + g_free (curvatures); + g_free (smoothed_curvatures); + g_clear_pointer (&radii, g_free); + if (keypoints) + g_array_free (keypoints, TRUE); + g_clear_pointer (&visited, g_hash_table_destroy); + + if (gimp_async_is_stopped (async)) + goto end1; + } + else + { + g_clear_object (&strokes); + } + + if (closed_distmap) + { + GeglNode *graph; + GeglNode *input; + GeglNode *op; + + /* Flooding needs a distance map for closed line art. */ + *closed_distmap = g_new (gfloat, width * height); + + graph = gegl_node_new (); + input = gegl_node_new_child (graph, + "operation", "gegl:buffer-source", + "buffer", closed, + NULL); + op = gegl_node_new_child (graph, + "operation", "gegl:distance-transform", + "metric", GEGL_DISTANCE_METRIC_EUCLIDEAN, + "normalize", FALSE, + NULL); + gegl_node_connect_to (input, "output", + op, "input"); + gegl_node_blit (op, 1.0, gegl_buffer_get_extent (closed), + NULL, *closed_distmap, + GEGL_AUTO_ROWSTRIDE, GEGL_BLIT_DEFAULT); + g_object_unref (graph); + } + + end1: + g_clear_object (&strokes); + + if (gimp_async_is_stopped (async)) + g_clear_object (&closed); + + return closed; +} + +static void +gimp_lineart_denoise (GeglBuffer *buffer, + int minimum_area, + GimpAsync *async) +{ + /* Keep connected regions with significant area. */ + GArray *region; + GQueue *q = g_queue_new (); + gint width = gegl_buffer_get_width (buffer); + gint height = gegl_buffer_get_height (buffer); + gboolean *visited = g_new0 (gboolean, width * height); + gint x, y; + + region = g_array_sized_new (TRUE, TRUE, sizeof (Pixel *), minimum_area); + + for (y = 0; y < height; ++y) + for (x = 0; x < width; ++x) + { + guchar has_stroke; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end; + } + + gegl_buffer_sample (buffer, x, y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[x + y * width]) + { + Pixel *p = g_new (Pixel, 1); + gint regionSize = 0; + + p->x = x; + p->y = y; + + g_queue_push_tail (q, p); + visited[x + y * width] = TRUE; + + while (! g_queue_is_empty (q)) + { + Pixel *p; + gint p2x; + gint p2y; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end; + } + + p = (Pixel *) g_queue_pop_head (q); + + p2x = p->x + 1; + p2y = p->y; + if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height) + { + gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x +p2y * width] = TRUE; + } + } + p2x = p->x - 1; + p2y = p->y; + if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height) + { + gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + } + p2x = p->x; + p2y = p->y - 1; + if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height) + { + gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + } + p2x = p->x; + p2y = p->y + 1; + if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height) + { + gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + } + p2x = p->x + 1; + p2y = p->y + 1; + if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height) + { + gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + } + p2x = p->x - 1; + p2y = p->y - 1; + if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height) + { + gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + } + p2x = p->x - 1; + p2y = p->y + 1; + if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height) + { + gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + } + p2x = p->x + 1; + p2y = p->y - 1; + if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height) + { + gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (has_stroke && ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + } + + ++regionSize; + if (regionSize < minimum_area) + g_array_append_val (region, *p); + g_free (p); + } + if (regionSize < minimum_area) + { + Pixel *pixel = (Pixel *) region->data; + gint i = 0; + + for (; i < region->len; i++) + { + guchar val = 0; + gegl_buffer_set (buffer, GEGL_RECTANGLE (pixel->x, pixel->y, 1, 1), 0, + NULL, &val, GEGL_AUTO_ROWSTRIDE); + pixel++; + } + } + g_array_remove_range (region, 0, region->len); + } + } + + end: + g_array_free (region, TRUE); + g_queue_free_full (q, g_free); + g_free (visited); +} + +static void +gimp_lineart_compute_normals_curvatures (GeglBuffer *mask, + gfloat *normals, + gfloat *curvatures, + gfloat *smoothed_curvatures, + int normal_estimate_mask_size, + GimpAsync *async) +{ + gfloat *edgels_curvatures = NULL; + gfloat *smoothed_curvature; + GArray *es = NULL; + Edgel **e; + gint width = gegl_buffer_get_width (mask); + + es = gimp_edgelset_new (mask, async); + if (gimp_async_is_stopped (async)) + goto end; + + e = (Edgel **) es->data; + + gimp_edgelset_smooth_normals (es, normal_estimate_mask_size, async); + if (gimp_async_is_stopped (async)) + goto end; + + gimp_edgelset_compute_curvature (es, async); + if (gimp_async_is_stopped (async)) + goto end; + + while (*e) + { + const float curvature = ((*e)->curvature > 0.0f) ? (*e)->curvature : 0.0f; + const float w = MAX (1e-8f, curvature * curvature); + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end; + } + + normals[((*e)->x + (*e)->y * width) * 2] += w * (*e)->x_normal; + normals[((*e)->x + (*e)->y * width) * 2 + 1] += w * (*e)->y_normal; + curvatures[(*e)->x + (*e)->y * width] = MAX (curvature, + curvatures[(*e)->x + (*e)->y * width]); + e++; + } + for (int y = 0; y < gegl_buffer_get_height (mask); ++y) + { + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end; + } + + for (int x = 0; x < gegl_buffer_get_width (mask); ++x) + { + const float _angle = atan2f (normals[(x + y * width) * 2 + 1], + normals[(x + y * width) * 2]); + normals[(x + y * width) * 2] = cosf (_angle); + normals[(x + y * width) * 2 + 1] = sinf (_angle); + } + } + + /* Smooth curvatures on edgels, then take maximum on each pixel. */ + edgels_curvatures = gimp_lineart_get_smooth_curvatures (es, async); + if (gimp_async_is_stopped (async)) + goto end; + + smoothed_curvature = edgels_curvatures; + + e = (Edgel **) es->data; + while (*e) + { + gfloat *pixel_curvature = &smoothed_curvatures[(*e)->x + (*e)->y * width]; + + if (*pixel_curvature < *smoothed_curvature) + *pixel_curvature = *smoothed_curvature; + + ++smoothed_curvature; + e++; + } + + end: + g_free (edgels_curvatures); + + if (es) + g_array_free (es, TRUE); +} + +static gfloat * +gimp_lineart_get_smooth_curvatures (GArray *edgelset, + GimpAsync *async) +{ + Edgel **e; + gfloat *smoothed_curvatures = g_new0 (gfloat, edgelset->len); + gfloat weights[9]; + gfloat smoothed_curvature; + gfloat weights_sum; + gint idx = 0; + + weights[0] = 1.0f; + for (int i = 1; i <= 8; ++i) + weights[i] = expf (-(i * i) / 30.0f); + + e = (Edgel **) edgelset->data; + while (*e) + { + Edgel *edgel_before = g_array_index (edgelset, Edgel*, (*e)->previous); + Edgel *edgel_after = g_array_index (edgelset, Edgel*, (*e)->next); + int n = 5; + int i = 1; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + g_free (smoothed_curvatures); + + return NULL; + } + + smoothed_curvature = (*e)->curvature; + weights_sum = weights[0]; + while (n-- && (edgel_after != edgel_before)) + { + smoothed_curvature += weights[i] * edgel_before->curvature; + smoothed_curvature += weights[i] * edgel_after->curvature; + edgel_before = g_array_index (edgelset, Edgel*, edgel_before->previous); + edgel_after = g_array_index (edgelset, Edgel*, edgel_after->next); + weights_sum += 2 * weights[i]; + i++; + } + smoothed_curvature /= weights_sum; + smoothed_curvatures[idx++] = smoothed_curvature; + + e++; + } + + return smoothed_curvatures; +} + +/** + * Keep one pixel per connected component of curvature extremums. + */ +static GArray * +gimp_lineart_curvature_extremums (gfloat *curvatures, + gfloat *smoothed_curvatures, + gint width, + gint height, + GimpAsync *async) +{ + gboolean *visited = g_new0 (gboolean, width * height); + GQueue *q = g_queue_new (); + GArray *max_positions; + + max_positions = g_array_new (FALSE, TRUE, sizeof (Pixel)); + + for (int y = 0; y < height; ++y) + { + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end; + } + + for (int x = 0; x < width; ++x) + { + if ((curvatures[x + y * width] > 0.0) && ! visited[x + y * width]) + { + Pixel *p = g_new (Pixel, 1); + Pixel max_smoothed_curvature_pixel; + Pixel max_raw_curvature_pixel; + gfloat max_smoothed_curvature; + gfloat max_raw_curvature; + + max_smoothed_curvature_pixel = gimp_vector2_new (-1.0, -1.0); + max_smoothed_curvature = 0.0f; + + max_raw_curvature_pixel = gimp_vector2_new (x, y); + max_raw_curvature = curvatures[x + y * width]; + + p->x = x; + p->y = y; + g_queue_push_tail (q, p); + visited[x + y * width] = TRUE; + + while (! g_queue_is_empty (q)) + { + gfloat sc; + gfloat c; + gint p2x; + gint p2y; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + goto end; + } + + p = (Pixel *) g_queue_pop_head (q); + sc = smoothed_curvatures[(gint) p->x + (gint) p->y * width]; + c = curvatures[(gint) p->x + (gint) p->y * width]; + + curvatures[(gint) p->x + (gint) p->y * width] = 0.0f; + + p2x = (gint) p->x + 1; + p2y = (gint) p->y; + if (p2x >= 0 && p2x < width && + p2y >= 0 && p2y < height && + curvatures[p2x + p2y * width] > 0.0 && + ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + + p2x = p->x - 1; + p2y = p->y; + if (p2x >= 0 && p2x < width && + p2y >= 0 && p2y < height && + curvatures[p2x + p2y * width] > 0.0 && + ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + + p2x = p->x; + p2y = p->y - 1; + if (p2x >= 0 && p2x < width && + p2y >= 0 && p2y < height && + curvatures[p2x + p2y * width] > 0.0 && + ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + + p2x = p->x; + p2y = p->y + 1; + if (p2x >= 0 && p2x < width && + p2y >= 0 && p2y < height && + curvatures[p2x + p2y * width] > 0.0 && + ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + + p2x = p->x + 1; + p2y = p->y + 1; + if (p2x >= 0 && p2x < width && + p2y >= 0 && p2y < height && + curvatures[p2x + p2y * width] > 0.0 && + ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + + p2x = p->x - 1; + p2y = p->y - 1; + if (p2x >= 0 && p2x < width && + p2y >= 0 && p2y < height && + curvatures[p2x + p2y * width] > 0.0 && + ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + + p2x = p->x - 1; + p2y = p->y + 1; + if (p2x >= 0 && p2x < width && + p2y >= 0 && p2y < height && + curvatures[p2x + p2y * width] > 0.0 && + ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + + p2x = p->x + 1; + p2y = p->y - 1; + if (p2x >= 0 && p2x < width && + p2y >= 0 && p2y < height && + curvatures[p2x + p2y * width] > 0.0 && + ! visited[p2x + p2y * width]) + { + Pixel *p2 = g_new (Pixel, 1); + + p2->x = p2x; + p2->y = p2y; + g_queue_push_tail (q, p2); + visited[p2x + p2y * width] = TRUE; + } + + if (sc > max_smoothed_curvature) + { + max_smoothed_curvature_pixel = *p; + max_smoothed_curvature = sc; + } + if (c > max_raw_curvature) + { + max_raw_curvature_pixel = *p; + max_raw_curvature = c; + } + g_free (p); + } + if (max_smoothed_curvature > 0.0f) + { + curvatures[(gint) max_smoothed_curvature_pixel.x + (gint) max_smoothed_curvature_pixel.y * width] = max_smoothed_curvature; + g_array_append_val (max_positions, max_smoothed_curvature_pixel); + } + else + { + curvatures[(gint) max_raw_curvature_pixel.x + (gint) max_raw_curvature_pixel.y * width] = max_raw_curvature; + g_array_append_val (max_positions, max_raw_curvature_pixel); + } + } + } + } + + end: + g_queue_free_full (q, g_free); + g_free (visited); + + if (gimp_async_is_stopped (async)) + { + g_array_free (max_positions, TRUE); + max_positions = NULL; + } + + return max_positions; +} + +static gint +gimp_spline_candidate_cmp (const SplineCandidate *a, + const SplineCandidate *b, + gpointer user_data) +{ + /* This comparison actually returns the opposite of common comparison + * functions on purpose, as we want the first element on the list to + * be the "bigger". + */ + if (a->quality < b->quality) + return 1; + else if (a->quality > b->quality) + return -1; + else + return 0; +} + +static GList * +gimp_lineart_find_spline_candidates (GArray *max_positions, + gfloat *normals, + gint width, + gint distance_threshold, + gfloat max_angle_deg, + GimpAsync *async) +{ + GList *candidates = NULL; + const float CosMin = cosf (M_PI * (max_angle_deg / 180.0)); + gint i; + + for (i = 0; i < max_positions->len; i++) + { + Pixel p1 = g_array_index (max_positions, Pixel, i); + gint j; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + g_list_free_full (candidates, g_free); + + return NULL; + } + + for (j = i + 1; j < max_positions->len; j++) + { + Pixel p2 = g_array_index (max_positions, Pixel, j); + const float distance = gimp_vector2_length_val (gimp_vector2_sub_val (p1, p2)); + + if (distance <= distance_threshold) + { + GimpVector2 normalP1; + GimpVector2 normalP2; + GimpVector2 p1f; + GimpVector2 p2f; + GimpVector2 p1p2; + float cosN; + float qualityA; + float qualityB; + float qualityC; + float quality; + + normalP1 = gimp_vector2_new (normals[((gint) p1.x + (gint) p1.y * width) * 2], + normals[((gint) p1.x + (gint) p1.y * width) * 2 + 1]); + normalP2 = gimp_vector2_new (normals[((gint) p2.x + (gint) p2.y * width) * 2], + normals[((gint) p2.x + (gint) p2.y * width) * 2 + 1]); + p1f = gimp_vector2_new (p1.x, p1.y); + p2f = gimp_vector2_new (p2.x, p2.y); + p1p2 = gimp_vector2_sub_val (p2f, p1f); + + cosN = gimp_vector2_inner_product_val (normalP1, (gimp_vector2_neg_val (normalP2))); + qualityA = MAX (0.0f, 1 - distance / distance_threshold); + qualityB = MAX (0.0f, + (float) (gimp_vector2_inner_product_val (normalP1, p1p2) - gimp_vector2_inner_product_val (normalP2, p1p2)) / + distance); + qualityC = MAX (0.0f, cosN - CosMin); + quality = qualityA * qualityB * qualityC; + if (quality > 0) + { + SplineCandidate *candidate = g_new (SplineCandidate, 1); + + candidate->p1 = p1; + candidate->p2 = p2; + candidate->quality = quality; + + candidates = g_list_insert_sorted_with_data (candidates, candidate, + (GCompareDataFunc) gimp_spline_candidate_cmp, + NULL); + } + } + } + } + return candidates; +} + +static GArray * +gimp_lineart_discrete_spline (Pixel p0, + GimpVector2 n0, + Pixel p1, + GimpVector2 n1) +{ + GArray *points = g_array_new (FALSE, TRUE, sizeof (Pixel)); + const double a0 = 2 * p0.x - 2 * p1.x + n0.x - n1.x; + const double b0 = -3 * p0.x + 3 * p1.x - 2 * n0.x + n1.x; + const double c0 = n0.x; + const double d0 = p0.x; + const double a1 = 2 * p0.y - 2 * p1.y + n0.y - n1.y; + const double b1 = -3 * p0.y + 3 * p1.y - 2 * n0.y + n1.y; + const double c1 = n0.y; + const double d1 = p0.y; + + double t = 0.0; + const double dtMin = 1.0 / MAX (fabs (p0.x - p1.x), fabs (p0.y - p1.y)); + Pixel point = gimp_vector2_new ((gint) round (d0), (gint) round (d1)); + + g_array_append_val (points, point); + + while (t <= 1.0) + { + const double t2 = t * t; + const double t3 = t * t2; + double dx; + double dy; + Pixel p = gimp_vector2_new ((gint) round (a0 * t3 + b0 * t2 + c0 * t + d0), + (gint) round (a1 * t3 + b1 * t2 + c1 * t + d1)); + + /* create gimp_vector2_neq () ? */ + if (g_array_index (points, Pixel, points->len - 1).x != p.x || + g_array_index (points, Pixel, points->len - 1).y != p.y) + { + g_array_append_val (points, p); + } + dx = fabs (3 * a0 * t * t + 2 * b0 * t + c0) + 1e-8; + dy = fabs (3 * a1 * t * t + 2 * b1 * t + c1) + 1e-8; + t += MIN (dtMin, 0.75 / MAX (dx, dy)); + } + if (g_array_index (points, Pixel, points->len - 1).x != p1.x || + g_array_index (points, Pixel, points->len - 1).y != p1.y) + { + g_array_append_val (points, p1); + } + return points; +} + +static gint +gimp_number_of_transitions (GArray *pixels, + GeglBuffer *buffer) +{ + int result = 0; + + if (pixels->len > 0) + { + Pixel it = g_array_index (pixels, Pixel, 0); + guchar value; + gboolean previous; + gint i; + + gegl_buffer_sample (buffer, (gint) it.x, (gint) it.y, NULL, &value, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + previous = (gboolean) value; + + /* Starts at the second element. */ + for (i = 1; i < pixels->len; i++) + { + it = g_array_index (pixels, Pixel, i); + + gegl_buffer_sample (buffer, (gint) it.x, (gint) it.y, NULL, &value, NULL, + GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + result += ((gboolean) value != previous); + previous = (gboolean) value; + } + } + + return result; +} + +/** + * gimp_line_art_allow_closure: + * @mask: the current state of line art closure. + * @pixels: the pixels of a candidate closure (spline or segment). + * @fill_pixels: #GList of unsignificant pixels to bucket fill. + * @significant_size: number of pixels for area to be considered + * "significant". + * @minimum_size: number of pixels for area to be allowed. + * + * Checks whether adding the set of points @pixels to @mask will create + * 4-connected background regions whose size (i.e. number of pixels) + * will be below @minimum_size. If it creates such small areas, the + * function will refuse this candidate spline/segment, with the + * exception of very small areas under @significant_size. These + * micro-area are considered "unsignificant" and accepted (because they + * can be created in some conditions, for instance when created curves + * cross or start from a same endpoint), and one pixel for each + * micro-area will be added to @fill_pixels to be later filled along + * with the candidate pixels. + * + * Returns: #TRUE if @pixels should be added to @mask, #FALSE otherwise. + */ +static gboolean +gimp_line_art_allow_closure (GeglBuffer *mask, + GArray *pixels, + GList **fill_pixels, + int significant_size, + int minimum_size) +{ + /* A theorem from the paper is that a zone with more than + * `2 * (@minimum_size - 1)` edgels (border pixels) will have more + * than @minimum_size pixels. + * Since we are following the edges of the area, we can therefore stop + * earlier if we reach this number of edgels. + */ + const glong max_edgel_count = 2 * minimum_size; + + Pixel *p = (Pixel*) pixels->data; + GList *fp = NULL; + gint i; + + /* Mark pixels */ + for (i = 0; i < pixels->len; i++) + { + if (p->x >= 0 && p->x < gegl_buffer_get_width (mask) && + p->y >= 0 && p->y < gegl_buffer_get_height (mask)) + { + guchar val; + + gegl_buffer_sample (mask, (gint) p->x, (gint) p->y, NULL, &val, + NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + val = val ? 3 : 2; + gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p->x, (gint) p->y, 1, 1), 0, + NULL, &val, GEGL_AUTO_ROWSTRIDE); + } + p++; + } + + for (i = 0; i < pixels->len; i++) + { + Pixel p = g_array_index (pixels, Pixel, i); + + for (int direction = 0; direction < 4; ++direction) + { + if (p.x >= 0 && p.x < gegl_buffer_get_width (mask) && + p.y >= 0 && p.y < gegl_buffer_get_height (mask) && + border_in_direction (mask, p, direction)) + { + Edgel e; + guchar val; + glong count; + glong area; + + gegl_buffer_sample (mask, (gint) p.x, (gint) p.y, NULL, &val, + NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if ((gboolean) (val & (4 << direction))) + continue; + + gimp_edgel_init (&e); + e.x = p.x; + e.y = p.y; + e.direction = direction; + + count = gimp_edgel_track_mark (mask, e, max_edgel_count); + if ((count != -1) && (count <= max_edgel_count)) + { + area = gimp_edgel_region_area (mask, e); + + if (area >= significant_size && area < minimum_size) + { + gint j; + + /* Remove marks */ + for (j = 0; j < pixels->len; j++) + { + Pixel p2 = g_array_index (pixels, Pixel, j); + + if (p2.x >= 0 && p2.x < gegl_buffer_get_width (mask) && + p2.y >= 0 && p2.y < gegl_buffer_get_height (mask)) + { + guchar val; + + gegl_buffer_sample (mask, (gint) p2.x, (gint) p2.y, NULL, &val, + NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + val &= 1; + gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p2.x, (gint) p2.y, 1, 1), 0, + NULL, &val, GEGL_AUTO_ROWSTRIDE); + } + } + g_list_free_full (fp, g_free); + + return FALSE; + } + else if (area > 0 && area < significant_size) + { + Pixel *np = g_new (Pixel, 1); + + np->x = direction == XPlusDirection ? p.x + 1 : (direction == XMinusDirection ? p.x - 1 : p.x); + np->y = direction == YPlusDirection ? p.y + 1 : (direction == YMinusDirection ? p.y - 1 : p.y); + + if (np->x >= 0 && np->x < gegl_buffer_get_width (mask) && + np->y >= 0 && np->y < gegl_buffer_get_height (mask)) + fp = g_list_prepend (fp, np); + else + g_free (np); + } + } + } + } + } + + *fill_pixels = g_list_concat (*fill_pixels, fp); + /* Remove marks */ + for (i = 0; i < pixels->len; i++) + { + Pixel p = g_array_index (pixels, Pixel, i); + + if (p.x >= 0 && p.x < gegl_buffer_get_width (mask) && + p.y >= 0 && p.y < gegl_buffer_get_height (mask)) + { + guchar val; + + gegl_buffer_sample (mask, (gint) p.x, (gint) p.y, NULL, &val, + NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + val &= 1; + gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p.x, (gint) p.y, 1, 1), 0, + NULL, &val, GEGL_AUTO_ROWSTRIDE); + } + } + return TRUE; +} + +static GArray * +gimp_lineart_line_segment_until_hit (const GeglBuffer *mask, + Pixel start, + GimpVector2 direction, + int size) +{ + GeglBuffer *buffer = (GeglBuffer *) mask; + gboolean out = FALSE; + GArray *points = g_array_new (FALSE, TRUE, sizeof (Pixel)); + int tmax; + GimpVector2 p0 = gimp_vector2_new (start.x, start.y); + + gimp_vector2_mul (&direction, (gdouble) size); + direction.x = round (direction.x); + direction.y = round (direction.y); + + tmax = MAX (abs ((int) direction.x), abs ((int) direction.y)); + + for (int t = 0; t <= tmax; ++t) + { + GimpVector2 v = gimp_vector2_add_val (p0, gimp_vector2_mul_val (direction, (float)t / tmax)); + Pixel p; + + p.x = (gint) round (v.x); + p.y = (gint) round (v.y); + if (p.x >= 0 && p.x < gegl_buffer_get_width (buffer) && + p.y >= 0 && p.y < gegl_buffer_get_height (buffer)) + { + guchar val; + gegl_buffer_sample (buffer, p.x, p.y, NULL, &val, + NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (out && val) + { + return points; + } + out = ! val; + } + else if (out) + { + return points; + } + else + { + g_array_free (points, TRUE); + return g_array_new (FALSE, TRUE, sizeof (Pixel)); + } + g_array_append_val (points, p); + } + + g_array_free (points, TRUE); + return g_array_new (FALSE, TRUE, sizeof (Pixel)); +} + +static gfloat * +gimp_lineart_estimate_strokes_radii (GeglBuffer *mask, + GimpAsync *async) +{ + GeglBufferIterator *gi; + gfloat *dist; + gfloat *thickness; + GeglNode *graph; + GeglNode *input; + GeglNode *op; + gint width = gegl_buffer_get_width (mask); + gint height = gegl_buffer_get_height (mask); + + /* Compute a distance map for the line art. */ + dist = g_new (gfloat, width * height); + + graph = gegl_node_new (); + input = gegl_node_new_child (graph, + "operation", "gegl:buffer-source", + "buffer", mask, + NULL); + op = gegl_node_new_child (graph, + "operation", "gegl:distance-transform", + "metric", GEGL_DISTANCE_METRIC_EUCLIDEAN, + "normalize", FALSE, + NULL); + gegl_node_connect_to (input, "output", op, "input"); + gegl_node_blit (op, 1.0, gegl_buffer_get_extent (mask), + NULL, dist, GEGL_AUTO_ROWSTRIDE, GEGL_BLIT_DEFAULT); + g_object_unref (graph); + + thickness = g_new0 (gfloat, width * height); + gi = gegl_buffer_iterator_new (mask, NULL, 0, NULL, + GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1); + while (gegl_buffer_iterator_next (gi)) + { + guint8 *m = (guint8*) gi->items[0].data; + gint startx = gi->items[0].roi.x; + gint starty = gi->items[0].roi.y; + gint endy = starty + gi->items[0].roi.height; + gint endx = startx + gi->items[0].roi.width; + gint x; + gint y; + + if (gimp_async_is_canceled (async)) + { + gegl_buffer_iterator_stop (gi); + + gimp_async_abort (async); + + goto end; + } + + for (y = starty; y < endy; y++) + for (x = startx; x < endx; x++) + { + if (*m && dist[x + y * width] == 1.0) + { + gint dx = x; + gint dy = y; + gfloat d = 1.0; + gfloat nd; + gboolean neighbour_thicker = TRUE; + + while (neighbour_thicker) + { + gint px = dx - 1; + gint py = dy - 1; + gint nx = dx + 1; + gint ny = dy + 1; + + neighbour_thicker = FALSE; + if (px >= 0) + { + if ((nd = dist[px + dy * width]) > d) + { + d = nd; + dx = px; + neighbour_thicker = TRUE; + continue; + } + if (py >= 0 && (nd = dist[px + py * width]) > d) + { + d = nd; + dx = px; + dy = py; + neighbour_thicker = TRUE; + continue; + } + if (ny < height && (nd = dist[px + ny * width]) > d) + { + d = nd; + dx = px; + dy = ny; + neighbour_thicker = TRUE; + continue; + } + } + if (nx < width) + { + if ((nd = dist[nx + dy * width]) > d) + { + d = nd; + dx = nx; + neighbour_thicker = TRUE; + continue; + } + if (py >= 0 && (nd = dist[nx + py * width]) > d) + { + d = nd; + dx = nx; + dy = py; + neighbour_thicker = TRUE; + continue; + } + if (ny < height && (nd = dist[nx + ny * width]) > d) + { + d = nd; + dx = nx; + dy = ny; + neighbour_thicker = TRUE; + continue; + } + } + if (py > 0 && (nd = dist[dx + py * width]) > d) + { + d = nd; + dy = py; + neighbour_thicker = TRUE; + continue; + } + if (ny < height && (nd = dist[dx + ny * width]) > d) + { + d = nd; + dy = ny; + neighbour_thicker = TRUE; + continue; + } + } + thickness[(gint) x + (gint) y * width] = d; + } + m++; + } + } + + end: + g_free (dist); + + if (gimp_async_is_stopped (async)) + g_clear_pointer (&thickness, g_free); + + return thickness; +} + +static void +gimp_line_art_simple_fill (GeglBuffer *buffer, + gint x, + gint y, + gint *counter) +{ + guchar val; + + if (x < 0 || x >= gegl_buffer_get_width (buffer) || + y < 0 || y >= gegl_buffer_get_height (buffer) || + *counter <= 0) + return; + + gegl_buffer_sample (buffer, x, y, NULL, &val, + NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + + if (! val) + { + val = 1; + gegl_buffer_set (buffer, GEGL_RECTANGLE (x, y, 1, 1), 0, + NULL, &val, GEGL_AUTO_ROWSTRIDE); + (*counter)--; + gimp_line_art_simple_fill (buffer, x + 1, y, counter); + gimp_line_art_simple_fill (buffer, x - 1, y, counter); + gimp_line_art_simple_fill (buffer, x, y + 1, counter); + gimp_line_art_simple_fill (buffer, x, y - 1, counter); + } +} + +static guint +visited_hash_fun (Pixel *key) +{ + /* Cantor pairing function. */ + return (key->x + key->y) * (key->x + key->y + 1) / 2 + key->y; +} + +static gboolean +visited_equal_fun (Pixel *e1, + Pixel *e2) +{ + return (e1->x == e2->x && e1->y == e2->y); +} + +static inline gboolean +border_in_direction (GeglBuffer *mask, + Pixel p, + int direction) +{ + gint px = (gint) p.x + DeltaX[direction]; + gint py = (gint) p.y + DeltaY[direction]; + + if (px >= 0 && px < gegl_buffer_get_width (mask) && + py >= 0 && py < gegl_buffer_get_height (mask)) + { + guchar val; + + gegl_buffer_sample (mask, px, py, NULL, &val, + NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + return ! ((gboolean) val); + } + return TRUE; +} + +static inline GimpVector2 +pair2normal (Pixel p, + gfloat *normals, + gint width) +{ + return gimp_vector2_new (normals[((gint) p.x + (gint) p.y * width) * 2], + normals[((gint) p.x + (gint) p.y * width) * 2 + 1]); +} +/* Edgel functions */ + +static Edgel * +gimp_edgel_new (int x, + int y, + Direction direction) +{ + Edgel *edgel = g_new (Edgel, 1); + + edgel->x = x; + edgel->y = y; + edgel->direction = direction; + + gimp_edgel_init (edgel); + + return edgel; +} + +static void +gimp_edgel_init (Edgel *edgel) +{ + edgel->x_normal = 0; + edgel->y_normal = 0; + edgel->curvature = 0; + edgel->next = edgel->previous = G_MAXUINT; +} + +static void +gimp_edgel_clear (Edgel **edgel) +{ + g_clear_pointer (edgel, g_free); +} + +static int +gimp_edgel_cmp (const Edgel* e1, + const Edgel* e2) +{ + gimp_assert (e1 && e2); + + if ((e1->x == e2->x) && (e1->y == e2->y) && + (e1->direction == e2->direction)) + return 0; + else if ((e1->y < e2->y) || (e1->y == e2->y && e1->x < e2->x) || + (e1->y == e2->y && e1->x == e2->x && e1->direction < e2->direction)) + return -1; + else + return 1; +} + +static guint +edgel2index_hash_fun (Edgel *key) +{ + /* Cantor pairing function. + * Was not sure how to use the direction though. :-/ + */ + return (key->x + key->y) * (key->x + key->y + 1) / 2 + key->y * key->direction; +} + +static gboolean +edgel2index_equal_fun (Edgel *e1, + Edgel *e2) +{ + return (e1->x == e2->x && e1->y == e2->y && + e1->direction == e2->direction); +} + +/** + * @mask; + * @edgel: + * @size_limit: + * + * Track a border, marking inner pixels with a bit corresponding to the + * edgel traversed (4 << direction) for direction in {0,1,2,3}. + * Stop tracking after @size_limit edgels have been visited. + * + * Returns: Number of visited edgels, or -1 if an already visited edgel + * has been encountered. + */ +static glong +gimp_edgel_track_mark (GeglBuffer *mask, + Edgel edgel, + long size_limit) +{ + Edgel start = edgel; + long count = 1; + + do + { + guchar val; + + gimp_edgelset_next8 (mask, &edgel, &edgel); + gegl_buffer_sample (mask, edgel.x, edgel.y, NULL, &val, + NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE); + if (val & 2) + { + /* Only mark pixels of the spline/segment */ + if (val & (4 << edgel.direction)) + return -1; + + /* Mark edgel in pixel (1 == In Mask, 2 == Spline/Segment) */ + val |= (4 << edgel.direction); + gegl_buffer_set (mask, GEGL_RECTANGLE (edgel.x, edgel.y, 1, 1), 0, + NULL, &val, GEGL_AUTO_ROWSTRIDE); + } + if (gimp_edgel_cmp (&edgel, &start) != 0) + ++count; + } + while (gimp_edgel_cmp (&edgel, &start) != 0 && count <= size_limit); + + return count; +} + +/** + * gimp_edgel_region_area: + * @mask: current state of closed line art buffer. + * @start_edgel: edgel to follow. + * + * Follows a line border, starting from @start_edgel to compute the area + * enclosed by this border. + * Unfortunately this may return a negative area when the line does not + * close a zone. In this case, there is an uncertaincy on the size of + * the created zone, and we should consider it a big size. + * + * Returns: the area enclosed by the followed line, or a negative value + * if the zone is not closed (hence actual area unknown). + */ +static glong +gimp_edgel_region_area (const GeglBuffer *mask, + Edgel start_edgel) +{ + Edgel edgel = start_edgel; + glong area = 0; + + do + { + if (edgel.direction == XPlusDirection) + area -= edgel.x; + else if (edgel.direction == XMinusDirection) + area += edgel.x - 1; + + gimp_edgelset_next8 (mask, &edgel, &edgel); + } + while (gimp_edgel_cmp (&edgel, &start_edgel) != 0); + + return area; +} + +/* Edgel sets */ + +static GArray * +gimp_edgelset_new (GeglBuffer *buffer, + GimpAsync *async) +{ + GeglBufferIterator *gi; + GArray *set; + GHashTable *edgel2index; + gint width = gegl_buffer_get_width (buffer); + gint height = gegl_buffer_get_height (buffer); + + set = g_array_new (TRUE, TRUE, sizeof (Edgel *)); + g_array_set_clear_func (set, (GDestroyNotify) gimp_edgel_clear); + + if (width <= 1 || height <= 1) + return set; + + edgel2index = g_hash_table_new ((GHashFunc) edgel2index_hash_fun, + (GEqualFunc) edgel2index_equal_fun); + + gi = gegl_buffer_iterator_new (buffer, GEGL_RECTANGLE (0, 0, width, height), + 0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 5); + gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (0, -1, width, height), + 0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE); + gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (0, 1, width, height), + 0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE); + gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (-1, 0, width, height), + 0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE); + gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (1, 0, width, height), + 0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE); + while (gegl_buffer_iterator_next (gi)) + { + guint8 *p = (guint8*) gi->items[0].data; + guint8 *prevy = (guint8*) gi->items[1].data; + guint8 *nexty = (guint8*) gi->items[2].data; + guint8 *prevx = (guint8*) gi->items[3].data; + guint8 *nextx = (guint8*) gi->items[4].data; + gint startx = gi->items[0].roi.x; + gint starty = gi->items[0].roi.y; + gint endy = starty + gi->items[0].roi.height; + gint endx = startx + gi->items[0].roi.width; + gint x; + gint y; + + if (gimp_async_is_canceled (async)) + { + gegl_buffer_iterator_stop (gi); + + gimp_async_abort (async); + + goto end; + } + + for (y = starty; y < endy; y++) + for (x = startx; x < endx; x++) + { + if (*(p++)) + { + if (! *prevy) + gimp_edgelset_add (set, x, y, YMinusDirection, edgel2index); + if (! *nexty) + gimp_edgelset_add (set, x, y, YPlusDirection, edgel2index); + if (! *prevx) + gimp_edgelset_add (set, x, y, XMinusDirection, edgel2index); + if (! *nextx) + gimp_edgelset_add (set, x, y, XPlusDirection, edgel2index); + } + prevy++; + nexty++; + prevx++; + nextx++; + } + } + + gimp_edgelset_build_graph (set, buffer, edgel2index, async); + if (gimp_async_is_stopped (async)) + goto end; + + gimp_edgelset_init_normals (set); + + end: + g_hash_table_destroy (edgel2index); + + if (gimp_async_is_stopped (async)) + { + g_array_free (set, TRUE); + set = NULL; + } + + return set; +} + +static void +gimp_edgelset_add (GArray *set, + int x, + int y, + Direction direction, + GHashTable *edgel2index) +{ + Edgel *edgel = gimp_edgel_new (x, y, direction); + unsigned long position = set->len; + + g_array_append_val (set, edgel); + g_hash_table_insert (edgel2index, edgel, GUINT_TO_POINTER (position)); +} + +static void +gimp_edgelset_init_normals (GArray *set) +{ + Edgel **e = (Edgel**) set->data; + + while (*e) + { + GimpVector2 n = Direction2Normal[(*e)->direction]; + + (*e)->x_normal = n.x; + (*e)->y_normal = n.y; + e++; + } +} + +static void +gimp_edgelset_smooth_normals (GArray *set, + int mask_size, + GimpAsync *async) +{ + const gfloat sigma = mask_size * 0.775; + const gfloat den = 2 * sigma * sigma; + gfloat weights[65]; + GimpVector2 smoothed_normal; + gint i; + + gimp_assert (mask_size <= 65); + + weights[0] = 1.0f; + for (int i = 1; i <= mask_size; ++i) + weights[i] = expf (-(i * i) / den); + + for (i = 0; i < set->len; i++) + { + Edgel *it = g_array_index (set, Edgel*, i); + Edgel *edgel_before = g_array_index (set, Edgel*, it->previous); + Edgel *edgel_after = g_array_index (set, Edgel*, it->next); + int n = mask_size; + int i = 1; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + return; + } + + smoothed_normal = Direction2Normal[it->direction]; + while (n-- && (edgel_after != edgel_before)) + { + smoothed_normal = gimp_vector2_add_val (smoothed_normal, + gimp_vector2_mul_val (Direction2Normal[edgel_before->direction], weights[i])); + smoothed_normal = gimp_vector2_add_val (smoothed_normal, + gimp_vector2_mul_val (Direction2Normal[edgel_after->direction], weights[i])); + edgel_before = g_array_index (set, Edgel *, edgel_before->previous); + edgel_after = g_array_index (set, Edgel *, edgel_after->next); + ++i; + } + gimp_vector2_normalize (&smoothed_normal); + it->x_normal = smoothed_normal.x; + it->y_normal = smoothed_normal.y; + } +} + +static void +gimp_edgelset_compute_curvature (GArray *set, + GimpAsync *async) +{ + gint i; + + for (i = 0; i < set->len; i++) + { + Edgel *it = g_array_index (set, Edgel*, i); + Edgel *previous = g_array_index (set, Edgel *, it->previous); + Edgel *next = g_array_index (set, Edgel *, it->next); + GimpVector2 n_prev = gimp_vector2_new (previous->x_normal, previous->y_normal); + GimpVector2 n_next = gimp_vector2_new (next->x_normal, next->y_normal); + GimpVector2 diff = gimp_vector2_mul_val (gimp_vector2_sub_val (n_next, n_prev), + 0.5); + const float c = gimp_vector2_length_val (diff); + const float crossp = n_prev.x * n_next.y - n_prev.y * n_next.x; + + it->curvature = (crossp > 0.0f) ? c : -c; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + return; + } + } +} + +static void +gimp_edgelset_build_graph (GArray *set, + GeglBuffer *buffer, + GHashTable *edgel2index, + GimpAsync *async) +{ + Edgel edgel; + gint i; + + for (i = 0; i < set->len; i++) + { + Edgel *neighbor; + Edgel *it = g_array_index (set, Edgel *, i); + guint neighbor_pos; + + if (gimp_async_is_canceled (async)) + { + gimp_async_abort (async); + + return; + } + + gimp_edgelset_next8 (buffer, it, &edgel); + + gimp_assert (g_hash_table_contains (edgel2index, &edgel)); + neighbor_pos = GPOINTER_TO_UINT (g_hash_table_lookup (edgel2index, &edgel)); + it->next = neighbor_pos; + neighbor = g_array_index (set, Edgel *, neighbor_pos); + neighbor->previous = i; + } +} + +static void +gimp_edgelset_next8 (const GeglBuffer *buffer, + Edgel *it, + Edgel *n) +{ + guint8 pixels[9]; + + n->x = it->x; + n->y = it->y; + n->direction = it->direction; + + gegl_buffer_get ((GeglBuffer *) buffer, + GEGL_RECTANGLE (n->x - 1, n->y - 1, 3, 3), + 1.0, NULL, pixels, GEGL_AUTO_ROWSTRIDE, + GEGL_ABYSS_NONE); + switch (n->direction) + { + case XPlusDirection: + if (pixels[8]) + { + ++(n->y); + ++(n->x); + n->direction = YMinusDirection; + } + else if (pixels[7]) + { + ++(n->y); + } + else + { + n->direction = YPlusDirection; + } + break; + case YMinusDirection: + if (pixels[2]) + { + ++(n->x); + --(n->y); + n->direction = XMinusDirection; + } + else if (pixels[5]) + { + ++(n->x); + } + else + { + n->direction = XPlusDirection; + } + break; + case XMinusDirection: + if (pixels[0]) + { + --(n->x); + --(n->y); + n->direction = YPlusDirection; + } + else if (pixels[1]) + { + --(n->y); + } + else + { + n->direction = YMinusDirection; + } + break; + case YPlusDirection: + if (pixels[6]) + { + --(n->x); + ++(n->y); + n->direction = XPlusDirection; + } + else if (pixels[3]) + { + --(n->x); + } + else + { + n->direction = XMinusDirection; + } + break; + default: + g_return_if_reached (); + break; + } +} |