Hello World as a Point Filter Operation
Now let’s go in a completely different directly, much more raster-like. This time, we will implement a point filter operation.
Point Filter operations are a good choice when you don’t care about the surrounding pixels, i.e. that each pixel depends only on the pixel data at corresponding coordinates in the input.
My demo point filter operation will simply display a “H” in the middle of the input drawable, by inverting the color value linearly. It means we will also care about the pixel coordinates.
No-Op Point Filter Operation
But first of all, let’s create the most basic Point Filter operation possible:
#ifdef GEGL_PROPERTIES
#else
#define GEGL_OP_POINT_FILTER
#define GEGL_OP_NAME gimp_tutorial_point_filter_op
#define GEGL_OP_C_SOURCE gimp-tutorial-point-filter-op.c
#include "gegl-op.h"
static gboolean
process (GeglOperation *op,
void *in_buf,
void *out_buf,
glong n_pixels,
const GeglRectangle *roi,
gint level)
{
memcpy (out_buf, in_buf, n_pixels * 4 * sizeof (gfloat));
}
static void
gegl_op_class_init (GeglOpClass *klass)
{
GeglOperationClass *operation_class = GEGL_OPERATION_CLASS (klass);
GeglOperationPointFilterClass *point_filter_class = GEGL_OPERATION_POINT_FILTER_CLASS (klass);
point_filter_class->process = process;
gegl_operation_class_set_keys (operation_class,
"title", "Hello World Point Filter",
"name", "zemarmot:hello-world-point-filter",
"categories", "Artistic",
"description", "Hello World as a Point filter for official GIMP tutorial",
NULL);
}
#endif
You will recognize the similar structure as our Meta Operation, except
that GEGL_OP_POINT_FILTER
replaces GEGL_OP_META
. Also now we
implement the process
method of the GeglOperationPointFilterClass
.
Note that the format of in_buf
and out_buf
is “RGBA float”, i.e.
linear RGBA in floating point. This is the default format for point
filter operations. We could override this if we wanted, by
reimplementing the prepare()
method of GeglOperationClass
.
To make this a proper no-op operation, we just copy the input data into the output.
Processing the Input into the Output
NI will only process the input to produce some kind of “H” by inverting pixels in specific coordinates.
For this, we will only edit process()
:
static gboolean
process (GeglOperation *op,
void *in_buf,
void *out_buf,
glong n_pixels,
const GeglRectangle *roi,
gint level)
{
GeglNode *gegl = op->node;
GeglNode *input;
gfloat *GEGL_ALIGNED in_pixel;
gfloat *GEGL_ALIGNED out_pixel;
GeglRectangle irect;
glong i;
gboolean invert = FALSE;
gint x = roi->x;
gint y = roi->y;
input = gegl_node_get_input_proxy (gegl, "input");
irect = gegl_node_get_bounding_box (input);
in_pixel = in_buf;
out_pixel = out_buf;
for (i = 0; i < n_pixels; i++)
{
if (y >= irect.height / 4 + irect.y && y <= 3 * irect.height / 4 + irect.y &&
((x >= irect.width / 3 - irect.width / 100 + irect.x && x <= irect.width / 3 + irect.width / 100 + irect.x) ||
(x >= 2 * irect.width / 3 - irect.width / 100 + irect.x && x <= 2 * irect.width / 3 + irect.width / 100 + irect.x) ||
(x > irect.width / 3 + irect.width / 100 + irect.x && x < 2 * irect.width / 3 + irect.width / 100 + irect.x &&
y >= irect.height / 2 - irect.height / 100 + irect.y && y <= irect.height / 2 + irect.height / 100 + irect.y)))
invert = TRUE;
else
invert = FALSE;
out_pixel[0] = invert ? 1.0 - in_pixel[0] : in_pixel[0];
out_pixel[1] = invert ? 1.0 - in_pixel[1] : in_pixel[1];
out_pixel[2] = invert ? 1.0 - in_pixel[2] : in_pixel[2];
out_pixel[3] = in_pixel[3];
in_pixel += 4;
out_pixel += 4;
if (x + 1 >= roi->x + roi->width)
{
x = roi->x;
y++;
}
else
{
x++;
}
}
return TRUE;
}
This piece of code is interesting as it shows how you can use the ROI to figure out your coordinates on the image, which you can then compare to the input dimensions.
Then it shows how we change pixel values. Since we know that we are in
floating point (from 0.0 to 1.0 within the color space, though you can
go unbounded), every gfloat
is a channel value, and for every pixel,
we have 4 values one after the other (red, green, blue and finally
alpha).
Changing the input and output color model, space or TRC
In this demo, we made the choice to work in linear, though it may not be
the best choice. Say you decided to work in perceptual because inverting
colors gave nicer results?
As we said above, for this, we could reimplement the prepare()
method
of GeglOperationClass
:
static void prepare
(GeglOperation *operation)
{
const Babl *space = gegl_operation_get_source_space (operation, "input");
gegl_operation_set_format (operation, "input", babl_format_with_space ("R~G~B~A float", space));
gegl_operation_set_format (operation, "output", babl_format_with_space ("R~G~B~A float", space));
}
static void
gegl_op_class_init (GeglOpClass *klass)
{
GeglOperationClass *operation_class = GEGL_OPERATION_CLASS (klass);
GeglOperationPointFilterClass *point_filter_class = GEGL_OPERATION_POINT_FILTER_CLASS (klass);
operation_class->prepare = prepare;
point_filter_class->process = process;
gegl_operation_class_set_keys (operation_class,
"title", "Hello World Point Filter",
"name", "zemarmot:hello-world-point-filter",
"categories", "Artistic",
"description", "Hello World as a Point filter for official GIMP tutorial",
NULL);
}
What we are doing in prepare()
here is simply telling GEGL that:
- I want “input” pad data as RGBA in perceptual TRC (which means in fact using the sRGB curve in GEGL), but staying within my same color space.
- I will return “output” pad data as RGBA perceptual, also within the same color space.
This is an extremely important step, because then you are able to make
some assumption in your code. For instance in our process()
code, we
were already assuming that every pixel is made of 4 floating point
channels (in this given order: red, then green, then blue, and finally
alpha). It means that even if someone were inputting a grayscale or CMYK
buffer, you would receive perceptual RGBA. Take this as a known fact.
GEGL does all necessary intermediate conversion on your behalf, hence
ensuring correct colors.
Note that in some cases, you may want to have conditional input format depending on real input format. For instance, if the input was CMYK, in some cases, making a round-trip to a RGB space may be counter-productive (though when processing artistic filters, changing models is often necessary).
For the “output” model and space, just advertize what you return and do not do any unecessary conversions yourself (in particular do not try to return to the original format). Indeed you don’t know what is the next node. Maybe there are more filters after you and they may request exactly the format you would output. If you were to convert it, maybe GEGL would convert it back for the next node, so an unnecessary round-trip conversion would have happened.
In other words: don’t try to be clever and let GEGL take care of conversions, unless you really know what you do.
Here is a comparative export of how our point filter Hello World renders
when run on our GIMP 3.0 splash screen (by Sevenix, CC by-sa 4.0) with
exactly same process()
code, except that the left version is processed
on linear space and right version on perceptual space:
A bit on Color Spaces in babl
Regarding color models and spaces, GEGL itself relies on babl, our pixel encoding and color space conversion engine, also born from the GIMP project.
babl
supports many color models (RGB, CMYK, grayscale, indexed images,
CIE Lab, HSV, HSL… and just too many more to cite them all in
this tutorial), with or without alpha channel, with various channel
types (integers of various sizes, floating point in half, single or
double…), using any possible TRC.
I will not dive extremely deep on this topic, but I just wanted to give a bit of insight on some of its syntax, in particular regarding RGB. In the context of babl and GEGL, when you read:
RGB
: this is linear RGB (bypassing any TRC which may be contained in the color profile but still preserving its chromaticities).R~G~B~
: this is perceptual RGB (again bypassing color space, yet preserving chromaticities).R'G'B'
: this is the RGB space following TRC and chromaticities from the space attached to it.
Note that all colors have a space attached to it by definition. Simply when it is not explicitly set, a default is used. For RGB formats in particular, the default is the sRGB space.
For this reason, it is better practice to always define color formats
with babl_format_with_space()
.
If you wish to use the default space, set NULL
as second argument.
This is preferred to using the legacy babl_format()
since you make it explicit that you chose the default space on purpose.
In our above code, what we did is deciding to work on perceptual RGB, in floating point, while still keeping the source image’s space chromaticities.
Of course, this is mostly true for relative color models (such as CMYK, RGB, HSV, HSL…). For absolute color models (where model and space are indissociable), such as CIE L*a*b*, attaching a space won’t actually change the format yet it is an interesting trick because it allows round-trips while preserving space information.
Conclusion
I am not going to add any operation property this time as I assume you got how it worked. But I leave doing this as an exercise.
Of course since we hardcoded the position to decide which pixel to edit, changing the text is not a good property anymore, but you could add some properties to tweak the size, position and ratio of your ‘H’ for instance.
- Back to “How to write a filter” tutorial index
- Previous tutorial: Hello World as a Meta Operation
- Next tutorial: Integrate with GIMP