How to write a GIMP plug-in, part II

Written By Dave Neary.

In the first part, I presented essential elements to build a plug-in interface with The GIMP. Now we will produce a simple but useful algorithm that we could use in our plug-in.

Introduction

The algorithm we are going to implement is a simple blur. It is included in The GIMP as “Filters->Blur->Blur” with default parameters.

That algorithm is very simple. Each pixel in our image is replaced by a mean value of its neighbours. For example, if we look at the simplest case where the neighbourhood is 3x3 (see figure 1), in that case the center value will be replaced with 5, the mean of the 9 numbers in its neighbourhood.

With this method, edge differences are splatted, giving a blurred result. One can choose another radius, using a (2r + 1) x (2r + 1) matrix.

Image structure

In Part I we wrote a run() function that did nothing useful. Let’s look again at run() prototype:

static void run (const gchar     *name,
               gint             nparams,
               const GimpParam *param,
               gint            *nreturn_vals,
               GimpParam      **return_vals);

We saw that for a filter (i.e. a plug-in that modifies the image), the first three input parameters were the run mode, an identifier for the image, and another one for the active drawable (layer or mask).

A GIMP image is a structure that contains, among others, guides, layers, layer masks, and any data associated to the image. The word “drawable” is often used in GIMP internal structures. A “drawable” is an object where you can get, and sometimes modify, raw data. So : layers, layer masks, selections are all “drawables”.

Drawables

Drawables

Accessing the data

To get a GimpDrawable from its identifier, we need the gimp_drawable_get() function:

GimpDrawable *gimp_drawable_get (gint32 drawable_id);

From this structure, one can access drawable data through a GimpPixelRgn structure, and one can check the drawable type (RGB, gray level). The full listing of functions available for a GimpDrawable can be found in the API.

Two very important functions for plug-ins are gimp_drawable_mask_bounds() and gimp_pixel_rgn_init(). The first gives the active selection limits on the drawable, and the second initialises the GimpPixelRgn we will use to access the data.

As soon as we have a well initialised GimpPixelRgn, we can access the image data in several different ways, by pixel, by rectangle, by row or by column. The best method will depend on the algorithm one plans to use. Moreover, The GIMP uses a tile-based architecture, and loading or unloading data is expensive, so we should not use it more than necessary.

Tiles

Tiles

The main functions to get and set image data are:

void gimp_pixel_rgn_get_pixel (GimpPixelRgn *pr,
                             guchar       *buf,
                             gint          x,
                             gint          y);
void gimp_pixel_rgn_get_row   (GimpPixelRgn *pr,
                             guchar       *buf,
                             gint          x,
                             gint          y,
                             gint          width);
void gimp_pixel_rgn_get_col   (GimpPixelRgn *pr,
                             guchar       *buf,
                             gint          x,
                             gint          y,
                             gint          height);
void gimp_pixel_rgn_get_rect  (GimpPixelRgn *pr,
                             guchar       *buf,
                             gint          x,
                             gint          y,
                             gint          width,
                             gint          height);
void gimp_pixel_rgn_set_pixel (GimpPixelRgn *pr,
                             const guchar *buf,
                             gint          x,
                             gint          y);
void gimp_pixel_rgn_set_row   (GimpPixelRgn *pr,
                             const guchar *buf,
                             gint          x,
                             gint          y,
                             gint          width);
void gimp_pixel_rgn_set_col   (GimpPixelRgn *pr,
                             const guchar *buf,
                             gint          x,
                             gint          y,
                             gint          height);
void gimp_pixel_rgn_set_rect  (GimpPixelRgn *pr,
                             const guchar *buf,
                             gint          x,
                             gint          y,
                             gint          width,
                             gint          height);

There is also another way to access image data (it’s even used more often), that allows to manage data at the tile level. We will look at it in detail later.

Updating the image

At last, a plug-in that has modified a drawable data must flush it to send data to the core, and to tell the application that the display must be updated. This is done with the following function:

gimp_displays_flush ();
gimp_drawable_detach (drawable);

Implementing blur()

To be able to try out several different processing methods, we will delegate the job to a blur() function. Our run() is below.

static void
run (const gchar      *name,
   gint              nparams,
   const GimpParam  *param,
   gint             *nreturn_vals,
   GimpParam       **return_vals)
{
	static GimpParam  values[1];
	GimpPDBStatusType status = GIMP_PDB_SUCCESS;
	GimpRunMode       run_mode;
	GimpDrawable     *drawable;

	/* Setting mandatory output values */
	*nreturn_vals = 1;
	*return_vals  = values;

	values[0].type = GIMP_PDB_STATUS;
	values[0].data.d_status = status;

	/* Getting run_mode - we won't display a dialog if 
	 * we are in NONINTERACTIVE mode */
	run_mode = param[0].data.d_int32;

	/*  Get the specified drawable  */
	drawable = gimp_drawable_get (param[2].data.d_drawable);

	gimp_progress_init ("My Blur...");

	/* Let's time blur
	 *
	 *   GTimer timer = g_timer_new time ();
	 */

	blur (drawable);

	/*   g_print ("blur() took %g seconds.\n", g_timer_elapsed (timer));
	 *   g_timer_destroy (timer);
	 */

	gimp_displays_flush ();
	gimp_drawable_detach (drawable);
}

There are a few lines here that need to be explained a bit more. The call to gimp_progress_init() initialises a progress measurement for our plug-in. Later, if we call gimp_progress_update(double percent), the percentage given as an input parameter will be shown graphically. The run_mode tells us whether the plug-in was launched in a way such as we can display a graphical interface or not. Possible values are GIMP_RUN_INTERACTIVE, GIMP_RUN_NONINTERACTIVE or GIMP_RUN_WITH_LAST_VALS, which mean the plug-in was executed from The GIMP, from a script, or from the “Repeat last filter” menu entry.

Regarding the blur algorithm itself, the first version using gimp_pixel_rgn_(get|set)_pixel() is found below. Some functions in it have not been explained yet.

gimp_drawable_mask_bounds() allows calculation of the filter’s effect limits, excluding any region that is not in the active selection. Limiting the processing this way allows an important performance improvement.

gimp_pixel_rgn_init() takes as input parameters the drawable, its limits for the processing, and two booleans that significantly modify the behaviour of the resulting GimpPixelRgn. The first one tells that “set” operations must be done on shadow tiles, in order to leave original data as is until gimp_drawable_merge_shadow() is called, when all modified data will be merged. The second one tells that modified tiles should be tagged “dirty” and sent to the core to be merged. Most of the time, to read data, one uses FALSE and FALSE for these two parameters, and to write data, one uses TRUE and TRUE. Other combinations are possible but seldom used.

static void
blur (GimpDrawable *drawable)
{
  gint         i, j, k, channels;
  gint         x1, y1, x2, y2;
  GimpPixelRgn rgn_in, rgn_out;
  guchar       output[4];

  /* Gets upper left and lower right coordinates,
   * and layers number in the image */
  gimp_drawable_mask_bounds (drawable->drawable_id,
                             &x1, &y1,
                             &x2, &y2);
  channels = gimp_drawable_bpp (drawable->drawable_id);

  /* Initialises two PixelRgns, one to read original data,
   * and the other to write output data. That second one will
   * be merged at the end by the call to
   * gimp_drawable_merge_shadow() */
  gimp_pixel_rgn_init (&rgn_in,
                       drawable,
                       x1, y1,
                       x2 - x1, y2 - y1, 
                       FALSE, FALSE);
  gimp_pixel_rgn_init (&rgn_out,
                       drawable,
                       x1, y1,
                       x2 - x1, y2 - y1, 
                       TRUE, TRUE);

  for (i = x1; i < x2; i++)
    {
      for (j = y1; j < y2; j++)
        {
          guchar pixel[9][4];

          /* Get nine pixels */
          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[0],
                                    MAX (i - 1, x1),
                                    MAX (j - 1, y1));
          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[1],
                                    MAX (i - 1, x1),
                                    j);
          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[2],
                                    MAX (i - 1, x1),
                                    MIN (j + 1, y2 - 1));

          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[3],
                                    i,
                                    MAX (j - 1, y1));
          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[4],
                                    i,
                                    j);
          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[5],
                                    i,
                                    MIN (j + 1, y2 - 1));

          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[6],
                                    MIN (i + 1, x2 - 1),
                                    MAX (j - 1, y1));
          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[7],
                                    MIN (i + 1, x2 - 1),
                                    j);
          gimp_pixel_rgn_get_pixel (&rgn_in,
                                    pixel[8],
                                    MIN (i + 1, x2 - 1),
                                    MIN (j + 1, y2 - 1));

          /* For each layer, compute the average of the
           * nine */
          for (k = 0; k < channels; k++)
            {
              int tmp, sum = 0;
              for (tmp = 0; tmp < 9; tmp++)
                sum += pixel[tmp][k];
              output[k] = sum / 9;
            }

          gimp_pixel_rgn_set_pixel (&rgn_out,
                                    output,
                                    i, j);
        }

      if (i % 10 == 0)
        gimp_progress_update ((gdouble) (i - x1) / (gdouble) (x2 - x1));
    }

  /*  Update the modified region */
  gimp_drawable_flush (drawable);
  gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
  gimp_drawable_update (drawable->drawable_id,
                        x1, y1,
                        x2 - x1, y2 - y1);
}

Row processing

Our function has a bug drawback: performance. On a 300x300 selection, with the timing code uncommented, blur() took 12 minutes on my K6-2 350MHz, well loaded with other stuff. To compare, on the same selection, Gaussian blur took 3 seconds.

If we modify our function to rather use gimp_pixel_rgn_(get|set)_row() the result is far better. We reduce the timing for the 300x300 selection from 760 seconds to 6 seconds. blur() V2 is below:

static void
blur (GimpDrawable *drawable)
{
  gint         i, j, k, channels;
  gint         x1, y1, x2, y2;
  GimpPixelRgn rgn_in, rgn_out;
  guchar      *row1, *row2, *row3;
  guchar      *outrow;

  gimp_drawable_mask_bounds (drawable->drawable_id,
                             &x1, &y1,
                             &x2, &y2);
  channels = gimp_drawable_bpp (drawable->drawable_id);

  gimp_pixel_rgn_init (&rgn_in,
                       drawable,
                       x1, y1,
                       x2 - x1, y2 - y1, 
                       FALSE, FALSE);
  gimp_pixel_rgn_init (&rgn_out,
                       drawable,
                       x1, y1,
                       x2 - x1, y2 - y1,
                       TRUE, TRUE);

  /* Initialise enough memory for row1, row2, row3, outrow */
  row1 = g_new (guchar, channels * (x2 - x1));
  row2 = g_new (guchar, channels * (x2 - x1));
  row3 = g_new (guchar, channels * (x2 - x1));
  outrow = g_new (guchar, channels * (x2 - x1));

  for (i = y1; i < y2; i++)
    {
      /* Get row i-1, i, i+1 */
      gimp_pixel_rgn_get_row (&rgn_in,
                              row1,
                              x1, MAX (y1, i - 1),
                              x2 - x1);
      gimp_pixel_rgn_get_row (&rgn_in,
                              row2,
                              x1, i,
                              x2 - x1);
      gimp_pixel_rgn_get_row (&rgn_in,
                              row3,
                              x1, MIN (y2 - 1, i + 1),
                              x2 - x1);

      for (j = x1; j < x2; j++)
        {
          /* For each layer, compute the average of the nine
           * pixels */
          for (k = 0; k < channels; k++)
            {
              int sum = 0;
              sum = row1[channels * MAX ((j - 1 - x1), 0) + k]           +
                    row1[channels * (j - x1) + k]                        +
                    row1[channels * MIN ((j + 1 - x1), x2 - x1 - 1) + k] +
                    row2[channels * MAX ((j - 1 - x1), 0) + k]           +
                    row2[channels * (j - x1) + k]                        +
                    row2[channels * MIN ((j + 1 - x1), x2 - x1 - 1) + k] +
                    row3[channels * MAX ((j - 1 - x1), 0) + k]           +
                    row3[channels * (j - x1) + k]                        +
                    row3[channels * MIN ((j + 1 - x1), x2 - x1 - 1) + k];
              outrow[channels * (j - x1) + k] = sum / 9;
            }

       }

       gimp_pixel_rgn_set_row (&rgn_out,
                               outrow,
                               x1, i,
                               x2 - x1);

       if (i % 10 == 0)
            gimp_progress_update ((gdouble) (i - y1) / (gdouble) (y2 - y1));
  }

  g_free (row1);
  g_free (row2);
  g_free (row3);
  g_free (outrow);

  gimp_drawable_flush (drawable);
  gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
  gimp_drawable_update (drawable->drawable_id,
                        x1, y1,
                        x2 - x1, y2 - y1);
}

Have a look at the slow or fast blur complete code.

Next part

In next part, we will see how to process the image tile by tile. We will also have a look at preferences, by modifying our algorithm so it can take an input parameter.

Creative Commons
License

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License.