How to write a GIMP plug-in, part III

Written By Dave Neary.

In the second part, I told you about manipulating image data by pixel or row. This time, I will go farther and process data by tile, which will improve our plug-in performance. I will also update our algorithm to take larger radius into account, and build a graphical interface to allow changing that parameter.

Introduction

Let’s have a look at our simple algorithm: for each pixel, generate a (2r+1)x(2r+1) neighbourhood and for each layer, replace the layer’s pixel value with the average value in the neighbourhood.

It’s a bit more complex than that - we have to be careful near image borders for example, but this algorithm makes a blur effect that is not so bad in general.

But until now, we wrote the algorithm for a 3x3 neighbourhood. Time has come to generalise this part and to introduce the radius as a parameter.

First, a word on tiles.

Tile management

A tile is an image data block with a 64x64 size. Usually, tiles are sent to the plug-in on demand one by one, by shared memory. Of course this process needs huge resources and should be avoided.

Usually, one doesn’t need any particular cache, each tile is sent when one needs it and freed when one asks for another one. Nevertheless, we can tell our plug-in to keep a tile cache to avoid this constant round trip, by calling the function:

gimp_tile_cache_ntiles (gulong ntiles);

In the second part example, we called gimp_pixel_rgn_get_row() and gimp_pixel_rgn_set_row() but without using any cache.

The number of tiles in a tile row will be the layer width divided by the tile width, plus one. So, for a layer width of 65, we will cache two tiles. As we usually also process shadow tiles, we can double that number to compute the ideal cache size for our plug-in.

gimp_tile_cache_ntiles (2 * (drawable->width / 
                        gimp_tile_width () + 1));

With the cache, our slow plug-in becomes fast. On a 300x300 selection, our last blur took 3 seconds, but on a 2000x1500 selection it was much slower - 142 seconds.

Adding the above line of code, things are getting better: 11 seconds. We still lose transition time when we reach tile borders, we can go down to 10 seconds when multiplying by 4 instead of 2 (meaning we cache two tiles rows), but the more tiles we cache, the more hard disk access we make, which reduce the time gain at a point.

Algorithm generalisation

We can modify the algorithm to take a parameter into account: radius. With a radius of 3, the neighbourhood of a pixel will be 7x7, instead of 3x3 with a radius of 1. To achieve this I modify the previous algorithm:

  • allocate space for 2r+1 tile rows
  • initialise this rows array, taking care of borders
  • for each tile row
    • for each pixel in the tile row
    • compute the neighbourhood average, taking care of borders
  • get a new tile row and cycle rows

This algorithm is more complex than the last one, because the average computing will be a O(r²) algorithm.

The modified code to get this behaviour is below. Most of the work is done in the process_row function. init_mem and shuffle are there to keep the blur code clean and small.

static void blur        (GimpDrawable *drawable);

static void init_mem    (guchar     ***row,
                         guchar      **outrow,
                         gint          num_bytes);
static void process_row (guchar      **row,
                         guchar       *outrow,
                         gint          x1,
                         gint          y1,
                         gint          width,
                         gint          height,
                         gint          channels,
                         gint          i);
static void shuffle     (GimpPixelRgn *rgn_in,
                         guchar      **row,
                         gint          x1,
                         gint          y1,
                         gint          width,
                         gint          height,
                         gint          ypos);

/* The radius is still a constant, we'll change that when the
 * graphical interface will be built. */
static gint radius = 3;
...

static void
blur (GimpDrawable *drawable)
{
  gint         i, ii, channels;
  gint         x1, y1, x2, y2;
  GimpPixelRgn rgn_in, rgn_out;
  guchar     **row;
  guchar      *outrow;
  gint         width, height;

  gimp_progress_init ("My Blur...");

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

  channels = gimp_drawable_bpp (drawable->drawable_id);

  /* Allocate a big enough tile cache */
  gimp_tile_cache_ntiles (2 * (drawable->width /
                               gimp_tile_width () + 1));

  /* 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,
                       width, height, 
                       FALSE, FALSE);
  gimp_pixel_rgn_init (&rgn_out,
                       drawable,
                       x1, y1,
                       width, height, 
                       TRUE, TRUE);

  /* Allocate memory for input and output tile rows */
  init_mem (&row, &outrow, width * channels);

  for (ii = -radius; ii <= radius; ii++)
    {
      gimp_pixel_rgn_get_row (&rgn_in,
                              row[radius + ii],
                              x1, y1 + CLAMP (ii, 0, height - 1), 
                              width);
    }

  for (i = 0; i < height; i++)
    {
      /* To be done for each tile row */
      process_row (row,
                   outrow,
                   x1, y1,
                   width, height,
                   channels,
                   i);
      gimp_pixel_rgn_set_row (&rgn_out,
                              outrow,
                              x1, i + y1,
                              width);
      /* shift tile rows to insert the new one at the end */
      shuffle (&rgn_in,
               row,
               x1, y1,
               width, height,
               i);
      if (i % 10 == 0)
        gimp_progress_update ((gdouble) i / (gdouble) height);
    }

  /* We could also put that in a separate function but it's
   * rather simple */
  for (ii = 0; ii < 2 * radius + 1; ii++)
    g_free (row[ii]);

  g_free (row);
  g_free (outrow);

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

static void
init_mem (guchar ***row,
          guchar  **outrow,
          gint      num_bytes)
{
  gint i;

  /* Allocate enough memory for row and outrow */
  *row = g_new (char *, (2 * radius + 1));

  for (i = -radius; i <= radius; i++)
    (*row)[i + radius] = g_new (guchar, num_bytes);

  *outrow = g_new (guchar, num_bytes);
}

static void
process_row (guchar **row,
             guchar  *outrow,
             gint     x1,
             gint     y1,
             gint     width,
             gint     height,
             gint     channels,
             gint     i)
{
  gint j;

  for (j = 0; j < width; j++)
    {
      gint k, ii, jj;
      gint left = (j - radius),
           right = (j + radius);

      /* For each layer, compute the average of the
       * (2r+1)x(2r+1) pixels */
      for (k = 0; k < channels; k++)
        {
          gint sum = 0;

          for (ii = 0; ii < 2 * radius + 1; ii++)
            for (jj = left; jj <= right; jj++)
              sum += row[ii][channels * CLAMP (jj, 0, width - 1) + k];

          outrow[channels * j + k] =
            sum / (4 * radius * radius + 4 * radius + 1);
        }
    }
}

static void
shuffle (GimpPixelRgn *rgn_in,
         guchar      **row,
         gint          x1,
         gint          y1,
         gint          width,
         gint          height,
         gint          ypos)
{
  gint    i;
  guchar *tmp_row;

  /* Get tile row (i + radius + 1) into row[0] */
  gimp_pixel_rgn_get_row (rgn_in,
                          row[0],
                          x1, MIN (ypos + radius + y1, y1 + height - 1),
                          width);

  /* Permute row[i] with row[i-1] and row[0] with row[2r] */
  tmp_row = row[0];
  for (i = 1; i < 2 * radius + 1; i++)
    row[i - 1] = row[i];
  row[2 * radius] = tmp_row;
}

Adding a graphical interface and saving parameters

To let the user modify the radius, or let a non-interactive script give it as a parameter, we now need to get back to our run() function and settle some simple things.

First we create a structure to allow saving and returning options. Usually one does this even for plug-ins with only one parameter.

typedef struct
{
  gint radius;
} MyBlurVals;


/* Set up default values for options */
static MyBlurVals bvals =
{
  3  /* radius */
};

Next, we modify the run() function so that execution modes are taken into account. In interactive mode and repeat last filter mode, we try to get the last values used by the gimp_get_data() function, which takes a unique data identifier as its first input parameter. Usually, one uses the procedure’s name.

Finally, in interactive mode, we add a few lines that will build the graphical interface allowing options modification.

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);

  switch (run_mode)
    {
    case GIMP_RUN_INTERACTIVE:
      /* Get options last values if needed */
      gimp_get_data ("plug-in-myblur", &bvals);

      /* Display the dialog */
      if (! blur_dialog (drawable))
        return;
      break;

    case GIMP_RUN_NONINTERACTIVE:
      if (nparams != 4)
        status = GIMP_PDB_CALLING_ERROR;
      if (status == GIMP_PDB_SUCCESS)
        bvals.radius = param[3].data.d_int32;
      break;

    case GIMP_RUN_WITH_LAST_VALS:
      /*  Get options last values if needed  */
      gimp_get_data ("plug-in-myblur", &bvals);
      break;

    default:
      break;
    }

  blur (drawable);

  gimp_displays_flush ();
  gimp_drawable_detach (drawable);

  /*  Finally, set options in the core  */
  if (run_mode == GIMP_RUN_INTERACTIVE)
    gimp_set_data ("plug-in-myblur", &bvals, sizeof (MyBlurVals));

  return;
}

The graphical interface

I won’t detail GTK+ programming as this is done very well in other places. Our first try will be very simple. We will use the utility widget of GIMP, the GimpDialog, to create a window with a header, a numeric control of type GtkSpinButton (associated with a GtkAdjustment) and its label, nicely framed in a GtkFrame.

In the following parts, in order to show how easy one can do such things, I will add a preview in the dialog to show real time effects of the parameters.

Our final dialog will look like this (tree generated with Glade):

Glade tree

Glade tree

In The GIMP 2.2, there is a number of widgets that come bundled with parameters that allow a coherent behaviour, consistent with GNOME Human Interface Guidelines. GimpPreview also appeared in 2.2. Let’s make a first try without it:

Blur dialog

Blur dialog

static gboolean
blur_dialog (GimpDrawable *drawable)
{
	GtkWidget *dialog;
	GtkWidget *main_vbox;
	GtkWidget *main_hbox;
	GtkWidget *frame;
	GtkWidget *radius_label;
	GtkWidget *alignment;
	GtkWidget *spinbutton;
	GtkObject *spinbutton_adj;
	GtkWidget *frame_label;
	gboolean   run;

	gimp_ui_init ("myblur", FALSE);

	dialog = gimp_dialog_new ("My blur", "myblur",
		                      NULL, 0,
		                      gimp_standard_help_func, "plug-in-myblur",

		                      GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
		                      GTK_STOCK_OK,     GTK_RESPONSE_OK,

		                      NULL);

	main_vbox = gtk_vbox_new (FALSE, 6);
	gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), main_vbox);
	gtk_widget_show (main_vbox);

	frame = gtk_frame_new (NULL);
	gtk_widget_show (frame);
	gtk_box_pack_start (GTK_BOX (main_vbox), frame, TRUE, TRUE, 0);
	gtk_container_set_border_width (GTK_CONTAINER (frame), 6);

	alignment = gtk_alignment_new (0.5, 0.5, 1, 1);
	gtk_widget_show (alignment);
	gtk_container_add (GTK_CONTAINER (frame), alignment);
	gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 6, 6, 6);

	main_hbox = gtk_hbox_new (FALSE, 0);
	gtk_widget_show (main_hbox);
	gtk_container_add (GTK_CONTAINER (alignment), main_hbox);

	radius_label = gtk_label_new_with_mnemonic ("_Radius:");
	gtk_widget_show (radius_label);
	gtk_box_pack_start (GTK_BOX (main_hbox), radius_label, FALSE, FALSE, 6);
	gtk_label_set_justify (GTK_LABEL (radius_label), GTK_JUSTIFY_RIGHT);

	spinbutton_adj = gtk_adjustment_new (3, 1, 16, 1, 5, 5);
	spinbutton = gtk_spin_button_new (GTK_ADJUSTMENT (spinbutton_adj), 1, 0);
	gtk_widget_show (spinbutton);
	gtk_box_pack_start (GTK_BOX (main_hbox), spinbutton, FALSE, FALSE, 6);
	gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (spinbutton), TRUE);

	frame_label = gtk_label_new ("<b>Modify radius</b>");
	gtk_widget_show (frame_label);
	gtk_frame_set_label_widget (GTK_FRAME (frame), frame_label);
	gtk_label_set_use_markup (GTK_LABEL (frame_label), TRUE);

	g_signal_connect (spinbutton_adj, "value_changed",
		              G_CALLBACK (gimp_int_adjustment_update),
		              &bvals.radius);
	gtk_widget_show (dialog);

	run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);

	gtk_widget_destroy (dialog);

	return run;
}

Adding a GimpPreview

Adding a GimpPreview is quite easy. First we create a GtkWidget with gimp_drawable_preview_new(), then we attach an invalidated signal to it, which will call the blur function to update the preview. We also add a second parameter to MyBlurVals to remember the activation state of the preview.

A method to update easily the preview is to add a preview parameter in the blur function, and if preview is not NULL, to take GimpPreview limits. So when we call blur from run(), we set the preview parameter to NULL.

To take GimpPreview limits, we use gimp_preview_get_position() and gimp_preview_get_size(), so we can generate only what will be displayed.

To achieve this the right way we’ll tune some of the code - we don’t need to update the progress bar while generating the preview, and we should tell at GimpPixelRgn init time that the tiles should not be sent back to the core.

Finally, we display the updated preview with the gimp_drawable_preview_draw_region() function. We get a dialog box that shows us in real time the plug-in effects. Moreover, thanks to the GIMP core, our plug-in already takes selections into account.

Blur dialog

Blur dialog, improved

Blur a selection

Blur a selection

Here are the two functions in their last version:

static void
blur (GimpDrawable *drawable,
      GimpPreview  *preview)
{
  gint         i, ii, channels;
  gint         x1, y1, x2, y2;
  GimpPixelRgn rgn_in, rgn_out;
  guchar     **row;
  guchar      *outrow;
  gint         width, height;

  if (!preview)
    gimp_progress_init ("My Blur...");

  /* Gets upper left and lower right coordinates,
   * and layers number in the image */
  if (preview)
  {
    gimp_preview_get_position (preview, &x1, &y1);
    gimp_preview_get_size (preview, &width, &height);
    x2 = x1 + width;
    y2 = y1 + height;
  }
  else
  {
    gimp_drawable_mask_bounds (drawable->drawable_id,
                               &x1, &y1,
                               &x2, &y2);
    width = x2 - x1;
    height = y2 - y1;
  }

  channels = gimp_drawable_bpp (drawable->drawable_id);

  /* Allocate a big enough tile cache */
  gimp_tile_cache_ntiles (2 * (drawable->width / 
                               gimp_tile_width () + 1));

  /* 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,
                       width, height, 
                       FALSE, FALSE);
  gimp_pixel_rgn_init (&rgn_out,
                       drawable,
                       x1, y1,
                       width, height, 
                       preview == NULL, TRUE);

  /* Allocate memory for input and output tile rows */
  init_mem (&row, &outrow, width * channels);
  
  for (ii = -bvals.radius; ii <= bvals.radius; ii++)
    {
      gimp_pixel_rgn_get_row (&rgn_in,
                              row[bvals.radius + ii],
                              x1, y1 + CLAMP (ii, 0, height - 1), 
                              width);
    }

  for (i = 0; i < height; i++)
    {
      /* To be done for each tile row */
      process_row (row,
                   outrow,
                   x1, y1,
                   width, height,
                   channels,
                   i);
      gimp_pixel_rgn_set_row (&rgn_out,
                              outrow,
                              x1, i + y1,
                              width);
      /* shift tile rows to insert the new one at the end */
      shuffle (&rgn_in,
               row,
               x1, y1,
               width, height,
               i);
      if (i % 10 == 0 && !preview)
        gimp_progress_update ((gdouble) i / (gdouble) height);
    }

  for (ii = 0; ii < 2 * bvals.radius + 1; ii++)
    g_free (row[ii]);

  g_free (row);
  g_free (outrow);

  /*  Update the modified region  */
  if (preview)
    {
      gimp_drawable_preview_draw_region (GIMP_DRAWABLE_PREVIEW (preview),
                                         &rgn_out);
    }
  else
    {
      gimp_drawable_flush (drawable);
      gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
      gimp_drawable_update (drawable->drawable_id,
                            x1, y1,
                            width, height);
    }
}

static gboolean
blur_dialog (GimpDrawable *drawable)
{
  GtkWidget *dialog;
  GtkWidget *main_vbox;
  GtkWidget *main_hbox;
  GtkWidget *preview;
  GtkWidget *frame;
  GtkWidget *radius_label;
  GtkWidget *alignment;
  GtkWidget *spinbutton;
  GtkObject *spinbutton_adj;
  GtkWidget *frame_label;
  gboolean   run;

  gimp_ui_init ("myblur", FALSE);

  dialog = gimp_dialog_new ("My blur", "myblur",
                            NULL, 0,
                            gimp_standard_help_func, "plug-in-myblur",

                            GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                            GTK_STOCK_OK,     GTK_RESPONSE_OK,

                            NULL);

  main_vbox = gtk_vbox_new (FALSE, 6);
  gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), main_vbox);
  gtk_widget_show (main_vbox);

  preview = gimp_drawable_preview_new (drawable, &bvals.preview);
  gtk_box_pack_start (GTK_BOX (main_vbox), preview, TRUE, TRUE, 0);
  gtk_widget_show (preview);

  frame = gimp_frame_new ("Blur radius");
  gtk_box_pack_start (GTK_BOX (main_vbox), frame, FALSE, FALSE, 0);
  gtk_widget_show (frame);

  alignment = gtk_alignment_new (0.5, 0.5, 1, 1);
  gtk_widget_show (alignment);
  gtk_container_add (GTK_CONTAINER (frame), alignment);
  gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 6, 6, 6);

  main_hbox = gtk_hbox_new (FALSE, 12);
  gtk_container_set_border_width (GTK_CONTAINER (main_hbox), 12);
  gtk_widget_show (main_hbox);
  gtk_container_add (GTK_CONTAINER (alignment), main_hbox);

  radius_label = gtk_label_new_with_mnemonic ("_Radius:");
  gtk_widget_show (radius_label);
  gtk_box_pack_start (GTK_BOX (main_hbox), radius_label, FALSE, FALSE, 6);
  gtk_label_set_justify (GTK_LABEL (radius_label), GTK_JUSTIFY_RIGHT);

  spinbutton = gimp_spin_button_new (&spinbutton_adj, bvals.radius, 
                                     1, 32, 1, 1, 1, 5, 0);
  gtk_box_pack_start (GTK_BOX (main_hbox), spinbutton, FALSE, FALSE, 0);
  gtk_widget_show (spinbutton);

  g_signal_connect_swapped (preview, "invalidated",
                            G_CALLBACK (blur),
                            drawable);
  g_signal_connect_swapped (spinbutton_adj, "value_changed",
                            G_CALLBACK (gimp_preview_invalidate),
                            preview);

  blur (drawable, GIMP_PREVIEW (preview));

  g_signal_connect (spinbutton_adj, "value_changed",
                    G_CALLBACK (gimp_int_adjustment_update),
                    &bvals.radius);
  gtk_widget_show (dialog);

  run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);

  gtk_widget_destroy (dialog);

  return run;
}

Have a look at the tiled, UI or preview blur complete code.

Conclusion

In these articles, we saw basic concepts for several aspects of a GIMP plug-in. We messed with image data treatment through a simple algorithm, and followed a path that showed us how to avoid performance problems. Finally, we generalised the algorithm and added parameters to it, and we used some GIMP widgets to make a nice user interface.

Thanks

Thanks to my wife Anne and to David Odin (preview master) for helping me while I was writing this article.

Creative Commons
License

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