When selecting by color with the Fuzzy Selection, Color Selection, or Bucket Fill tool, we use gimp_pickable_get_buffer (). This only looks at the raw pixels and not any active filters, which can result in some unexpected selections (for example, an NDE offset filter will make it appear as if you are selecting the wrong area). There are plans to let users decide whether to select the raw pixels or the filtered layer. For now, this sets the default action to consider the layer as seen by the user, with filters included in the calculation.
1205 lines
42 KiB
C++
1205 lines
42 KiB
C++
/* GIMP - The GNU Image Manipulation Program
|
|
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
|
|
*
|
|
* 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 <stdlib.h>
|
|
|
|
#include <cairo.h>
|
|
#include <gegl.h>
|
|
#include <gdk-pixbuf/gdk-pixbuf.h>
|
|
|
|
#include "libgimpcolor/gimpcolor.h"
|
|
#include "libgimpmath/gimpmath.h"
|
|
|
|
extern "C"
|
|
{
|
|
|
|
#include "core-types.h"
|
|
|
|
#include "gegl/gimp-babl.h"
|
|
#include "gegl/gimp-gegl-utils.h"
|
|
|
|
#include "gimp-parallel.h"
|
|
#include "gimp-utils.h" /* GIMP_TIMER */
|
|
#include "gimpasync.h"
|
|
#include "gimplineart.h"
|
|
#include "gimppickable.h"
|
|
#include "gimppickable-contiguous-region.h"
|
|
|
|
|
|
#define EPSILON 1e-6
|
|
|
|
#define PIXELS_PER_THREAD \
|
|
(/* each thread costs as much as */ 64.0 * 64.0 /* pixels */)
|
|
|
|
|
|
typedef struct
|
|
{
|
|
gint x;
|
|
gint y;
|
|
gint level;
|
|
} BorderPixel;
|
|
|
|
|
|
/* local function prototypes */
|
|
|
|
static const Babl * choose_format (GeglBuffer *buffer,
|
|
GimpSelectCriterion select_criterion,
|
|
gint *n_components,
|
|
gboolean *has_alpha);
|
|
static gfloat pixel_difference (const gfloat *col1,
|
|
const gfloat *col2,
|
|
gboolean antialias,
|
|
gfloat threshold,
|
|
gint n_components,
|
|
gboolean has_alpha,
|
|
gboolean select_transparent,
|
|
GimpSelectCriterion select_criterion);
|
|
static void push_segment (GQueue *segment_queue,
|
|
gint y,
|
|
gint old_y,
|
|
gint start,
|
|
gint end,
|
|
gint new_y,
|
|
gint new_start,
|
|
gint new_end);
|
|
static void pop_segment (GQueue *segment_queue,
|
|
gint *y,
|
|
gint *old_y,
|
|
gint *start,
|
|
gint *end);
|
|
static gboolean find_contiguous_segment (const gfloat *col,
|
|
GeglBuffer *src_buffer,
|
|
GeglSampler *src_sampler,
|
|
const GeglRectangle *src_extent,
|
|
GeglBuffer *mask_buffer,
|
|
const Babl *src_format,
|
|
const Babl *mask_format,
|
|
gint n_components,
|
|
gboolean has_alpha,
|
|
gboolean select_transparent,
|
|
GimpSelectCriterion select_criterion,
|
|
gboolean antialias,
|
|
gfloat threshold,
|
|
gint initial_x,
|
|
gint initial_y,
|
|
gint *start,
|
|
gint *end,
|
|
gfloat *row);
|
|
static void find_contiguous_region (GeglBuffer *src_buffer,
|
|
GeglBuffer *mask_buffer,
|
|
const Babl *format,
|
|
gint n_components,
|
|
gboolean has_alpha,
|
|
gboolean select_transparent,
|
|
GimpSelectCriterion select_criterion,
|
|
gboolean antialias,
|
|
gfloat threshold,
|
|
gboolean diagonal_neighbors,
|
|
gint x,
|
|
gint y,
|
|
const gfloat *col);
|
|
|
|
static void line_art_queue_pixel (GQueue *queue,
|
|
gint x,
|
|
gint y,
|
|
gint level);
|
|
|
|
|
|
/* public functions */
|
|
|
|
GeglBuffer *
|
|
gimp_pickable_contiguous_region_by_seed (GimpPickable *pickable,
|
|
gboolean antialias,
|
|
gfloat threshold,
|
|
gboolean select_transparent,
|
|
GimpSelectCriterion select_criterion,
|
|
gboolean diagonal_neighbors,
|
|
gint x,
|
|
gint y)
|
|
{
|
|
GeglBuffer *src_buffer;
|
|
GeglBuffer *mask_buffer;
|
|
const Babl *format;
|
|
GeglRectangle extent;
|
|
gint n_components;
|
|
gboolean has_alpha;
|
|
gfloat start_col[MAX_CHANNELS];
|
|
|
|
g_return_val_if_fail (GIMP_IS_PICKABLE (pickable), NULL);
|
|
|
|
gimp_pickable_flush (pickable);
|
|
src_buffer = gimp_pickable_get_buffer_with_effects (pickable);
|
|
|
|
format = choose_format (src_buffer, select_criterion,
|
|
&n_components, &has_alpha);
|
|
gegl_buffer_sample (src_buffer, x, y, NULL, start_col, format,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
|
|
if (has_alpha)
|
|
{
|
|
if (select_transparent)
|
|
{
|
|
/* don't select transparent regions if the start pixel isn't
|
|
* fully transparent
|
|
*/
|
|
if (start_col[n_components - 1] > 0)
|
|
select_transparent = FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
select_transparent = FALSE;
|
|
}
|
|
|
|
extent = *gegl_buffer_get_extent (src_buffer);
|
|
|
|
mask_buffer = gegl_buffer_new (&extent, babl_format ("Y float"));
|
|
|
|
if (x >= extent.x && x < (extent.x + extent.width) &&
|
|
y >= extent.y && y < (extent.y + extent.height))
|
|
{
|
|
GIMP_TIMER_START();
|
|
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, n_components, has_alpha,
|
|
select_transparent, select_criterion,
|
|
antialias, threshold, diagonal_neighbors,
|
|
x, y, start_col);
|
|
|
|
GIMP_TIMER_END("foo");
|
|
}
|
|
|
|
return mask_buffer;
|
|
}
|
|
|
|
GeglBuffer *
|
|
gimp_pickable_contiguous_region_by_color (GimpPickable *pickable,
|
|
gboolean antialias,
|
|
gfloat threshold,
|
|
gboolean select_transparent,
|
|
GimpSelectCriterion select_criterion,
|
|
GeglColor *color)
|
|
{
|
|
/* Scan over the pickable's active layer, finding pixels within the
|
|
* specified threshold from the given R, G, & B values. If
|
|
* antialiasing is on, use the same antialiasing scheme as in
|
|
* fuzzy_select. Modify the pickable's mask to reflect the
|
|
* additional selection
|
|
*/
|
|
GeglBuffer *src_buffer;
|
|
GeglBuffer *mask_buffer;
|
|
const Babl *format;
|
|
gint n_components;
|
|
gboolean has_alpha;
|
|
gfloat start_col[MAX_CHANNELS];
|
|
|
|
g_return_val_if_fail (GIMP_IS_PICKABLE (pickable), NULL);
|
|
g_return_val_if_fail (GEGL_IS_COLOR (color), NULL);
|
|
|
|
/* increase the threshold by EPSILON, to allow for conversion errors,
|
|
* especially when threshold == 0 (see issue #1554.) we need to do this
|
|
* here, but not in the other functions, since the input color gets converted
|
|
* to the format in which we perform the comparison through a different path
|
|
* than the pickable's pixels, which can introduce error.
|
|
*/
|
|
threshold += EPSILON;
|
|
|
|
gimp_pickable_flush (pickable);
|
|
|
|
src_buffer = gimp_pickable_get_buffer_with_effects (pickable);
|
|
|
|
format = choose_format (src_buffer, select_criterion,
|
|
&n_components, &has_alpha);
|
|
|
|
gegl_color_get_pixel (color, format, start_col);
|
|
|
|
if (has_alpha)
|
|
{
|
|
if (select_transparent)
|
|
{
|
|
/* don't select transparency if "color" isn't fully transparent
|
|
*/
|
|
if (start_col[n_components - 1] > 0.0)
|
|
select_transparent = FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
select_transparent = FALSE;
|
|
}
|
|
|
|
mask_buffer = gegl_buffer_new (gegl_buffer_get_extent (src_buffer),
|
|
babl_format ("Y float"));
|
|
|
|
gegl_parallel_distribute_area (
|
|
gegl_buffer_get_extent (src_buffer), PIXELS_PER_THREAD,
|
|
[=] (const GeglRectangle *area)
|
|
{
|
|
GeglBufferIterator *iter;
|
|
|
|
iter = gegl_buffer_iterator_new (src_buffer,
|
|
area, 0, format,
|
|
GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 2);
|
|
|
|
gegl_buffer_iterator_add (iter, mask_buffer,
|
|
area, 0, babl_format ("Y float"),
|
|
GEGL_ACCESS_WRITE, GEGL_ABYSS_NONE);
|
|
|
|
while (gegl_buffer_iterator_next (iter))
|
|
{
|
|
const gfloat *src = (const gfloat *) iter->items[0].data;
|
|
gfloat *dest = ( gfloat *) iter->items[1].data;
|
|
gint count = iter->length;
|
|
|
|
while (count--)
|
|
{
|
|
/* Find how closely the colors match */
|
|
*dest = pixel_difference (start_col, src,
|
|
antialias,
|
|
threshold,
|
|
n_components,
|
|
has_alpha,
|
|
select_transparent,
|
|
select_criterion);
|
|
|
|
src += n_components;
|
|
dest += 1;
|
|
}
|
|
}
|
|
});
|
|
|
|
return mask_buffer;
|
|
}
|
|
|
|
GeglBuffer *
|
|
gimp_pickable_contiguous_region_by_line_art (GimpPickable *pickable,
|
|
GimpLineArt *line_art,
|
|
GeglBuffer *fill_buffer,
|
|
GeglColor *fill_color,
|
|
gfloat fill_threshold,
|
|
gint fill_offset_x,
|
|
gint fill_offset_y,
|
|
gint x,
|
|
gint y)
|
|
{
|
|
GeglBuffer *src_buffer;
|
|
GeglBuffer *mask_buffer;
|
|
const Babl *format = babl_format ("Y float");
|
|
gfloat *distmap = NULL;
|
|
GeglRectangle extent;
|
|
gboolean free_line_art = FALSE;
|
|
gboolean free_src_buffer = FALSE;
|
|
gboolean filled = FALSE;
|
|
guchar start_col;
|
|
|
|
g_return_val_if_fail (GIMP_IS_PICKABLE (pickable) || GIMP_IS_LINE_ART (line_art), NULL);
|
|
|
|
if (! line_art)
|
|
{
|
|
/* It is much better experience to pre-compute the line art,
|
|
* but it may not be always possible (for instance when
|
|
* selecting/filling through a PDB call).
|
|
*/
|
|
line_art = gimp_line_art_new ();
|
|
gimp_line_art_set_input (line_art, pickable);
|
|
free_line_art = TRUE;
|
|
}
|
|
|
|
src_buffer = gimp_line_art_get (line_art, &distmap);
|
|
g_return_val_if_fail (src_buffer && distmap, NULL);
|
|
|
|
if (fill_buffer != NULL)
|
|
{
|
|
GeglBufferIterator *gi;
|
|
const Babl *fill_format;
|
|
const GeglRectangle *fill_extent;
|
|
gfloat fill_col[MAX_CHANNELS];
|
|
gboolean has_alpha;
|
|
gint n_components;
|
|
|
|
fill_extent = gegl_buffer_get_extent (fill_buffer);
|
|
fill_format = choose_format (fill_buffer,
|
|
GIMP_SELECT_CRITERION_COMPOSITE,
|
|
&n_components, &has_alpha);
|
|
fill_format = babl_format_with_space (babl_format_get_encoding (fill_format),
|
|
gegl_buffer_get_format (fill_buffer));
|
|
gegl_color_get_pixel (fill_color, fill_format, fill_col);
|
|
|
|
src_buffer = gimp_gegl_buffer_dup (src_buffer);
|
|
gi = gegl_buffer_iterator_new (src_buffer, NULL, 0, NULL,
|
|
GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 2);
|
|
gegl_buffer_iterator_add (gi, fill_buffer,
|
|
GEGL_RECTANGLE (-fill_offset_x, -fill_offset_y,
|
|
fill_extent->width + fill_offset_x,
|
|
fill_extent->height + fill_offset_y),
|
|
0, fill_format, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
|
|
while (gegl_buffer_iterator_next (gi))
|
|
{
|
|
guchar *data = (guchar*) gi->items[0].data;
|
|
const gfloat *fill = (const gfloat *) gi->items[1].data;
|
|
gint k;
|
|
|
|
for (k = 0; k < gi->length; k++)
|
|
{
|
|
/* Only consider if the line art source hasn't filled yet,
|
|
* and also if this the fill target has full opacity.
|
|
*/
|
|
if (! *data &&
|
|
( ! has_alpha ||
|
|
fill[n_components - 1] == 1.0))
|
|
{
|
|
gfloat diff;
|
|
|
|
diff = pixel_difference (fill, fill_col,
|
|
FALSE,
|
|
fill_threshold,
|
|
n_components, has_alpha, FALSE,
|
|
GIMP_SELECT_CRITERION_COMPOSITE);
|
|
|
|
/* Make the additional fill pixel a closure pixel. */
|
|
if (diff == 1.0)
|
|
*data = 2;
|
|
}
|
|
|
|
data++;
|
|
fill += n_components;
|
|
}
|
|
}
|
|
|
|
free_src_buffer = TRUE;
|
|
}
|
|
|
|
gegl_buffer_sample (src_buffer, x, y, NULL, &start_col, NULL,
|
|
GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
|
|
|
|
extent = *gegl_buffer_get_extent (src_buffer);
|
|
|
|
mask_buffer = gegl_buffer_new (&extent, format);
|
|
|
|
if (start_col)
|
|
{
|
|
if (start_col == 1)
|
|
{
|
|
/* As a special exception, if you fill over a line art pixel, only
|
|
* fill the pixel and exit
|
|
*/
|
|
gfloat col = 1.0;
|
|
|
|
gegl_buffer_set (mask_buffer, GEGL_RECTANGLE (x, y, 1, 1),
|
|
0, format, &col, GEGL_AUTO_ROWSTRIDE);
|
|
}
|
|
else /* start_col == 2 */
|
|
{
|
|
/* If you fill over a closure pixel, let's fill on all sides
|
|
* of the start point. Otherwise we get a very weird result
|
|
* with only a single pixel filled in the middle of an empty
|
|
* region (since closure pixels are invisible by nature).
|
|
*/
|
|
gfloat col = 0.0;
|
|
|
|
if (x - 1 >= extent.x && x - 1 < extent.x + extent.width &&
|
|
y - 1 >= extent.y && y - 1 < (extent.y + extent.height))
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x - 1, y - 1, &col);
|
|
|
|
if (x - 1 >= extent.x && x - 1 < extent.x + extent.width &&
|
|
y >= extent.y && y < (extent.y + extent.height))
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x - 1, y, &col);
|
|
|
|
if (x - 1 >= extent.x && x - 1 < extent.x + extent.width &&
|
|
y + 1 >= extent.y && y + 1 < (extent.y + extent.height))
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x - 1, y + 1, &col);
|
|
|
|
if (x >= extent.x && x < extent.x + extent.width &&
|
|
y - 1 >= extent.y && y - 1 < (extent.y + extent.height))
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x, y - 1, &col);
|
|
|
|
if (x >= extent.x && x < extent.x + extent.width &&
|
|
y + 1 >= extent.y && y + 1 < (extent.y + extent.height))
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x, y + 1, &col);
|
|
|
|
if (x + 1 >= extent.x && x + 1 < extent.x + extent.width &&
|
|
y - 1 >= extent.y && y - 1 < (extent.y + extent.height))
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x + 1, y - 1, &col);
|
|
|
|
if (x + 1 >= extent.x && x + 1 < extent.x + extent.width &&
|
|
y >= extent.y && y < (extent.y + extent.height))
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x + 1, y, &col);
|
|
|
|
if (x + 1 >= extent.x && x + 1 < extent.x + extent.width &&
|
|
y + 1 >= extent.y && y + 1 < (extent.y + extent.height))
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x + 1, y + 1, &col);
|
|
|
|
filled = TRUE;
|
|
}
|
|
}
|
|
else if (x >= extent.x && x < (extent.x + extent.width) &&
|
|
y >= extent.y && y < (extent.y + extent.height))
|
|
{
|
|
gfloat col = 0.0;
|
|
|
|
find_contiguous_region (src_buffer, mask_buffer,
|
|
format, 1, FALSE,
|
|
FALSE, GIMP_SELECT_CRITERION_COMPOSITE,
|
|
FALSE, 0.0, FALSE,
|
|
x, y, &col);
|
|
filled = TRUE;
|
|
}
|
|
|
|
if (filled)
|
|
{
|
|
GQueue *queue = g_queue_new ();
|
|
gfloat *mask;
|
|
gint width = gegl_buffer_get_width (src_buffer);
|
|
gint height = gegl_buffer_get_height (src_buffer);
|
|
gint line_art_max_grow;
|
|
gint nx, ny;
|
|
|
|
GIMP_TIMER_START();
|
|
/* The last step of the line art algorithm is to make sure that
|
|
* selections does not leave "holes" between its borders and the
|
|
* line arts, while not stepping over as well.
|
|
* I used to run the "gegl:watershed-transform" operation to flood
|
|
* the stroke pixels, but for such simple need, this simple code
|
|
* is so much faster while producing better results.
|
|
*/
|
|
mask = g_new (gfloat, width * height);
|
|
gegl_buffer_get (mask_buffer, NULL, 1.0, NULL,
|
|
mask, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
|
|
|
|
for (y = 0; y < height; y++)
|
|
for (x = 0; x < width; x++)
|
|
{
|
|
if (distmap[x + y * width] == 1.0)
|
|
{
|
|
if (x > 0)
|
|
{
|
|
nx = x - 1;
|
|
if (y > 0)
|
|
{
|
|
ny = y - 1;
|
|
if (mask[nx + ny * width] != 0.0)
|
|
{
|
|
line_art_queue_pixel (queue, x, y, 1);
|
|
continue;
|
|
}
|
|
}
|
|
ny = y;
|
|
if (mask[nx + ny * width] != 0.0)
|
|
{
|
|
line_art_queue_pixel (queue, x, y, 1);
|
|
continue;
|
|
}
|
|
if (y < height - 1)
|
|
{
|
|
ny = y + 1;
|
|
if (mask[nx + ny * width] != 0.0)
|
|
{
|
|
line_art_queue_pixel (queue, x, y, 1);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
if (x < width - 1)
|
|
{
|
|
nx = x + 1;
|
|
if (y > 0)
|
|
{
|
|
ny = y - 1;
|
|
if (mask[nx + ny * width] != 0.0)
|
|
{
|
|
line_art_queue_pixel (queue, x, y, 1);
|
|
continue;
|
|
}
|
|
}
|
|
ny = y;
|
|
if (mask[nx + ny * width] != 0.0)
|
|
{
|
|
line_art_queue_pixel (queue, x, y, 1);
|
|
continue;
|
|
}
|
|
if (y < height - 1)
|
|
{
|
|
ny = y + 1;
|
|
if (mask[nx + ny * width] != 0.0)
|
|
{
|
|
line_art_queue_pixel (queue, x, y, 1);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
nx = x;
|
|
if (y > 0)
|
|
{
|
|
ny = y - 1;
|
|
if (mask[nx + ny * width] != 0.0)
|
|
{
|
|
line_art_queue_pixel (queue, x, y, 1);
|
|
continue;
|
|
}
|
|
}
|
|
if (y < height - 1)
|
|
{
|
|
ny = y + 1;
|
|
if (mask[nx + ny * width] != 0.0)
|
|
{
|
|
line_art_queue_pixel (queue, x, y, 1);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
g_object_get (line_art,
|
|
"max-grow", &line_art_max_grow,
|
|
(void *) NULL);
|
|
while (! g_queue_is_empty (queue))
|
|
{
|
|
BorderPixel *c = (BorderPixel *) g_queue_pop_head (queue);
|
|
|
|
if (mask[c->x + c->y * width] != 1.0)
|
|
{
|
|
mask[c->x + c->y * width] = 1.0;
|
|
if (c->level >= line_art_max_grow)
|
|
{
|
|
/* Do not overflood under line arts. */
|
|
g_free (c);
|
|
continue;
|
|
}
|
|
if (c->x > 0)
|
|
{
|
|
nx = c->x - 1;
|
|
if (c->y > 0)
|
|
{
|
|
ny = c->y - 1;
|
|
if (mask[nx + ny * width] == 0.0 &&
|
|
distmap[nx + ny * width] > distmap[c->x + c->y * width])
|
|
line_art_queue_pixel (queue, nx, ny, c->level + 1);
|
|
}
|
|
ny = c->y;
|
|
if (mask[nx + ny * width] == 0.0 &&
|
|
distmap[nx + ny * width] > distmap[c->x + c->y * width])
|
|
line_art_queue_pixel (queue, nx, ny, c->level + 1);
|
|
if (c->y < height - 1)
|
|
{
|
|
ny = c->y + 1;
|
|
if (mask[nx + ny * width] == 0.0 &&
|
|
distmap[nx + ny * width] > distmap[c->x + c->y * width])
|
|
line_art_queue_pixel (queue, nx, ny, c->level + 1);
|
|
}
|
|
}
|
|
if (c->x < width - 1)
|
|
{
|
|
nx = c->x + 1;
|
|
if (c->y > 0)
|
|
{
|
|
ny = c->y - 1;
|
|
if (mask[nx + ny * width] == 0.0 &&
|
|
distmap[nx + ny * width] > distmap[c->x + c->y * width])
|
|
line_art_queue_pixel (queue, nx, ny, c->level + 1);
|
|
}
|
|
ny = c->y;
|
|
if (mask[nx + ny * width] == 0.0 &&
|
|
distmap[nx + ny * width] > distmap[c->x + c->y * width])
|
|
line_art_queue_pixel (queue, nx, ny, c->level + 1);
|
|
if (c->y < height - 1)
|
|
{
|
|
ny = c->y + 1;
|
|
if (mask[nx + ny * width] == 0.0 &&
|
|
distmap[nx + ny * width] > distmap[c->x + c->y * width])
|
|
line_art_queue_pixel (queue, nx, ny, c->level + 1);
|
|
}
|
|
}
|
|
nx = c->x;
|
|
if (c->y > 0)
|
|
{
|
|
ny = c->y - 1;
|
|
if (mask[nx + ny * width] == 0.0 &&
|
|
distmap[nx + ny * width] > distmap[c->x + c->y * width])
|
|
line_art_queue_pixel (queue, nx, ny, c->level + 1);
|
|
}
|
|
if (c->y < height - 1)
|
|
{
|
|
ny = c->y + 1;
|
|
if (mask[nx + ny * width] == 0.0 &&
|
|
distmap[nx + ny * width] > distmap[c->x + c->y * width])
|
|
line_art_queue_pixel (queue, nx, ny, c->level + 1);
|
|
}
|
|
}
|
|
g_free (c);
|
|
}
|
|
g_queue_free (queue);
|
|
gegl_buffer_set (mask_buffer, gegl_buffer_get_extent (mask_buffer),
|
|
0, NULL, mask, GEGL_AUTO_ROWSTRIDE);
|
|
g_free (mask);
|
|
|
|
GIMP_TIMER_END("watershed line art");
|
|
}
|
|
if (free_line_art)
|
|
g_clear_object (&line_art);
|
|
if (free_src_buffer)
|
|
g_object_unref (src_buffer);
|
|
|
|
return mask_buffer;
|
|
}
|
|
|
|
/* private functions */
|
|
|
|
static const Babl *
|
|
choose_format (GeglBuffer *buffer,
|
|
GimpSelectCriterion select_criterion,
|
|
gint *n_components,
|
|
gboolean *has_alpha)
|
|
{
|
|
const Babl *format = gegl_buffer_get_format (buffer);
|
|
|
|
*has_alpha = babl_format_has_alpha (format);
|
|
|
|
switch (select_criterion)
|
|
{
|
|
case GIMP_SELECT_CRITERION_COMPOSITE:
|
|
if (babl_format_is_palette (format))
|
|
format = babl_format ("R'G'B'A float");
|
|
else
|
|
format = gimp_babl_format (gimp_babl_format_get_base_type (format),
|
|
GIMP_PRECISION_FLOAT_NON_LINEAR,
|
|
*has_alpha,
|
|
NULL);
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_RGB_RED:
|
|
case GIMP_SELECT_CRITERION_RGB_GREEN:
|
|
case GIMP_SELECT_CRITERION_RGB_BLUE:
|
|
case GIMP_SELECT_CRITERION_ALPHA:
|
|
format = babl_format ("R'G'B'A float");
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_HSV_HUE:
|
|
case GIMP_SELECT_CRITERION_HSV_SATURATION:
|
|
case GIMP_SELECT_CRITERION_HSV_VALUE:
|
|
format = babl_format ("HSVA float");
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_LCH_LIGHTNESS:
|
|
format = babl_format ("CIE L alpha float");
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_LCH_CHROMA:
|
|
case GIMP_SELECT_CRITERION_LCH_HUE:
|
|
format = babl_format ("CIE LCH(ab) alpha float");
|
|
break;
|
|
|
|
default:
|
|
g_return_val_if_reached (NULL);
|
|
break;
|
|
}
|
|
|
|
*n_components = babl_format_get_n_components (format);
|
|
|
|
return format;
|
|
}
|
|
|
|
static gfloat
|
|
pixel_difference (const gfloat *col1,
|
|
const gfloat *col2,
|
|
gboolean antialias,
|
|
gfloat threshold,
|
|
gint n_components,
|
|
gboolean has_alpha,
|
|
gboolean select_transparent,
|
|
GimpSelectCriterion select_criterion)
|
|
{
|
|
gfloat max = 0.0;
|
|
|
|
/* if there is an alpha channel, never select transparent regions */
|
|
if (! select_transparent && has_alpha && col2[n_components - 1] == 0.0)
|
|
return 0.0;
|
|
|
|
if (select_transparent && has_alpha)
|
|
{
|
|
max = fabs (col1[n_components - 1] - col2[n_components - 1]);
|
|
}
|
|
else
|
|
{
|
|
gfloat diff;
|
|
gint b;
|
|
|
|
if (has_alpha)
|
|
n_components--;
|
|
|
|
switch (select_criterion)
|
|
{
|
|
case GIMP_SELECT_CRITERION_COMPOSITE:
|
|
for (b = 0; b < n_components; b++)
|
|
{
|
|
diff = fabs (col1[b] - col2[b]);
|
|
if (diff > max)
|
|
max = diff;
|
|
}
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_RGB_RED:
|
|
max = fabs (col1[0] - col2[0]);
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_RGB_GREEN:
|
|
max = fabs (col1[1] - col2[1]);
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_RGB_BLUE:
|
|
max = fabs (col1[2] - col2[2]);
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_ALPHA:
|
|
max = fabs (col1[3] - col2[3]);
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_HSV_HUE:
|
|
if (col1[1] > EPSILON)
|
|
{
|
|
if (col2[1] > EPSILON)
|
|
{
|
|
max = fabs (col1[0] - col2[0]);
|
|
max = MIN (max, 1.0 - max);
|
|
}
|
|
else
|
|
{
|
|
/* "infinite" difference. anything >> 1 will do. */
|
|
max = 10.0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (col2[1] > EPSILON)
|
|
{
|
|
/* "infinite" difference. anything >> 1 will do. */
|
|
max = 10.0;
|
|
}
|
|
else
|
|
{
|
|
max = 0.0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_HSV_SATURATION:
|
|
max = fabs (col1[1] - col2[1]);
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_HSV_VALUE:
|
|
max = fabs (col1[2] - col2[2]);
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_LCH_LIGHTNESS:
|
|
max = fabs (col1[0] - col2[0]) / 100.0;
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_LCH_CHROMA:
|
|
max = fabs (col1[1] - col2[1]) / 100.0;
|
|
break;
|
|
|
|
case GIMP_SELECT_CRITERION_LCH_HUE:
|
|
if (col1[1] > 100.0 * EPSILON)
|
|
{
|
|
if (col2[1] > 100.0 * EPSILON)
|
|
{
|
|
max = fabs (col1[2] - col2[2]) / 360.0;
|
|
max = MIN (max, 1.0 - max);
|
|
}
|
|
else
|
|
{
|
|
/* "infinite" difference. anything >> 1 will do. */
|
|
max = 10.0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (col2[1] > 100.0 * EPSILON)
|
|
{
|
|
/* "infinite" difference. anything >> 1 will do. */
|
|
max = 10.0;
|
|
}
|
|
else
|
|
{
|
|
max = 0.0;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (antialias && threshold > 0.0)
|
|
{
|
|
gfloat aa = 1.5 - (max / threshold);
|
|
|
|
if (aa <= 0.0)
|
|
return 0.0;
|
|
else if (aa < 0.5)
|
|
return aa * 2.0;
|
|
else
|
|
return 1.0;
|
|
}
|
|
else
|
|
{
|
|
if (max > threshold)
|
|
return 0.0;
|
|
else
|
|
return 1.0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
push_segment (GQueue *segment_queue,
|
|
gint y,
|
|
gint old_y,
|
|
gint start,
|
|
gint end,
|
|
gint new_y,
|
|
gint new_start,
|
|
gint new_end)
|
|
{
|
|
/* To avoid excessive memory allocation (y, old_y, start, end) tuples are
|
|
* stored in interleaved format:
|
|
*
|
|
* [y1] [old_y1] [start1] [end1] [y2] [old_y2] [start2] [end2]
|
|
*/
|
|
|
|
if (new_y != old_y)
|
|
{
|
|
/* If the new segment's y-coordinate is different than the old (source)
|
|
* segment's y-coordinate, push the entire segment.
|
|
*/
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (new_y));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (y));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (new_start));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (new_end));
|
|
}
|
|
else
|
|
{
|
|
/* Otherwise, only push the set-difference between the new segment and
|
|
* the source segment (since we've already scanned the source segment.)
|
|
* Note that the `+ 1` and `- 1` terms of the end/start coordinates below
|
|
* are only necessary when `diagonal_neighbors` is on (and otherwise make
|
|
* the segments slightly larger than necessary), but, meh...
|
|
*/
|
|
if (new_start < start)
|
|
{
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (new_y));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (y));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (new_start));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (start + 1));
|
|
}
|
|
|
|
if (new_end > end)
|
|
{
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (new_y));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (y));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (end - 1));
|
|
g_queue_push_tail (segment_queue, GINT_TO_POINTER (new_end));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
pop_segment (GQueue *segment_queue,
|
|
gint *y,
|
|
gint *old_y,
|
|
gint *start,
|
|
gint *end)
|
|
{
|
|
*y = GPOINTER_TO_INT (g_queue_pop_head (segment_queue));
|
|
*old_y = GPOINTER_TO_INT (g_queue_pop_head (segment_queue));
|
|
*start = GPOINTER_TO_INT (g_queue_pop_head (segment_queue));
|
|
*end = GPOINTER_TO_INT (g_queue_pop_head (segment_queue));
|
|
}
|
|
|
|
/* #define FETCH_ROW 1 */
|
|
|
|
static gboolean
|
|
find_contiguous_segment (const gfloat *col,
|
|
GeglBuffer *src_buffer,
|
|
GeglSampler *src_sampler,
|
|
const GeglRectangle *src_extent,
|
|
GeglBuffer *mask_buffer,
|
|
const Babl *src_format,
|
|
const Babl *mask_format,
|
|
gint n_components,
|
|
gboolean has_alpha,
|
|
gboolean select_transparent,
|
|
GimpSelectCriterion select_criterion,
|
|
gboolean antialias,
|
|
gfloat threshold,
|
|
gint initial_x,
|
|
gint initial_y,
|
|
gint *start,
|
|
gint *end,
|
|
gfloat *row)
|
|
{
|
|
gfloat *s;
|
|
gfloat mask_row_buf[src_extent->width];
|
|
gfloat *mask_row = mask_row_buf - src_extent->x;
|
|
gfloat diff;
|
|
|
|
#ifdef FETCH_ROW
|
|
gegl_buffer_get (src_buffer, GEGL_RECTANGLE (0, initial_y, width, 1), 1.0,
|
|
src_format,
|
|
row, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
|
|
s = row + initial_x * n_components;
|
|
#else
|
|
s = (gfloat *) g_alloca (n_components * sizeof (gfloat));
|
|
|
|
gegl_sampler_get (src_sampler,
|
|
initial_x, initial_y, NULL, s, GEGL_ABYSS_NONE);
|
|
#endif
|
|
|
|
diff = pixel_difference (col, s, antialias, threshold,
|
|
n_components, has_alpha, select_transparent,
|
|
select_criterion);
|
|
|
|
/* check the starting pixel */
|
|
if (! diff)
|
|
return FALSE;
|
|
|
|
mask_row[initial_x] = diff;
|
|
|
|
*start = initial_x - 1;
|
|
#ifdef FETCH_ROW
|
|
s = row + *start * n_components;
|
|
#endif
|
|
|
|
while (*start >= src_extent->x)
|
|
{
|
|
#ifndef FETCH_ROW
|
|
gegl_sampler_get (src_sampler,
|
|
*start, initial_y, NULL, s, GEGL_ABYSS_NONE);
|
|
#endif
|
|
|
|
diff = pixel_difference (col, s, antialias, threshold,
|
|
n_components, has_alpha, select_transparent,
|
|
select_criterion);
|
|
if (diff == 0.0)
|
|
break;
|
|
|
|
mask_row[*start] = diff;
|
|
|
|
(*start)--;
|
|
#ifdef FETCH_ROW
|
|
s -= n_components;
|
|
#endif
|
|
}
|
|
|
|
*end = initial_x + 1;
|
|
#ifdef FETCH_ROW
|
|
s = row + *end * n_components;
|
|
#endif
|
|
|
|
while (*end < src_extent->x + src_extent->width)
|
|
{
|
|
#ifndef FETCH_ROW
|
|
gegl_sampler_get (src_sampler,
|
|
*end, initial_y, NULL, s, GEGL_ABYSS_NONE);
|
|
#endif
|
|
|
|
diff = pixel_difference (col, s, antialias, threshold,
|
|
n_components, has_alpha, select_transparent,
|
|
select_criterion);
|
|
if (diff == 0.0)
|
|
break;
|
|
|
|
mask_row[*end] = diff;
|
|
|
|
(*end)++;
|
|
#ifdef FETCH_ROW
|
|
s += n_components;
|
|
#endif
|
|
}
|
|
|
|
gegl_buffer_set (mask_buffer, GEGL_RECTANGLE (*start + 1, initial_y,
|
|
*end - *start - 1, 1),
|
|
0, mask_format, &mask_row[*start + 1],
|
|
GEGL_AUTO_ROWSTRIDE);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
find_contiguous_region (GeglBuffer *src_buffer,
|
|
GeglBuffer *mask_buffer,
|
|
const Babl *format,
|
|
gint n_components,
|
|
gboolean has_alpha,
|
|
gboolean select_transparent,
|
|
GimpSelectCriterion select_criterion,
|
|
gboolean antialias,
|
|
gfloat threshold,
|
|
gboolean diagonal_neighbors,
|
|
gint x,
|
|
gint y,
|
|
const gfloat *col)
|
|
{
|
|
const Babl *mask_format = babl_format ("Y float");
|
|
GeglSampler *src_sampler;
|
|
const GeglRectangle *src_extent;
|
|
gint old_y;
|
|
gint start, end;
|
|
gint new_start, new_end;
|
|
GQueue *segment_queue;
|
|
gfloat *row = NULL;
|
|
|
|
src_extent = gegl_buffer_get_extent (src_buffer);
|
|
|
|
#ifdef FETCH_ROW
|
|
row = g_new (gfloat, src_extent->width * n_components);
|
|
#endif
|
|
|
|
src_sampler = gegl_buffer_sampler_new (src_buffer,
|
|
format, GEGL_SAMPLER_NEAREST);
|
|
|
|
segment_queue = g_queue_new ();
|
|
|
|
push_segment (segment_queue,
|
|
y, /* dummy values: */ -1, 0, 0,
|
|
y, x - 1, x + 1);
|
|
|
|
do
|
|
{
|
|
pop_segment (segment_queue,
|
|
&y, &old_y, &start, &end);
|
|
|
|
for (x = start + 1; x < end; x++)
|
|
{
|
|
gfloat val;
|
|
|
|
gegl_buffer_get (mask_buffer, GEGL_RECTANGLE (x, y, 1, 1), 1.0,
|
|
mask_format, &val, GEGL_AUTO_ROWSTRIDE,
|
|
GEGL_ABYSS_NONE);
|
|
|
|
if (val != 0.0)
|
|
{
|
|
/* If the current pixel is selected, then we've already visited
|
|
* the next pixel. (Note that we assume that the maximal image
|
|
* width is sufficiently low that `x` won't overflow.)
|
|
*/
|
|
x++;
|
|
continue;
|
|
}
|
|
|
|
if (! find_contiguous_segment (col,
|
|
src_buffer, src_sampler, src_extent,
|
|
mask_buffer,
|
|
format, mask_format,
|
|
n_components,
|
|
has_alpha,
|
|
select_transparent, select_criterion,
|
|
antialias, threshold, x, y,
|
|
&new_start, &new_end,
|
|
row))
|
|
continue;
|
|
|
|
/* We can skip directly to `new_end + 1` on the next iteration, since
|
|
* we've just selected all pixels in the range `[x, new_end)`, and
|
|
* the pixel at `new_end` is above threshold. (Note that we assume
|
|
* that the maximal image width is sufficiently low that `x` won't
|
|
* overflow.)
|
|
*/
|
|
x = new_end;
|
|
|
|
if (diagonal_neighbors)
|
|
{
|
|
if (new_start >= src_extent->x)
|
|
new_start--;
|
|
|
|
if (new_end < src_extent->x + src_extent->width)
|
|
new_end++;
|
|
}
|
|
|
|
if (y + 1 < src_extent->y + src_extent->height)
|
|
{
|
|
push_segment (segment_queue,
|
|
y, old_y, start, end,
|
|
y + 1, new_start, new_end);
|
|
}
|
|
|
|
if (y - 1 >= src_extent->y)
|
|
{
|
|
push_segment (segment_queue,
|
|
y, old_y, start, end,
|
|
y - 1, new_start, new_end);
|
|
}
|
|
|
|
}
|
|
}
|
|
while (! g_queue_is_empty (segment_queue));
|
|
|
|
g_queue_free (segment_queue);
|
|
|
|
g_object_unref (src_sampler);
|
|
|
|
#ifdef FETCH_ROW
|
|
g_free (row);
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
line_art_queue_pixel (GQueue *queue,
|
|
gint x,
|
|
gint y,
|
|
gint level)
|
|
{
|
|
BorderPixel *p = g_new (BorderPixel, 1);
|
|
|
|
p->x = x;
|
|
p->y = y;
|
|
p->level = level;
|
|
|
|
g_queue_push_head (queue, p);
|
|
}
|
|
|
|
} /* extern "C" */
|