app: improve gimp_gegl_mask_combine_ellipse[_rect]()

Improve gimp_gegl_mask_combine_ellipse_rect() -- the funciton
responsible for rendering ellipse/rounded-rectangle selections.

Most notably, this commit significantly improves the function's
performance, by identifying whole tiles, whole rows, or parts of a
row, that are fully inside, or fully outside, the ellipse, and
filling them in bulk, instead of calculating the anti-aliasing
value at each pixel, which is now only done along the
circumference.

This commit also improves anti-aliasing, by more accurately
approximating the distance from a pixel to the ellipse, and by
normalizing the distance according to the pixel's cross-section
length in the direction of the said point.  In particular, we
guarantee that pixels that are fully inside/outside the ellipse
have a value of 1/0, respectively, facilitating the aforementioned
optimization.

Additionally, this commit fixes various edge cases where several
primitives coincide at a single pixel (in the rounded-rectangle
case), adds support for CHANNEL_OP_INTERSECT, and parallelizes
processing.
This commit is contained in:
Ell
2019-03-20 09:27:08 -04:00
parent 8a6e1c907d
commit 1044342393
4 changed files with 376 additions and 299 deletions

View File

@ -89,24 +89,6 @@ gimp_channel_combine_rect (GimpChannel *mask,
gimp_drawable_update (GIMP_DRAWABLE (mask), x, y, w, h);
}
/**
* gimp_channel_combine_ellipse:
* @mask: the channel with which to combine the ellipse
* @op: whether to replace, add to, or subtract from the current
* contents
* @x: x coordinate of upper left corner of ellipse
* @y: y coordinate of upper left corner of ellipse
* @w: width of ellipse bounding box
* @h: height of ellipse bounding box
* @antialias: if %TRUE, antialias the ellipse
*
* Mainly used for elliptical selections. If @op is
* %GIMP_CHANNEL_OP_REPLACE or %GIMP_CHANNEL_OP_ADD, sets pixels
* within the ellipse to 255. If @op is %GIMP_CHANNEL_OP_SUBTRACT,
* sets pixels within to zero. If @antialias is %TRUE, pixels that
* impinge on the edge of the ellipse are set to intermediate values,
* depending on how much they overlap.
**/
void
gimp_channel_combine_ellipse (GimpChannel *mask,
GimpChannelOps op,
@ -120,26 +102,6 @@ gimp_channel_combine_ellipse (GimpChannel *mask,
w / 2.0, h / 2.0, antialias);
}
/**
* gimp_channel_combine_ellipse_rect:
* @mask: the channel with which to combine the elliptic rect
* @op: whether to replace, add to, or subtract from the current
* contents
* @x: x coordinate of upper left corner of bounding rect
* @y: y coordinate of upper left corner of bounding rect
* @w: width of bounding rect
* @h: height of bounding rect
* @a: elliptic a-constant applied to corners
* @b: elliptic b-constant applied to corners
* @antialias: if %TRUE, antialias the elliptic corners
*
* Used for rounded cornered rectangles and ellipses. If @op is
* %GIMP_CHANNEL_OP_REPLACE or %GIMP_CHANNEL_OP_ADD, sets pixels
* within the ellipse to 255. If @op is %GIMP_CHANNEL_OP_SUBTRACT,
* sets pixels within to zero. If @antialias is %TRUE, pixels that
* impinge on the edge of the ellipse are set to intermediate values,
* depending on how much they overlap.
**/
void
gimp_channel_combine_ellipse_rect (GimpChannel *mask,
GimpChannelOps op,
@ -147,20 +109,19 @@ gimp_channel_combine_ellipse_rect (GimpChannel *mask,
gint y,
gint w,
gint h,
gdouble a,
gdouble b,
gdouble rx,
gdouble ry,
gboolean antialias)
{
GeglBuffer *buffer;
g_return_if_fail (GIMP_IS_CHANNEL (mask));
g_return_if_fail (a >= 0.0 && b >= 0.0);
g_return_if_fail (op != GIMP_CHANNEL_OP_INTERSECT);
buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (mask));
if (! gimp_gegl_mask_combine_ellipse_rect (buffer, op, x, y, w, h,
a, b, antialias))
rx, ry, antialias))
return;
gimp_rectangle_intersect (x, y, w, h,

View File

@ -38,8 +38,8 @@ void gimp_channel_combine_ellipse_rect (GimpChannel *mask,
gint y,
gint w,
gint h,
gdouble a,
gdouble b,
gdouble rx,
gdouble ry,
gboolean antialias);
void gimp_channel_combine_mask (GimpChannel *mask,
GimpChannel *add_on,

View File

@ -34,6 +34,12 @@ extern "C"
#include "gimp-gegl-mask-combine.h"
#define EPSILON 1e-6
#define PIXELS_PER_THREAD \
(/* each thread costs as much as */ 64.0 * 64.0 /* pixels */)
gboolean
gimp_gegl_mask_combine_rect (GeglBuffer *mask,
GimpChannelOps op,
@ -75,24 +81,6 @@ gimp_gegl_mask_combine_rect (GeglBuffer *mask,
return TRUE;
}
/**
* gimp_gegl_mask_combine_ellipse:
* @mask: the channel with which to combine the ellipse
* @op: whether to replace, add to, or subtract from the current
* contents
* @x: x coordinate of upper left corner of ellipse
* @y: y coordinate of upper left corner of ellipse
* @w: width of ellipse bounding box
* @h: height of ellipse bounding box
* @antialias: if %TRUE, antialias the ellipse
*
* Mainly used for elliptical selections. If @op is
* %GIMP_CHANNEL_OP_REPLACE or %GIMP_CHANNEL_OP_ADD, sets pixels
* within the ellipse to 255. If @op is %GIMP_CHANNEL_OP_SUBTRACT,
* sets pixels within to zero. If @antialias is %TRUE, pixels that
* impinge on the edge of the ellipse are set to intermediate values,
* depending on how much they overlap.
**/
gboolean
gimp_gegl_mask_combine_ellipse (GeglBuffer *mask,
GimpChannelOps op,
@ -106,77 +94,6 @@ gimp_gegl_mask_combine_ellipse (GeglBuffer *mask,
w / 2.0, h / 2.0, antialias);
}
static void
gimp_gegl_mask_combine_span (gfloat *data,
GimpChannelOps op,
gint x1,
gint x2,
gfloat value)
{
if (x2 <= x1)
return;
switch (op)
{
case GIMP_CHANNEL_OP_ADD:
case GIMP_CHANNEL_OP_REPLACE:
if (value == 1.0)
{
while (x1 < x2)
data[x1++] = 1.0;
}
else
{
while (x1 < x2)
{
const gfloat val = data[x1] + value;
data[x1++] = val > 1.0 ? 1.0 : val;
}
}
break;
case GIMP_CHANNEL_OP_SUBTRACT:
if (value == 1.0)
{
while (x1 < x2)
data[x1++] = 0.0;
}
else
{
while (x1 < x2)
{
const gfloat val = data[x1] - value;
data[x1++] = val > 0.0 ? val : 0.0;
}
}
break;
case GIMP_CHANNEL_OP_INTERSECT:
/* Should not happen */
break;
}
}
/**
* gimp_gegl_mask_combine_ellipse_rect:
* @mask: the channel with which to combine the elliptic rect
* @op: whether to replace, add to, or subtract from the current
* contents
* @x: x coordinate of upper left corner of bounding rect
* @y: y coordinate of upper left corner of bounding rect
* @w: width of bounding rect
* @h: height of bounding rect
* @a: elliptic a-constant applied to corners
* @b: elliptic b-constant applied to corners
* @antialias: if %TRUE, antialias the elliptic corners
*
* Used for rounded cornered rectangles and ellipses. If @op is
* %GIMP_CHANNEL_OP_REPLACE or %GIMP_CHANNEL_OP_ADD, sets pixels
* within the ellipse to 255. If @op is %GIMP_CHANNEL_OP_SUBTRACT,
* sets pixels within to zero. If @antialias is %TRUE, pixels that
* impinge on the edge of the ellipse are set to intermediate values,
* depending on how much they overlap.
**/
gboolean
gimp_gegl_mask_combine_ellipse_rect (GeglBuffer *mask,
GimpChannelOps op,
@ -184,200 +101,399 @@ gimp_gegl_mask_combine_ellipse_rect (GeglBuffer *mask,
gint y,
gint w,
gint h,
gdouble a,
gdouble b,
gdouble rx,
gdouble ry,
gboolean antialias)
{
GeglBufferIterator *iter;
GeglRectangle *roi;
gdouble a_sqr;
gdouble b_sqr;
gdouble ellipse_center_x;
gint x0, y0;
gint width, height;
GeglRectangle rect;
const Babl *format;
gint bpp;
gfloat one_f = 1.0f;
gpointer one;
gdouble cx;
gdouble cy;
gint left;
gint right;
gint top;
gint bottom;
g_return_val_if_fail (GEGL_IS_BUFFER (mask), FALSE);
g_return_val_if_fail (a >= 0.0 && b >= 0.0, FALSE);
g_return_val_if_fail (op != GIMP_CHANNEL_OP_INTERSECT, FALSE);
/* Make sure the elliptic corners fit into the rect */
a = MIN (a, w / 2.0);
b = MIN (b, h / 2.0);
if (rx <= EPSILON || ry <= EPSILON)
return gimp_gegl_mask_combine_rect (mask, op, x, y, w, h);
a_sqr = SQR (a);
b_sqr = SQR (b);
left = x;
right = x + w;
top = y;
bottom = y + h;
if (! gimp_rectangle_intersect (x, y, w, h,
0, 0,
gegl_buffer_get_width (mask),
gegl_buffer_get_height (mask),
&x0, &y0, &width, &height))
return FALSE;
cx = (left + right) / 2.0;
cy = (top + bottom) / 2.0;
ellipse_center_x = x + a;
rx = MIN (rx, w / 2.0);
ry = MIN (ry, h / 2.0);
iter = gegl_buffer_iterator_new (mask,
GEGL_RECTANGLE (x0, y0, width, height), 0,
babl_format ("Y float"),
GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1);
roi = &iter->items[0].roi;
while (gegl_buffer_iterator_next (iter))
if (! gegl_rectangle_intersect (&rect,
GEGL_RECTANGLE (x, y, w, h),
gegl_buffer_get_abyss (mask)))
{
gfloat *data = (gfloat *) iter->items[0].data;
gint py;
return FALSE;
}
for (py = roi->y;
py < roi->y + roi->height;
py++, data += roi->width)
format = gegl_buffer_get_format (mask);
if (antialias)
{
format = gimp_babl_format_change_component_type (
format, GIMP_COMPONENT_TYPE_FLOAT);
}
bpp = babl_format_get_bytes_per_pixel (format);
one = g_alloca (bpp);
babl_process (babl_fish ("Y float", format), &one_f, one, 1);
/* coordinate-system transforms. (x, y) coordinates are in the image
* coordinate-system, and (u, v) coordinates are in a coordinate-system
* aligned with the center of one of the elliptic corners, with the positive
* directions pointing away from the rectangle. when converting from (x, y)
* to (u, v), we use the closest elliptic corner.
*/
auto x_to_u = [=] (gdouble x)
{
if (x < cx)
return (left + rx) - x;
else
return x - (right - rx);
};
auto y_to_v = [=] (gdouble y)
{
if (y < cy)
return (top + ry) - y;
else
return y - (bottom - ry);
};
auto u_to_x_left = [=] (gdouble u)
{
return (left + rx) - u;
};
auto u_to_x_right = [=] (gdouble u)
{
return (right - rx) + u;
};
/* intersection of a horizontal line with the ellipse */
auto v_to_u = [=] (gdouble v)
{
if (v > 0.0)
return sqrt (MAX (SQR (rx) - SQR (rx * v / ry), 0.0));
else
return rx;
};
/* intersection of a vertical line with the ellipse */
auto u_to_v = [=] (gdouble u)
{
if (u > 0.0)
return sqrt (MAX (SQR (ry) - SQR (ry * u / rx), 0.0));
else
return ry;
};
/* signed, normalized distance of a point from the ellipse's circumference.
* the sign of the result determines if the point is inside (positive) or
* outside (negative) the ellipse. the result is normalized to the cross-
* section length of a pixel, in the direction of the closest point along the
* ellipse.
*
* we use the following method to approximate the distance: pass horizontal
* and vertical lines at the given point, P, and find their (positive) points
* of intersection with the ellipse, A and B. the segment AB is an
* approximation of the corresponding elliptic arc (see bug #147836). find
* the closest point, C, to P, along the segment AB. find the (positive)
* point of intersection, Q, of the line PC and the ellipse. Q is an
* approximation for the closest point to P along the ellipse, and the
* approximated distance is the distance from P to Q.
*/
auto ellipse_distance = [=] (gdouble u,
gdouble v)
{
gdouble du;
gdouble dv;
gdouble t;
gdouble a, b, c;
gdouble d;
u = MAX (u, 0.0);
v = MAX (v, 0.0);
du = v_to_u (v) - u;
dv = u_to_v (u) - v;
t = SQR (du) / (SQR (du) + SQR (dv));
du *= 1.0 - t;
dv *= t;
v *= rx / ry;
dv *= rx / ry;
a = SQR (du) + SQR (dv);
b = u * du + v * dv;
c = SQR (u) + SQR (v) - SQR (rx);
if (a <= EPSILON)
return 0.0;
if (c < 0.0)
t = (-b + sqrt (MAX (SQR (b) - a * c, 0.0))) / a;
else
t = (-b - sqrt (MAX (SQR (b) - a * c, 0.0))) / a;
dv *= ry / rx;
d = sqrt (SQR (du * t) + SQR (dv * t));
if (c > 0.0)
d = -d;
d /= sqrt (SQR (MIN (du / dv, dv / du)) + 1.0);
return d;
};
/* anti-aliased value of a pixel */
auto pixel_value = [=] (gint x,
gint y)
{
gdouble u = x_to_u (x + 0.5);
gdouble v = y_to_v (y + 0.5);
gdouble d = ellipse_distance (u, v);
/* use the distance of the pixel's center from the ellipse to approximate
* the coverage
*/
d = CLAMP (0.5 + d, 0.0, 1.0);
/* we're at the horizontal boundary of an elliptic corner */
if (u < 0.5)
d = d * (0.5 + u) + (0.5 - u);
/* we're at the vertical boundary of an elliptic corner */
if (v < 0.5)
d = d * (0.5 + v) + (0.5 - v);
/* opposite horizontal corners intersect the pixel */
if (x == (right - 1) - (x - left))
d = 2.0 * d - 1.0;
/* opposite vertical corners intersect the pixel */
if (y == (bottom - 1) - (y - top))
d = 2.0 * d - 1.0;
return d;
};
auto ellipse_range = [=] (gdouble y,
gdouble *x0,
gdouble *x1)
{
gdouble u = v_to_u (y_to_v (y));
*x0 = u_to_x_left (u);
*x1 = u_to_x_right (u);
};
auto fill0 = [=] (gpointer dest,
gint n)
{
switch (op)
{
case GIMP_CHANNEL_OP_REPLACE:
case GIMP_CHANNEL_OP_INTERSECT:
memset (dest, 0, bpp * n);
break;
case GIMP_CHANNEL_OP_ADD:
case GIMP_CHANNEL_OP_SUBTRACT:
break;
}
return (gpointer) ((guint8 *) dest + bpp * n);
};
auto fill1 = [=] (gpointer dest,
gint n)
{
switch (op)
{
case GIMP_CHANNEL_OP_REPLACE:
case GIMP_CHANNEL_OP_ADD:
gegl_memset_pattern (dest, one, bpp, n);
break;
case GIMP_CHANNEL_OP_SUBTRACT:
memset (dest, 0, bpp * n);
break;
case GIMP_CHANNEL_OP_INTERSECT:
break;
}
return (gpointer) ((guint8 *) dest + bpp * n);
};
auto set = [=] (gpointer dest,
gfloat value)
{
gfloat *p = (gfloat *) dest;
switch (op)
{
case GIMP_CHANNEL_OP_REPLACE:
*p = value;
break;
case GIMP_CHANNEL_OP_ADD:
*p = MIN (*p + value, 1.0);
break;
case GIMP_CHANNEL_OP_SUBTRACT:
*p = MAX (*p - value, 0.0);
break;
case GIMP_CHANNEL_OP_INTERSECT:
*p = MIN (*p, value);
break;
}
return (gpointer) (p + 1);
};
gegl_parallel_distribute_area (
&rect, PIXELS_PER_THREAD,
[=] (const GeglRectangle *area)
{
GeglBufferIterator *iter;
iter = gegl_buffer_iterator_new (
mask, area, 0, format,
op == GIMP_CHANNEL_OP_REPLACE ? GEGL_ACCESS_WRITE :
GEGL_ACCESS_READWRITE,
GEGL_ABYSS_NONE, 1);
while (gegl_buffer_iterator_next (iter))
{
const gint px = roi->x;
gdouble ellipse_center_y;
const GeglRectangle *roi = &iter->items[0].roi;
gpointer d = iter->items[0].data;
gdouble tx0, ty0;
gdouble tx1, ty1;
gdouble x0;
gdouble x1;
gint y;
if (py >= y + b && py < y + h - b)
{
/* we are on a row without rounded corners */
gimp_gegl_mask_combine_span (data, op, 0, roi->width, 1.0);
continue;
}
/* tile bounds */
tx0 = roi->x;
ty0 = roi->y;
/* Match the ellipse center y with our current y */
if (py < y + b)
{
ellipse_center_y = y + b;
}
else
{
ellipse_center_y = y + h - b;
}
tx1 = roi->x + roi->width;
ty1 = roi->y + roi->height;
/* For a non-antialiased ellipse, use the normal equation
* for an ellipse with an arbitrary center
* (ellipse_center_x, ellipse_center_y).
*/
if (! antialias)
{
gdouble half_ellipse_width_at_y;
gint x_start;
gint x_end;
tx0 += 0.5;
ty0 += 0.5;
half_ellipse_width_at_y =
sqrt (a_sqr -
a_sqr * SQR (py + 0.5f - ellipse_center_y) / b_sqr);
x_start = ROUND (ellipse_center_x - half_ellipse_width_at_y);
x_end = ROUND (ellipse_center_x + w - 2 * a +
half_ellipse_width_at_y);
gimp_gegl_mask_combine_span (data, op,
MAX (x_start - px, 0),
MIN (x_end - px, roi->width), 1.0);
tx1 -= 0.5;
ty1 -= 0.5;
}
else /* use antialiasing */
/* if the tile is fully inside/outside the ellipse, fill it with 1/0,
* respectively, and skip the rest.
*/
ellipse_range (ty0, &x0, &x1);
if (tx0 >= x0 && tx1 <= x1)
{
/* algorithm changed 7-18-04, because the previous one
* did not work well for eccentric ellipses. The new
* algorithm measures the distance to the ellipse in the
* X and Y directions, and uses trigonometry to
* approximate the distance to the ellipse as the
* distance to the hypotenuse of a right triangle whose
* legs are the X and Y distances. (WES)
*/
const gfloat yi = ABS (py + 0.5 - ellipse_center_y);
gfloat last_val = -1;
gint x_start = px;
gint cur_x;
ellipse_range (ty1, &x0, &x1);
for (cur_x = px; cur_x < (px + roi->width); cur_x++)
if (tx0 > x0 && tx1 <= x1)
{
gfloat xj;
gfloat xdist;
gfloat ydist;
gfloat r;
gfloat dist;
gfloat val;
fill1 (d, iter->length);
if (cur_x < x + w / 2)
continue;
}
}
else if (tx1 < x0 || tx0 > x1)
{
ellipse_range (ty1, &x0, &x1);
if (tx1 < x0 || tx0 > x1)
{
if ((ty0 - cy) * (ty1 - cy) >= 0.0)
{
ellipse_center_x = x + a;
}
else
{
ellipse_center_x = x + w - a;
}
fill0 (d, iter->length);
xj = ABS (cur_x + 0.5 - ellipse_center_x);
if (yi < b)
xdist = xj - a * sqrt (1 - SQR (yi) / b_sqr);
else
xdist = 1000.0; /* anything large will work */
if (xj < a)
ydist = yi - b * sqrt (1 - SQR (xj) / a_sqr);
else
ydist = 1000.0; /* anything large will work */
r = hypot (xdist, ydist);
if (r < 0.001)
dist = 0.0;
else
dist = xdist * ydist / r; /* trig formula for distance to
* hypotenuse
*/
if (xdist < 0.0)
dist *= -1;
if (dist < -0.5)
val = 1.0;
else if (dist < 0.5)
val = (1.0 - (dist + 0.5));
else
val = 0.0;
if (last_val != val)
{
if (last_val != -1)
gimp_gegl_mask_combine_span (data, op,
MAX (x_start - px, 0),
MIN (cur_x - px, roi->width),
last_val);
x_start = cur_x;
last_val = val;
}
/* skip ahead if we are on the straight segment
* between rounded corners
*/
if (cur_x >= x + a && cur_x < x + w - a)
{
gimp_gegl_mask_combine_span (data, op,
MAX (x_start - px, 0),
MIN (cur_x - px, roi->width),
last_val);
x_start = cur_x;
cur_x = x + w - a;
last_val = val = 1.0;
}
/* Time to change center? */
if (cur_x >= x + w / 2)
{
ellipse_center_x = x + w - a;
continue;
}
}
}
gimp_gegl_mask_combine_span (data, op,
MAX (x_start - px, 0),
MIN (cur_x - px, roi->width),
last_val);
for (y = roi->y; y < roi->y + roi->height; y++)
{
gint a, b;
if (antialias)
{
gdouble v = y_to_v (y + 0.5);
gdouble u0 = v_to_u (v - 0.5);
gdouble u1 = v_to_u (v + 0.5);
gint x;
a = floor (u_to_x_left (u0)) - roi->x;
a = CLAMP (a, 0, roi->width);
b = ceil (u_to_x_left (u1)) - roi->x;
b = CLAMP (b, a, roi->width);
d = fill0 (d, a);
for (x = roi->x + a; x < roi->x + b; x++)
d = set (d, pixel_value (x, y));
a = floor (u_to_x_right (u1)) - roi->x;
a = CLAMP (a, b, roi->width);
d = fill1 (d, a - b);
b = ceil (u_to_x_right (u0)) - roi->x;
b = CLAMP (b, a, roi->width);
for (x = roi->x + a; x < roi->x + b; x++)
d = set (d, pixel_value (x, y));
d = fill0 (d, roi->width - b);
}
else
{
ellipse_range (y + 0.5, &x0, &x1);
a = ceil (x0 - 0.5) - roi->x;
a = CLAMP (a, 0, roi->width);
b = floor (x1 + 0.5) - roi->x;
b = CLAMP (b, 0, roi->width);
d = fill0 (d, a);
d = fill1 (d, b - a);
d = fill0 (d, roi->width - b);
}
}
}
}
});
return TRUE;
}

View File

@ -38,8 +38,8 @@ gboolean gimp_gegl_mask_combine_ellipse_rect (GeglBuffer *mask,
gint y,
gint w,
gint h,
gdouble a,
gdouble b,
gdouble rx,
gdouble ry,
gboolean antialias);
gboolean gimp_gegl_mask_combine_buffer (GeglBuffer *mask,
GeglBuffer *add_on,