3013 lines
103 KiB
C
3013 lines
103 KiB
C
/* PIKA - Photo and Image Kooker Application
|
|
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
|
|
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
|
|
*
|
|
* Original copyright, applying to most contents (license remains unchanged):
|
|
* 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 "libpikabase/pikabase.h"
|
|
#include "libpikamath/pikamath.h"
|
|
|
|
#include "core-types.h"
|
|
|
|
#include "gegl/pika-gegl-loops.h"
|
|
#include "gegl/pika-gegl-utils.h"
|
|
|
|
#include "pika-parallel.h"
|
|
#include "pika-priorities.h"
|
|
#include "pika-utils.h" /* PIKA_TIMER */
|
|
#include "pikaasync.h"
|
|
#include "pikacancelable.h"
|
|
#include "pikadrawable.h"
|
|
#include "pikaimage.h"
|
|
#include "pikalineart.h"
|
|
#include "pikapickable.h"
|
|
#include "pikaprojection.h"
|
|
#include "pikaviewable.h"
|
|
#include "pikawaitable.h"
|
|
|
|
#include "pika-intl.h"
|
|
|
|
enum
|
|
{
|
|
COMPUTING_START,
|
|
COMPUTING_END,
|
|
LAST_SIGNAL,
|
|
};
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_SELECT_TRANSPARENT,
|
|
PROP_MAX_GROW,
|
|
PROP_THRESHOLD,
|
|
PROP_AUTOMATIC_CLOSURE,
|
|
PROP_SPLINE_MAX_LEN,
|
|
PROP_SEGMENT_MAX_LEN,
|
|
};
|
|
|
|
typedef struct _PikaLineArtPrivate PikaLineArtPrivate;
|
|
|
|
struct _PikaLineArtPrivate
|
|
{
|
|
gboolean frozen;
|
|
gboolean compute_after_thaw;
|
|
|
|
PikaAsync *async;
|
|
|
|
gint idle_id;
|
|
|
|
PikaPickable *input;
|
|
GeglBuffer *closed;
|
|
gfloat *distmap;
|
|
|
|
/* Used in the closing step. */
|
|
gboolean select_transparent;
|
|
gdouble threshold;
|
|
gboolean automatic_closure;
|
|
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;
|
|
gboolean automatic_closure;
|
|
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 PikaVector2 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 PikaVector2 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 pika_line_art_finalize (GObject *object);
|
|
static void pika_line_art_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec);
|
|
static void pika_line_art_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec);
|
|
|
|
/* Functions for asynchronous computation. */
|
|
|
|
static void pika_line_art_compute (PikaLineArt *line_art);
|
|
static void pika_line_art_compute_cb (PikaAsync *async,
|
|
PikaLineArt *line_art);
|
|
|
|
static PikaAsync * pika_line_art_prepare_async (PikaLineArt *line_art,
|
|
gint priority);
|
|
static void pika_line_art_prepare_async_func (PikaAsync *async,
|
|
LineArtData *data);
|
|
static LineArtData * line_art_data_new (GeglBuffer *buffer,
|
|
PikaLineArt *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 pika_line_art_idle (PikaLineArt *line_art);
|
|
static void pika_line_art_input_invalidate_preview (PikaViewable *viewable,
|
|
PikaLineArt *line_art);
|
|
|
|
|
|
/* All actual computation functions. */
|
|
|
|
static GeglBuffer * pika_line_art_close (GeglBuffer *buffer,
|
|
gboolean select_transparent,
|
|
gdouble stroke_threshold,
|
|
gboolean automatic_closure,
|
|
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,
|
|
PikaAsync *async);
|
|
|
|
static void pika_lineart_denoise (GeglBuffer *buffer,
|
|
int size,
|
|
PikaAsync *async);
|
|
static void pika_lineart_compute_normals_curvatures (GeglBuffer *mask,
|
|
gfloat *normals,
|
|
gfloat *curvatures,
|
|
gfloat *smoothed_curvatures,
|
|
int normal_estimate_mask_size,
|
|
PikaAsync *async);
|
|
static gfloat * pika_lineart_get_smooth_curvatures (GArray *edgelset,
|
|
PikaAsync *async);
|
|
static GArray * pika_lineart_curvature_extremums (gfloat *curvatures,
|
|
gfloat *smoothed_curvatures,
|
|
gint curvatures_width,
|
|
gint curvatures_height,
|
|
PikaAsync *async);
|
|
static gint pika_spline_candidate_cmp (const SplineCandidate *a,
|
|
const SplineCandidate *b,
|
|
gpointer user_data);
|
|
static GList * pika_lineart_find_spline_candidates (GArray *max_positions,
|
|
gfloat *normals,
|
|
gint width,
|
|
gint distance_threshold,
|
|
gfloat max_angle_deg,
|
|
PikaAsync *async);
|
|
|
|
static GArray * pika_lineart_discrete_spline (Pixel p0,
|
|
PikaVector2 n0,
|
|
Pixel p1,
|
|
PikaVector2 n1);
|
|
|
|
static gint pika_number_of_transitions (GArray *pixels,
|
|
GeglBuffer *buffer);
|
|
static gboolean pika_line_art_allow_closure (GeglBuffer *mask,
|
|
GArray *pixels,
|
|
GList **fill_pixels,
|
|
int significant_size,
|
|
int minimum_size);
|
|
static GArray * pika_lineart_line_segment_until_hit (const GeglBuffer *buffer,
|
|
Pixel start,
|
|
PikaVector2 direction,
|
|
int size);
|
|
static gfloat * pika_lineart_estimate_strokes_radii (GeglBuffer *mask,
|
|
PikaAsync *async);
|
|
static void pika_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 PikaVector2 pair2normal (Pixel p,
|
|
gfloat *normals,
|
|
gint width);
|
|
|
|
/* Edgel */
|
|
|
|
static Edgel * pika_edgel_new (int x,
|
|
int y,
|
|
Direction direction);
|
|
static void pika_edgel_init (Edgel *edgel);
|
|
static void pika_edgel_clear (Edgel **edgel);
|
|
static int pika_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 pika_edgel_track_mark (GeglBuffer *mask,
|
|
Edgel edgel,
|
|
long size_limit);
|
|
static glong pika_edgel_region_area (const GeglBuffer *mask,
|
|
Edgel start_edgel);
|
|
|
|
/* Edgel set */
|
|
|
|
static GArray * pika_edgelset_new (GeglBuffer *buffer,
|
|
PikaAsync *async);
|
|
static void pika_edgelset_add (GArray *set,
|
|
int x,
|
|
int y,
|
|
Direction direction,
|
|
GHashTable *edgel2index);
|
|
static void pika_edgelset_init_normals (GArray *set);
|
|
static void pika_edgelset_smooth_normals (GArray *set,
|
|
int mask_size,
|
|
PikaAsync *async);
|
|
static void pika_edgelset_compute_curvature (GArray *set,
|
|
PikaAsync *async);
|
|
|
|
static void pika_edgelset_build_graph (GArray *set,
|
|
GeglBuffer *buffer,
|
|
GHashTable *edgel2index,
|
|
PikaAsync *async);
|
|
static void pika_edgelset_next8 (const GeglBuffer *buffer,
|
|
Edgel *it,
|
|
Edgel *n);
|
|
|
|
G_DEFINE_TYPE_WITH_CODE (PikaLineArt, pika_line_art, PIKA_TYPE_OBJECT,
|
|
G_ADD_PRIVATE (PikaLineArt))
|
|
|
|
#define parent_class pika_line_art_parent_class
|
|
|
|
static guint pika_line_art_signals[LAST_SIGNAL] = { 0 };
|
|
|
|
static void
|
|
pika_line_art_class_init (PikaLineArtClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
pika_line_art_signals[COMPUTING_START] =
|
|
g_signal_new ("computing-start",
|
|
G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (PikaLineArtClass, computing_start),
|
|
NULL, NULL, NULL,
|
|
G_TYPE_NONE, 0);
|
|
pika_line_art_signals[COMPUTING_END] =
|
|
g_signal_new ("computing-end",
|
|
G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (PikaLineArtClass, computing_end),
|
|
NULL, NULL, NULL,
|
|
G_TYPE_NONE, 0);
|
|
|
|
object_class->finalize = pika_line_art_finalize;
|
|
object_class->set_property = pika_line_art_set_property;
|
|
object_class->get_property = pika_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 | PIKA_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 | PIKA_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 | PIKA_PARAM_READWRITE));
|
|
|
|
g_object_class_install_property (object_class, PROP_AUTOMATIC_CLOSURE,
|
|
g_param_spec_boolean ("automatic-closure",
|
|
_("Whether or not we should perform the closing step"),
|
|
_("Whether or not we should perform the closing step"),
|
|
TRUE,
|
|
G_PARAM_CONSTRUCT | PIKA_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 | PIKA_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 | PIKA_PARAM_READWRITE));
|
|
}
|
|
|
|
static void
|
|
pika_line_art_init (PikaLineArt *line_art)
|
|
{
|
|
line_art->priv = pika_line_art_get_instance_private (line_art);
|
|
}
|
|
|
|
static void
|
|
pika_line_art_finalize (GObject *object)
|
|
{
|
|
PikaLineArt *line_art = PIKA_LINE_ART (object);
|
|
|
|
line_art->priv->frozen = FALSE;
|
|
|
|
pika_line_art_set_input (line_art, NULL);
|
|
|
|
G_OBJECT_CLASS (parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
pika_line_art_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
PikaLineArt *line_art = PIKA_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);
|
|
pika_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);
|
|
pika_line_art_compute (line_art);
|
|
}
|
|
break;
|
|
case PROP_AUTOMATIC_CLOSURE:
|
|
if (line_art->priv->automatic_closure != g_value_get_boolean (value))
|
|
{
|
|
line_art->priv->automatic_closure = g_value_get_boolean (value);
|
|
pika_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;
|
|
pika_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;
|
|
pika_line_art_compute (line_art);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
pika_line_art_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
PikaLineArt *line_art = PIKA_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_AUTOMATIC_CLOSURE:
|
|
g_value_set_boolean (value, line_art->priv->automatic_closure);
|
|
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 */
|
|
|
|
PikaLineArt *
|
|
pika_line_art_new (void)
|
|
{
|
|
return g_object_new (PIKA_TYPE_LINE_ART,
|
|
NULL);
|
|
}
|
|
|
|
void
|
|
pika_line_art_bind_gap_length (PikaLineArt *line_art,
|
|
gboolean bound)
|
|
{
|
|
line_art->priv->max_len_bound = bound;
|
|
}
|
|
|
|
void
|
|
pika_line_art_set_input (PikaLineArt *line_art,
|
|
PikaPickable *pickable)
|
|
{
|
|
g_return_if_fail (pickable == NULL || PIKA_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);
|
|
|
|
pika_line_art_compute (line_art);
|
|
|
|
if (pickable)
|
|
{
|
|
g_signal_connect (pickable, "invalidate-preview",
|
|
G_CALLBACK (pika_line_art_input_invalidate_preview),
|
|
line_art);
|
|
}
|
|
}
|
|
}
|
|
|
|
PikaPickable *
|
|
pika_line_art_get_input (PikaLineArt *line_art)
|
|
{
|
|
return line_art->priv->input;
|
|
}
|
|
|
|
void
|
|
pika_line_art_freeze (PikaLineArt *line_art)
|
|
{
|
|
g_return_if_fail (! line_art->priv->frozen);
|
|
|
|
line_art->priv->frozen = TRUE;
|
|
line_art->priv->compute_after_thaw = FALSE;
|
|
}
|
|
|
|
void
|
|
pika_line_art_thaw (PikaLineArt *line_art)
|
|
{
|
|
g_return_if_fail (line_art->priv->frozen);
|
|
|
|
line_art->priv->frozen = FALSE;
|
|
if (line_art->priv->compute_after_thaw)
|
|
{
|
|
pika_line_art_compute (line_art);
|
|
line_art->priv->compute_after_thaw = FALSE;
|
|
}
|
|
}
|
|
|
|
gboolean
|
|
pika_line_art_is_frozen (PikaLineArt *line_art)
|
|
{
|
|
return line_art->priv->frozen;
|
|
}
|
|
|
|
GeglBuffer *
|
|
pika_line_art_get (PikaLineArt *line_art,
|
|
gfloat **distmap)
|
|
{
|
|
g_return_val_if_fail (line_art->priv->input, NULL);
|
|
|
|
if (line_art->priv->async)
|
|
{
|
|
pika_waitable_wait (PIKA_WAITABLE (line_art->priv->async));
|
|
}
|
|
else if (! line_art->priv->closed)
|
|
{
|
|
pika_line_art_compute (line_art);
|
|
if (line_art->priv->async)
|
|
pika_waitable_wait (PIKA_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
|
|
pika_line_art_compute (PikaLineArt *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 pika_line_art_compute_cb()
|
|
* bails if the async has been canceled, to avoid accessing the line art.
|
|
*/
|
|
g_signal_emit (line_art, pika_line_art_signals[COMPUTING_END], 0);
|
|
pika_cancelable_cancel (PIKA_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)
|
|
{
|
|
/* pika_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 (pika_line_art_input_invalidate_preview),
|
|
line_art);
|
|
line_art->priv->async = pika_line_art_prepare_async (line_art, +1);
|
|
g_signal_emit (line_art, pika_line_art_signals[COMPUTING_START], 0);
|
|
g_signal_handlers_unblock_by_func (
|
|
line_art->priv->input,
|
|
G_CALLBACK (pika_line_art_input_invalidate_preview),
|
|
line_art);
|
|
|
|
pika_async_add_callback_for_object (line_art->priv->async,
|
|
(PikaAsyncCallback) pika_line_art_compute_cb,
|
|
line_art, line_art);
|
|
}
|
|
}
|
|
|
|
static void
|
|
pika_line_art_compute_cb (PikaAsync *async,
|
|
PikaLineArt *line_art)
|
|
{
|
|
if (pika_async_is_canceled (async))
|
|
return;
|
|
|
|
if (pika_async_is_finished (async))
|
|
{
|
|
LineArtResult *result;
|
|
|
|
result = pika_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, pika_line_art_signals[COMPUTING_END], 0);
|
|
}
|
|
|
|
g_clear_object (&line_art->priv->async);
|
|
}
|
|
|
|
static PikaAsync *
|
|
pika_line_art_prepare_async (PikaLineArt *line_art,
|
|
gint priority)
|
|
{
|
|
GeglBuffer *buffer;
|
|
PikaAsync *async;
|
|
LineArtData *data;
|
|
|
|
g_return_val_if_fail (PIKA_IS_PICKABLE (line_art->priv->input), NULL);
|
|
|
|
pika_pickable_flush (line_art->priv->input);
|
|
|
|
buffer = pika_gegl_buffer_dup (
|
|
pika_pickable_get_buffer (line_art->priv->input));
|
|
|
|
data = line_art_data_new (buffer, line_art);
|
|
|
|
g_object_unref (buffer);
|
|
|
|
async = pika_parallel_run_async_full (
|
|
priority,
|
|
(PikaRunAsyncFunc) pika_line_art_prepare_async_func,
|
|
data, (GDestroyNotify) line_art_data_free);
|
|
|
|
return async;
|
|
}
|
|
|
|
static void
|
|
pika_line_art_prepare_async_func (PikaAsync *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 (pika_async_is_canceled (async))
|
|
{
|
|
gegl_buffer_iterator_stop (gi);
|
|
|
|
pika_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.
|
|
*/
|
|
PIKA_TIMER_START();
|
|
|
|
closed = pika_line_art_close (buffer,
|
|
select_transparent,
|
|
data->threshold,
|
|
data->automatic_closure,
|
|
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);
|
|
|
|
PIKA_TIMER_END("close line-art");
|
|
|
|
if (buffer != data->buffer)
|
|
g_object_unref (buffer);
|
|
|
|
if (! pika_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;
|
|
}
|
|
|
|
pika_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,
|
|
PikaLineArt *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->automatic_closure = line_art->priv->automatic_closure;
|
|
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
|
|
pika_line_art_idle (PikaLineArt *line_art)
|
|
{
|
|
line_art->priv->idle_id = 0;
|
|
|
|
pika_line_art_compute (line_art);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
pika_line_art_input_invalidate_preview (PikaViewable *viewable,
|
|
PikaLineArt *line_art)
|
|
{
|
|
if (! line_art->priv->idle_id)
|
|
{
|
|
line_art->priv->idle_id = g_idle_add_full (
|
|
PIKA_PRIORITY_VIEWABLE_IDLE,
|
|
(GSourceFunc) pika_line_art_idle,
|
|
line_art, NULL);
|
|
}
|
|
}
|
|
|
|
/* All actual computation functions. */
|
|
|
|
/**
|
|
* pika_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).
|
|
* @automatic_closure: whether the closing step should be performed or
|
|
* not. @spline_max_length and @segment_max_len are
|
|
* used only if @automatic_closure is %TRUE.
|
|
* @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 #PikaAsync 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 *
|
|
pika_line_art_close (GeglBuffer *buffer,
|
|
gboolean select_transparent,
|
|
gdouble stroke_threshold,
|
|
gboolean automatic_closure,
|
|
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,
|
|
PikaAsync *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);
|
|
pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
gegl_buffer_iterator_stop (gi);
|
|
|
|
pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
gegl_buffer_iterator_stop (gi);
|
|
|
|
pika_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) */
|
|
pika_lineart_denoise (strokes, minimal_lineart_area, async);
|
|
if (pika_async_is_stopped (async))
|
|
goto end1;
|
|
|
|
closed = g_object_ref (strokes);
|
|
|
|
if (automatic_closure &&
|
|
(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 */
|
|
pika_lineart_compute_normals_curvatures (strokes, normals, curvatures,
|
|
smoothed_curvatures,
|
|
normal_estimate_mask_size,
|
|
async);
|
|
if (pika_async_is_stopped (async))
|
|
goto end2;
|
|
|
|
radii = pika_lineart_estimate_strokes_radii (strokes, async);
|
|
if (pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 = pika_lineart_curvature_extremums (curvatures, smoothed_curvatures,
|
|
width, height, async);
|
|
if (pika_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 = pika_lineart_find_spline_candidates (keypoints, normals, width,
|
|
spline_max_length,
|
|
spline_max_angle,
|
|
async);
|
|
if (pika_async_is_stopped (async))
|
|
goto end3;
|
|
|
|
g_object_unref (closed);
|
|
closed = pika_gegl_buffer_dup (strokes);
|
|
|
|
/* Draw splines */
|
|
while (candidates)
|
|
{
|
|
Pixel *p1;
|
|
Pixel *p2;
|
|
gboolean inserted = FALSE;
|
|
|
|
if (pika_async_is_canceled (async))
|
|
{
|
|
pika_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;
|
|
PikaVector2 vect1 = pair2normal (*p1, normals, width);
|
|
PikaVector2 vect2 = pair2normal (*p2, normals, width);
|
|
gfloat distance = pika_vector2_length_val (pika_vector2_sub_val (*p1, *p2));
|
|
gint transitions;
|
|
|
|
pika_vector2_mul (&vect1, distance);
|
|
pika_vector2_mul (&vect1, spline_roundness);
|
|
pika_vector2_mul (&vect2, distance);
|
|
pika_vector2_mul (&vect2, spline_roundness);
|
|
|
|
discrete_curve = pika_lineart_discrete_spline (*p1, vect1, *p2, vect2);
|
|
|
|
transitions = allow_self_intersections ?
|
|
pika_number_of_transitions (discrete_curve, strokes) :
|
|
pika_number_of_transitions (discrete_curve, closed);
|
|
|
|
if (transitions == 2 &&
|
|
pika_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 (pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 = pika_lineart_line_segment_until_hit (closed, *point,
|
|
pair2normal (*point, normals, width),
|
|
segment_max_length);
|
|
|
|
if (segment->len &&
|
|
pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_async_abort (async);
|
|
|
|
goto end2;
|
|
}
|
|
|
|
/* XXX A best approach would be to generalize
|
|
* pika_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.
|
|
*/
|
|
pika_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 (pika_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_link (input, op);
|
|
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 (pika_async_is_stopped (async))
|
|
g_clear_object (&closed);
|
|
|
|
return closed;
|
|
}
|
|
|
|
static void
|
|
pika_lineart_denoise (GeglBuffer *buffer,
|
|
int minimum_area,
|
|
PikaAsync *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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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
|
|
pika_lineart_compute_normals_curvatures (GeglBuffer *mask,
|
|
gfloat *normals,
|
|
gfloat *curvatures,
|
|
gfloat *smoothed_curvatures,
|
|
int normal_estimate_mask_size,
|
|
PikaAsync *async)
|
|
{
|
|
gfloat *edgels_curvatures = NULL;
|
|
gfloat *smoothed_curvature;
|
|
GArray *es = NULL;
|
|
Edgel **e;
|
|
gint width = gegl_buffer_get_width (mask);
|
|
|
|
es = pika_edgelset_new (mask, async);
|
|
if (pika_async_is_stopped (async))
|
|
goto end;
|
|
|
|
e = (Edgel **) es->data;
|
|
|
|
pika_edgelset_smooth_normals (es, normal_estimate_mask_size, async);
|
|
if (pika_async_is_stopped (async))
|
|
goto end;
|
|
|
|
pika_edgelset_compute_curvature (es, async);
|
|
if (pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 = pika_lineart_get_smooth_curvatures (es, async);
|
|
if (pika_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 *
|
|
pika_lineart_get_smooth_curvatures (GArray *edgelset,
|
|
PikaAsync *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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 *
|
|
pika_lineart_curvature_extremums (gfloat *curvatures,
|
|
gfloat *smoothed_curvatures,
|
|
gint width,
|
|
gint height,
|
|
PikaAsync *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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 = pika_vector2_new (-1.0, -1.0);
|
|
max_smoothed_curvature = 0.0f;
|
|
|
|
max_raw_curvature_pixel = pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 (pika_async_is_stopped (async))
|
|
{
|
|
g_array_free (max_positions, TRUE);
|
|
max_positions = NULL;
|
|
}
|
|
|
|
return max_positions;
|
|
}
|
|
|
|
static gint
|
|
pika_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 *
|
|
pika_lineart_find_spline_candidates (GArray *max_positions,
|
|
gfloat *normals,
|
|
gint width,
|
|
gint distance_threshold,
|
|
gfloat max_angle_deg,
|
|
PikaAsync *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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_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 = pika_vector2_length_val (pika_vector2_sub_val (p1, p2));
|
|
|
|
if (distance <= distance_threshold)
|
|
{
|
|
PikaVector2 normalP1;
|
|
PikaVector2 normalP2;
|
|
PikaVector2 p1f;
|
|
PikaVector2 p2f;
|
|
PikaVector2 p1p2;
|
|
float cosN;
|
|
float qualityA;
|
|
float qualityB;
|
|
float qualityC;
|
|
float quality;
|
|
|
|
normalP1 = pika_vector2_new (normals[((gint) p1.x + (gint) p1.y * width) * 2],
|
|
normals[((gint) p1.x + (gint) p1.y * width) * 2 + 1]);
|
|
normalP2 = pika_vector2_new (normals[((gint) p2.x + (gint) p2.y * width) * 2],
|
|
normals[((gint) p2.x + (gint) p2.y * width) * 2 + 1]);
|
|
p1f = pika_vector2_new (p1.x, p1.y);
|
|
p2f = pika_vector2_new (p2.x, p2.y);
|
|
p1p2 = pika_vector2_sub_val (p2f, p1f);
|
|
|
|
cosN = pika_vector2_inner_product_val (normalP1, (pika_vector2_neg_val (normalP2)));
|
|
qualityA = MAX (0.0f, 1 - distance / distance_threshold);
|
|
qualityB = MAX (0.0f,
|
|
(float) (pika_vector2_inner_product_val (normalP1, p1p2) - pika_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) pika_spline_candidate_cmp,
|
|
NULL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
static GArray *
|
|
pika_lineart_discrete_spline (Pixel p0,
|
|
PikaVector2 n0,
|
|
Pixel p1,
|
|
PikaVector2 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 = pika_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 = pika_vector2_new ((gint) round (a0 * t3 + b0 * t2 + c0 * t + d0),
|
|
(gint) round (a1 * t3 + b1 * t2 + c1 * t + d1));
|
|
|
|
/* create pika_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
|
|
pika_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;
|
|
}
|
|
|
|
/**
|
|
* pika_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 insignificant 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 "insignificant" 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
|
|
pika_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;
|
|
|
|
pika_edgel_init (&e);
|
|
e.x = p.x;
|
|
e.y = p.y;
|
|
e.direction = direction;
|
|
|
|
count = pika_edgel_track_mark (mask, e, max_edgel_count);
|
|
if ((count != -1) && (count <= max_edgel_count))
|
|
{
|
|
area = pika_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 *
|
|
pika_lineart_line_segment_until_hit (const GeglBuffer *mask,
|
|
Pixel start,
|
|
PikaVector2 direction,
|
|
int size)
|
|
{
|
|
GeglBuffer *buffer = (GeglBuffer *) mask;
|
|
gboolean out = FALSE;
|
|
GArray *points = g_array_new (FALSE, TRUE, sizeof (Pixel));
|
|
int tmax;
|
|
PikaVector2 p0 = pika_vector2_new (start.x, start.y);
|
|
|
|
pika_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)
|
|
{
|
|
PikaVector2 v = pika_vector2_add_val (p0, pika_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 *
|
|
pika_lineart_estimate_strokes_radii (GeglBuffer *mask,
|
|
PikaAsync *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_link (input, op);
|
|
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 (pika_async_is_canceled (async))
|
|
{
|
|
gegl_buffer_iterator_stop (gi);
|
|
|
|
pika_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 (pika_async_is_stopped (async))
|
|
g_clear_pointer (&thickness, g_free);
|
|
|
|
return thickness;
|
|
}
|
|
|
|
static void
|
|
pika_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)--;
|
|
pika_line_art_simple_fill (buffer, x + 1, y, counter);
|
|
pika_line_art_simple_fill (buffer, x - 1, y, counter);
|
|
pika_line_art_simple_fill (buffer, x, y + 1, counter);
|
|
pika_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 PikaVector2
|
|
pair2normal (Pixel p,
|
|
gfloat *normals,
|
|
gint width)
|
|
{
|
|
return pika_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 *
|
|
pika_edgel_new (int x,
|
|
int y,
|
|
Direction direction)
|
|
{
|
|
Edgel *edgel = g_new (Edgel, 1);
|
|
|
|
edgel->x = x;
|
|
edgel->y = y;
|
|
edgel->direction = direction;
|
|
|
|
pika_edgel_init (edgel);
|
|
|
|
return edgel;
|
|
}
|
|
|
|
static void
|
|
pika_edgel_init (Edgel *edgel)
|
|
{
|
|
edgel->x_normal = 0;
|
|
edgel->y_normal = 0;
|
|
edgel->curvature = 0;
|
|
edgel->next = edgel->previous = G_MAXUINT;
|
|
}
|
|
|
|
static void
|
|
pika_edgel_clear (Edgel **edgel)
|
|
{
|
|
g_clear_pointer (edgel, g_free);
|
|
}
|
|
|
|
static int
|
|
pika_edgel_cmp (const Edgel* e1,
|
|
const Edgel* e2)
|
|
{
|
|
pika_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
|
|
pika_edgel_track_mark (GeglBuffer *mask,
|
|
Edgel edgel,
|
|
long size_limit)
|
|
{
|
|
Edgel start = edgel;
|
|
long count = 1;
|
|
|
|
do
|
|
{
|
|
guchar val;
|
|
|
|
pika_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 (pika_edgel_cmp (&edgel, &start) != 0)
|
|
++count;
|
|
}
|
|
while (pika_edgel_cmp (&edgel, &start) != 0 && count <= size_limit);
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* pika_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 uncertainty 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
|
|
pika_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;
|
|
|
|
pika_edgelset_next8 (mask, &edgel, &edgel);
|
|
}
|
|
while (pika_edgel_cmp (&edgel, &start_edgel) != 0);
|
|
|
|
return area;
|
|
}
|
|
|
|
/* Edgel sets */
|
|
|
|
static GArray *
|
|
pika_edgelset_new (GeglBuffer *buffer,
|
|
PikaAsync *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) pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
gegl_buffer_iterator_stop (gi);
|
|
|
|
pika_async_abort (async);
|
|
|
|
goto end;
|
|
}
|
|
|
|
for (y = starty; y < endy; y++)
|
|
for (x = startx; x < endx; x++)
|
|
{
|
|
if (*(p++))
|
|
{
|
|
if (! *prevy)
|
|
pika_edgelset_add (set, x, y, YMinusDirection, edgel2index);
|
|
if (! *nexty)
|
|
pika_edgelset_add (set, x, y, YPlusDirection, edgel2index);
|
|
if (! *prevx)
|
|
pika_edgelset_add (set, x, y, XMinusDirection, edgel2index);
|
|
if (! *nextx)
|
|
pika_edgelset_add (set, x, y, XPlusDirection, edgel2index);
|
|
}
|
|
prevy++;
|
|
nexty++;
|
|
prevx++;
|
|
nextx++;
|
|
}
|
|
}
|
|
|
|
pika_edgelset_build_graph (set, buffer, edgel2index, async);
|
|
if (pika_async_is_stopped (async))
|
|
goto end;
|
|
|
|
pika_edgelset_init_normals (set);
|
|
|
|
end:
|
|
g_hash_table_destroy (edgel2index);
|
|
|
|
if (pika_async_is_stopped (async))
|
|
{
|
|
g_array_free (set, TRUE);
|
|
set = NULL;
|
|
}
|
|
|
|
return set;
|
|
}
|
|
|
|
static void
|
|
pika_edgelset_add (GArray *set,
|
|
int x,
|
|
int y,
|
|
Direction direction,
|
|
GHashTable *edgel2index)
|
|
{
|
|
Edgel *edgel = pika_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
|
|
pika_edgelset_init_normals (GArray *set)
|
|
{
|
|
Edgel **e = (Edgel**) set->data;
|
|
|
|
while (*e)
|
|
{
|
|
PikaVector2 n = Direction2Normal[(*e)->direction];
|
|
|
|
(*e)->x_normal = n.x;
|
|
(*e)->y_normal = n.y;
|
|
e++;
|
|
}
|
|
}
|
|
|
|
static void
|
|
pika_edgelset_smooth_normals (GArray *set,
|
|
int mask_size,
|
|
PikaAsync *async)
|
|
{
|
|
const gfloat sigma = mask_size * 0.775;
|
|
const gfloat den = 2 * sigma * sigma;
|
|
gfloat weights[65];
|
|
PikaVector2 smoothed_normal;
|
|
gint i;
|
|
|
|
pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_async_abort (async);
|
|
|
|
return;
|
|
}
|
|
|
|
smoothed_normal = Direction2Normal[it->direction];
|
|
while (n-- && (edgel_after != edgel_before))
|
|
{
|
|
smoothed_normal = pika_vector2_add_val (smoothed_normal,
|
|
pika_vector2_mul_val (Direction2Normal[edgel_before->direction], weights[i]));
|
|
smoothed_normal = pika_vector2_add_val (smoothed_normal,
|
|
pika_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;
|
|
}
|
|
pika_vector2_normalize (&smoothed_normal);
|
|
it->x_normal = smoothed_normal.x;
|
|
it->y_normal = smoothed_normal.y;
|
|
}
|
|
}
|
|
|
|
static void
|
|
pika_edgelset_compute_curvature (GArray *set,
|
|
PikaAsync *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);
|
|
PikaVector2 n_prev = pika_vector2_new (previous->x_normal, previous->y_normal);
|
|
PikaVector2 n_next = pika_vector2_new (next->x_normal, next->y_normal);
|
|
PikaVector2 diff = pika_vector2_mul_val (pika_vector2_sub_val (n_next, n_prev),
|
|
0.5);
|
|
const float c = pika_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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_async_abort (async);
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
pika_edgelset_build_graph (GArray *set,
|
|
GeglBuffer *buffer,
|
|
GHashTable *edgel2index,
|
|
PikaAsync *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 (pika_async_is_canceled (async))
|
|
{
|
|
pika_async_abort (async);
|
|
|
|
return;
|
|
}
|
|
|
|
pika_edgelset_next8 (buffer, it, &edgel);
|
|
|
|
pika_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
|
|
pika_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;
|
|
}
|
|
}
|