
For this, I needed distmap of the closed version of the line art (after splines and segments are created). This will result in invisible stroke borders added when flooding in the end. These invisible borders will have a thickness of 0.0, which means that flooding will stop at once after these single pixels are filled, which makes it quick, and is perfect since created splines and segments are 1-pixel thick anyway. Only downside is having to run "gegl:distance-transform" a second time, but this still stays fast.
1930 lines
68 KiB
C
1930 lines
68 KiB
C
/* 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"
|
|
|
|
#define GEGL_ITERATOR2_API
|
|
#include <gegl.h>
|
|
|
|
#include "libgimpmath/gimpmath.h"
|
|
|
|
#include "core-types.h"
|
|
|
|
#include "gimplineart.h"
|
|
|
|
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_lineart_denoise (GeglBuffer *buffer,
|
|
int size);
|
|
static void gimp_lineart_compute_normals_curvatures (GeglBuffer *mask,
|
|
gfloat *normals,
|
|
gfloat *curvatures,
|
|
gfloat *smoothed_curvatures,
|
|
int normal_estimate_mask_size);
|
|
static gfloat * gimp_lineart_get_smooth_curvatures (GArray *edgelset);
|
|
static GArray * gimp_lineart_curvature_extremums (gfloat *curvatures,
|
|
gfloat *smoothed_curvatures,
|
|
gint curvatures_width,
|
|
gint curvatures_height);
|
|
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);
|
|
|
|
static GArray * gimp_lineart_discrete_spline (Pixel p0,
|
|
GimpVector2 n0,
|
|
Pixel p1,
|
|
GimpVector2 n1);
|
|
|
|
static gint gimp_number_of_transitions (GArray *pixels,
|
|
GeglBuffer *buffer,
|
|
gboolean border_value);
|
|
static gboolean gimp_lineart_curve_creates_region (GeglBuffer *mask,
|
|
GArray *pixels,
|
|
int lower_size_limit,
|
|
int upper_size_limit);
|
|
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);
|
|
|
|
/* 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 starting_edgel);
|
|
|
|
/* Edgel set */
|
|
|
|
static GArray * gimp_edgelset_new (GeglBuffer *buffer);
|
|
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);
|
|
static void gimp_edgelset_compute_curvature (GArray *set);
|
|
|
|
static void gimp_edgelset_build_graph (GArray *set,
|
|
GeglBuffer *buffer,
|
|
GHashTable *edgel2index);
|
|
static void gimp_edgelset_next8 (const GeglBuffer *buffer,
|
|
Edgel *it,
|
|
Edgel *n);
|
|
|
|
/* Public functions */
|
|
|
|
/**
|
|
* gimp_lineart_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).
|
|
* @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_length: the maximum length for creating splines between
|
|
* end points.
|
|
* @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:
|
|
* @segments_max_length: the maximum length for creating segments
|
|
* between end points. Unlike splines, segments
|
|
* are straight lines.
|
|
* @closed_distmap: a distance map of the closed line art pixels.
|
|
* @lineart_radii: a map of estimated radii of line art border pixels.
|
|
*
|
|
* 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.
|
|
*
|
|
* Returns: a new #GeglBuffer of format "Y u8" representing the
|
|
* binarized @line_art. If @lineart_radii and @lineart_distmap
|
|
* are not #NULL, newly allocated float buffer are returned,
|
|
* which can be used for overflowing created masks later.
|
|
*/
|
|
GeglBuffer *
|
|
gimp_lineart_close (GeglBuffer *buffer,
|
|
gboolean select_transparent,
|
|
gfloat stroke_threshold,
|
|
gint minimal_lineart_area,
|
|
gint normal_estimate_mask_size,
|
|
gfloat end_point_rate,
|
|
gint spline_max_length,
|
|
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,
|
|
gint segments_max_length,
|
|
gfloat **closed_distmap,
|
|
gfloat **lineart_radii)
|
|
{
|
|
const Babl *gray_format;
|
|
gfloat *normals;
|
|
gfloat *curvatures;
|
|
gfloat *smoothed_curvatures;
|
|
gfloat *radii;
|
|
GeglBufferIterator *gi;
|
|
GeglBuffer *closed;
|
|
GeglBuffer *strokes;
|
|
GHashTable *visited;
|
|
GArray *keypoints;
|
|
Pixel *point;
|
|
GList *candidates;
|
|
SplineCandidate *candidate;
|
|
guchar max_value = 0;
|
|
gfloat threshold;
|
|
gfloat clamped_threshold;
|
|
gint width = gegl_buffer_get_width (buffer);
|
|
gint height = gegl_buffer_get_height (buffer);
|
|
gint i;
|
|
|
|
normals = g_new0 (gfloat, width * height * 2);
|
|
curvatures = g_new0 (gfloat, width * height);
|
|
smoothed_curvatures = g_new0 (gfloat, width * height);
|
|
|
|
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);
|
|
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;
|
|
|
|
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;
|
|
|
|
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);
|
|
|
|
/* Estimate normals & curvature */
|
|
gimp_lineart_compute_normals_curvatures (strokes, normals, curvatures,
|
|
smoothed_curvatures,
|
|
normal_estimate_mask_size);
|
|
|
|
radii = gimp_lineart_estimate_strokes_radii (strokes);
|
|
threshold = 1.0f - end_point_rate;
|
|
clamped_threshold = MAX (0.25f, threshold);
|
|
for (i = 0; i < width; i++)
|
|
{
|
|
gint j;
|
|
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;
|
|
}
|
|
}
|
|
if (lineart_radii)
|
|
*lineart_radii = radii;
|
|
else
|
|
g_free (radii);
|
|
|
|
keypoints = gimp_lineart_curvature_extremums (curvatures, smoothed_curvatures,
|
|
width, height);
|
|
candidates = gimp_lineart_find_spline_candidates (keypoints, normals, width,
|
|
spline_max_length,
|
|
spline_max_angle);
|
|
closed = gegl_buffer_dup (strokes);
|
|
|
|
/* Draw splines */
|
|
visited = g_hash_table_new_full ((GHashFunc) visited_hash_fun,
|
|
(GEqualFunc) visited_equal_fun,
|
|
(GDestroyNotify) g_free, NULL);
|
|
while (candidates)
|
|
{
|
|
Pixel *p1 = g_new (Pixel, 1);
|
|
Pixel *p2 = g_new (Pixel, 1);
|
|
gboolean inserted = FALSE;
|
|
|
|
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, FALSE) :
|
|
gimp_number_of_transitions (discrete_curve, closed, FALSE);
|
|
|
|
if (transitions == 2 &&
|
|
! gimp_lineart_curve_creates_region (closed, discrete_curve,
|
|
created_regions_significant_area,
|
|
created_regions_minimum_area - 1))
|
|
{
|
|
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 = 1;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/* Draw straight line segments */
|
|
point = (Pixel *) keypoints->data;
|
|
for (i = 0; i < keypoints->len; i++)
|
|
{
|
|
Pixel *p = g_new (Pixel, 1);
|
|
gboolean inserted = FALSE;
|
|
|
|
*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),
|
|
segments_max_length);
|
|
|
|
if (segment->len &&
|
|
! gimp_lineart_curve_creates_region (closed, segment,
|
|
created_regions_significant_area,
|
|
created_regions_minimum_area - 1))
|
|
{
|
|
gint j;
|
|
|
|
for (j = 0; j < segment->len; j++)
|
|
{
|
|
Pixel p2 = g_array_index (segment, Pixel, j);
|
|
guchar val = 1;
|
|
|
|
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++;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
g_hash_table_destroy (visited);
|
|
g_array_free (keypoints, TRUE);
|
|
g_object_unref (strokes);
|
|
g_free (normals);
|
|
g_free (curvatures);
|
|
g_free (smoothed_curvatures);
|
|
g_list_free_full (candidates, g_free);
|
|
|
|
return closed;
|
|
}
|
|
|
|
/* Private functions */
|
|
|
|
static void
|
|
gimp_lineart_denoise (GeglBuffer *buffer,
|
|
int minimum_area)
|
|
{
|
|
/* 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;
|
|
|
|
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 = (Pixel *) g_queue_pop_head (q);
|
|
gint p2x;
|
|
gint p2y;
|
|
|
|
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);
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
gfloat *edgels_curvatures;
|
|
gfloat *smoothed_curvature;
|
|
GArray *es = gimp_edgelset_new (mask);
|
|
Edgel **e = (Edgel **) es->data;
|
|
gint width = gegl_buffer_get_width (mask);
|
|
|
|
gimp_edgelset_smooth_normals (es, normal_estimate_mask_size);
|
|
gimp_edgelset_compute_curvature (es);
|
|
|
|
while (*e)
|
|
{
|
|
const float curvature = ((*e)->curvature > 0.0f) ? (*e)->curvature : 0.0f;
|
|
const float w = MAX (1e-8f, curvature * curvature);
|
|
|
|
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)
|
|
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);
|
|
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++;
|
|
}
|
|
g_free (edgels_curvatures);
|
|
|
|
g_array_free (es, TRUE);
|
|
}
|
|
|
|
static gfloat *
|
|
gimp_lineart_get_smooth_curvatures (GArray *edgelset)
|
|
{
|
|
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;
|
|
|
|
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)
|
|
{
|
|
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)
|
|
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_curvature_pixel = gimp_vector2_new (-1.0, -1.0);
|
|
gfloat max_curvature = 0.0f;
|
|
|
|
p->x = x;
|
|
p->y = y;
|
|
g_queue_push_tail (q, p);
|
|
visited[x + y * width] = TRUE;
|
|
|
|
while (! g_queue_is_empty (q))
|
|
{
|
|
gfloat c;
|
|
gint p2x;
|
|
gint p2y;
|
|
|
|
p = (Pixel *) g_queue_pop_head (q);
|
|
c = smoothed_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 (c > max_curvature)
|
|
{
|
|
max_curvature_pixel = *p;
|
|
max_curvature = c;
|
|
}
|
|
g_free (p);
|
|
}
|
|
curvatures[(gint) max_curvature_pixel.x + (gint) max_curvature_pixel.y * width] = max_curvature;
|
|
g_array_append_val (max_positions, max_curvature_pixel);
|
|
}
|
|
}
|
|
g_queue_free_full (q, g_free);
|
|
g_free (visited);
|
|
|
|
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)
|
|
{
|
|
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;
|
|
|
|
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,
|
|
gboolean border_value)
|
|
{
|
|
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++)
|
|
{
|
|
gboolean val;
|
|
|
|
it = g_array_index (pixels, Pixel, i);
|
|
if (it.x >= 0 && it.x < gegl_buffer_get_width (buffer) &&
|
|
it.y >= 0 && it.y < gegl_buffer_get_height (buffer))
|
|
{
|
|
guchar value;
|
|
|
|
gegl_buffer_sample (buffer, (gint) it.x, (gint) it.y, NULL, &value, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
val = (gboolean) value;
|
|
}
|
|
else
|
|
{
|
|
val = border_value;
|
|
}
|
|
result += (val != previous);
|
|
previous = val;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check whether a set of points will create a 4-connected background
|
|
* region whose size (i.e. number of pixels) falls within a given interval.
|
|
*/
|
|
static gboolean
|
|
gimp_lineart_curve_creates_region (GeglBuffer *mask,
|
|
GArray *pixels,
|
|
int lower_size_limit,
|
|
int upper_size_limit)
|
|
{
|
|
const glong max_edgel_count = 2 * (upper_size_limit + 1);
|
|
Pixel *p = (Pixel*) pixels->data;
|
|
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 = -1 * gimp_edgel_region_area (mask, e)) >= lower_size_limit) &&
|
|
(area <= upper_size_limit))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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 FALSE;
|
|
}
|
|
|
|
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)
|
|
{
|
|
GeglBufferIterator *gi;
|
|
gfloat *dist;
|
|
gfloat *thickness;
|
|
GeglBuffer *distmap;
|
|
GeglNode *graph;
|
|
GeglNode *input;
|
|
GeglNode *op;
|
|
GeglNode *sink;
|
|
gint width = gegl_buffer_get_width (mask);
|
|
gint height = gegl_buffer_get_height (mask);
|
|
|
|
/* Compute a distance map for the line art. */
|
|
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);
|
|
sink = gegl_node_new_child (graph,
|
|
"operation", "gegl:buffer-sink",
|
|
"buffer", &distmap,
|
|
NULL);
|
|
gegl_node_connect_to (input, "output",
|
|
op, "input");
|
|
gegl_node_connect_to (op, "output",
|
|
sink, "input");
|
|
gegl_node_process (sink);
|
|
g_object_unref (graph);
|
|
|
|
dist = g_new (gfloat, width * height);
|
|
gegl_buffer_get (distmap, NULL, 1.0, NULL, dist,
|
|
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
|
|
|
|
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;
|
|
|
|
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++;
|
|
}
|
|
}
|
|
|
|
g_free (dist);
|
|
g_object_unref (distmap);
|
|
|
|
return thickness;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
static glong
|
|
gimp_edgel_region_area (const GeglBuffer *mask,
|
|
Edgel start_edgel)
|
|
{
|
|
Edgel edgel = start_edgel;
|
|
long 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)
|
|
{
|
|
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;
|
|
|
|
for (y = starty; y < endy; y++)
|
|
for (x = startx; x < endx; x++)
|
|
{
|
|
if (*(p++))
|
|
{
|
|
if (y == 0 || ! *prevy)
|
|
gimp_edgelset_add (set, x, y, YMinusDirection, edgel2index);
|
|
if (y == height - 1 || ! *nexty)
|
|
gimp_edgelset_add (set, x, y, YPlusDirection, edgel2index);
|
|
if (x == 0 || ! *prevx)
|
|
gimp_edgelset_add (set, x, y, XMinusDirection, edgel2index);
|
|
if (x == width - 1 || ! *nextx)
|
|
gimp_edgelset_add (set, x, y, XPlusDirection, edgel2index);
|
|
}
|
|
prevy++;
|
|
nexty++;
|
|
prevx++;
|
|
nextx++;
|
|
}
|
|
}
|
|
|
|
gimp_edgelset_build_graph (set, buffer, edgel2index);
|
|
g_hash_table_destroy (edgel2index);
|
|
gimp_edgelset_init_normals (set);
|
|
|
|
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)
|
|
{
|
|
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;
|
|
|
|
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)
|
|
{
|
|
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;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gimp_edgelset_build_graph (GArray *set,
|
|
GeglBuffer *buffer,
|
|
GHashTable *edgel2index)
|
|
{
|
|
Edgel edgel;
|
|
gint i;
|
|
|
|
for (i = 0; i < set->len; i++)
|
|
{
|
|
Edgel *neighbor;
|
|
Edgel *it = g_array_index (set, Edgel *, i);
|
|
guint neighbor_pos;
|
|
|
|
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)
|
|
{
|
|
const int lx = gegl_buffer_get_width ((GeglBuffer *) buffer) - 1;
|
|
const int ly = gegl_buffer_get_height ((GeglBuffer *) buffer) - 1;
|
|
guchar has_stroke;
|
|
|
|
n->x = it->x;
|
|
n->y = it->y;
|
|
n->direction = it->direction;
|
|
switch (n->direction)
|
|
{
|
|
case XPlusDirection:
|
|
gegl_buffer_sample ((GeglBuffer *) buffer, n->x + 1, n->y + 1, NULL, &has_stroke, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
if ((n->x != lx) && (n->y != ly) && has_stroke)
|
|
{
|
|
++(n->y);
|
|
++(n->x);
|
|
n->direction = YMinusDirection;
|
|
}
|
|
else
|
|
{
|
|
gegl_buffer_sample ((GeglBuffer *) buffer, n->x, n->y + 1, NULL, &has_stroke, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
if ((n->y != ly) && has_stroke)
|
|
{
|
|
++(n->y);
|
|
}
|
|
else
|
|
{
|
|
n->direction = YPlusDirection;
|
|
}
|
|
}
|
|
break;
|
|
case YMinusDirection:
|
|
gegl_buffer_sample ((GeglBuffer *) buffer, n->x + 1, n->y - 1, NULL, &has_stroke, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
if ((n->x != lx) && n->y && has_stroke)
|
|
{
|
|
++(n->x);
|
|
--(n->y);
|
|
n->direction = XMinusDirection;
|
|
}
|
|
else
|
|
{
|
|
gegl_buffer_sample ((GeglBuffer *) buffer, n->x + 1, n->y, NULL, &has_stroke, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
if ((n->x != lx) && has_stroke)
|
|
{
|
|
++(n->x);
|
|
}
|
|
else
|
|
{
|
|
n->direction = XPlusDirection;
|
|
}
|
|
}
|
|
break;
|
|
case XMinusDirection:
|
|
gegl_buffer_sample ((GeglBuffer *) buffer, n->x - 1, n->y - 1, NULL, &has_stroke, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
if (n->x && n->y && has_stroke)
|
|
{
|
|
--(n->x);
|
|
--(n->y);
|
|
n->direction = YPlusDirection;
|
|
}
|
|
else
|
|
{
|
|
gegl_buffer_sample ((GeglBuffer *) buffer, n->x, n->y - 1, NULL, &has_stroke, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
if (n->y && has_stroke)
|
|
{
|
|
--(n->y);
|
|
}
|
|
else
|
|
{
|
|
n->direction = YMinusDirection;
|
|
}
|
|
}
|
|
break;
|
|
case YPlusDirection:
|
|
gegl_buffer_sample ((GeglBuffer *) buffer, n->x - 1, n->y + 1, NULL, &has_stroke, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
if (n->x && (n->y != ly) && has_stroke)
|
|
{
|
|
--(n->x);
|
|
++(n->y);
|
|
n->direction = XPlusDirection;
|
|
}
|
|
else
|
|
{
|
|
gegl_buffer_sample ((GeglBuffer *) buffer, n->x - 1, n->y, NULL, &has_stroke, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
if (n->x && has_stroke)
|
|
{
|
|
--(n->x);
|
|
}
|
|
else
|
|
{
|
|
n->direction = XMinusDirection;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
gimp_assert (FALSE);
|
|
break;
|
|
}
|
|
}
|