Como escribir un plug-in para gimp (parte III)

Como escribir un plug-in para GIMP, parte III

Escrito por Dave Neary , traducido por AntI para Gimp.es

En la segunda parte, te decía de la manipulación de la imagen por pixel o filas. Esta vez, iré más lejos y procesaré datos con mosaicos, lo que mejorará el funcionamiento de nuestro plug-in. Tambien, actualizaré nuestro algoritmo para tomar un radio más grande y construir un interfaz gráfico para permitir el cambio de ese parámetro.

Introducción

Echamos una mirada a nuestro algoritmo: para cada pixel, genera unos vecinos (2r+1)x(2r+1) y para cada capa, remplaza el valor del pixel de la capa con el valor medio de los vecinos.

Es un poco más complejo que eso, tenemos que tener cuidado con los bordes de la imagen, por ejemplo, pero el algoritmo hace un efecto blur que no tan es malo, en general.

Pero hasta ahora, escribíamos el algoritmo para unos vecinos 3x3. Tiempo para generalizar esta parte e introducir el radio como parámetro.

Primero una palabra sobre mosaicos.

Gerencia del mosaico

Un mosaico es un bloque de datos de la imagen con un tamaño de 64x64. Generalmente, los mosaicos son enviados al plug-in bajo demanda, uno por uno, por memoria compartida. Por supuesto este proceso necesita unos recursos enormes y debe ser evitado.

Normalmente, no se necesita un caché particular, cada mosaico se envia cuando se necesita y se libera cuando se pide otro. Sin embargo podemos decir a nuestro plug-in que guarde un caché del mosaico para evitar este viaje constante, llamando a la función:

    
      gimp_tile_cache_ntiles (gulong ntiles);
         

En la segunda parte del ejemplo, llamabamos gimp_pixel_rgn_get_row() y gimp_pixel_rgn_set_row() pero sin usar ningún caché.

El número de mosaicos en una fila de mosaicos será el ancho de la capa dividido por el ancho del mosaico, más uno. Así, para una capa de 65 de ancho, guardaremos dos mosaicos. Como generalmente procesaremos mosaicos de sombra, podemos doblar el número para computar el tamaño de caché ideal para nuestro plug-in.

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

Con el caché, nuestro lento plug-in se convierte en rápido. Sobre una selección de 300x300, nuestro último blur duraba 3 segundos, pero sobre una selección de 2000x1500 era mucho más lento, 142 segundos.

Añadiendo la linea anterior de código, las cosas van mejor: 11 segundos. Todavia perdemos tiempo de transición en los bordes del mosiaco, podemos bajar a 10 segundos cuando multiplicamos por 4 en lugar de 2 (guardamos en caché dos filas de mosaicos), pero a más mosaicos guardamos en caché, más duro el acceso a disco, lo que reduce el tiempo ganado en un punto.

Generalización del algoritmo

Podemos modificar el algoritmo para tomar un parámetro en: radius. Con un radio de 3, los vecinos de un pixel serán 7x7, en lugar de 3x3 con un radio de 1. Para archivar esto modifico el algoritmo:

  • asignar el espacio para las filas del mosiaco 2r+1

  • inicializar el array de esta fila, teniendo cuidado con los bordes

  • para cada fila de mosaico

    • para cada pixel en la fila de mosaico

      • computar el promedio de los vecinos, teniendo cuidado con los bordes

    • obtener una nueva fila de mosaico y una fila del ciclo

Este algoritmo es más complejo que el pasado, porque computar el promedio será un algoritmo O(r²).

El código modificado para conseguir este comportamiento está abajo. La mayor parte de el trabajo se hace con la función del process_row.

init_mem y shuffle están allí mantener el código de blur limpio y pequeño.

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

Añadiendo un interfaz gráfico y guardando parámetros

Dejar al usuario modificar el radio o dejar a un script darlo, ahora, necesitamos volver a nuestra función run() y colocar algunas cosas.

Primero creamos una estructura que permita guardar y devolver opciones. Generalmente, se da este evento a plug.ins con un solo parámetro.

    
      typedef struct
      {
        gint radius;
      } MyBlurVals;


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

Después modificamos la función run() para que el modo de ejecución se tome en una cuenta. En modo interactivo y repetir el último modo de filtro, intentamos obtener el último valor usado por la función gimp_get_data(), lo que toma un único dato identificador como el primer parámetro de entrada. Generalmente, se usa el nombre del procedimiento.

Finalmente, en modo interactivo, añadimos unas pocas lineas que construirán un interfaz gráfico permitiendo la modificación de opciones.

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

El interfaz gráfico

No detallo la programación GTK+, ya lo hacen muy bien en otros lugares. Nuestro primer intento será muy sencillo. Usaremos la utilidad de GIMP, GimpDialog, para crear una ventana con una cabecera, un control numérico de tipo GtkSpinButton (asociado con GtkAdjustment) y su etiqueta, enmarcada en GtkFrame.

En las siguientes partes, para mostrar lo fácil que se pueden hacer estas cosas, añadiremos un previo en el diálogo para mostrar, en tiempo real, los efectos del parámetro.

Nuestro diálogo se mostrará como este (árbol generado con Glade):

            Glade tree

Arbol de Glade

En el GIMP 2.2, hay un número de widgets que vienen con los parámetros que permiten un comportamiento coherente, consistentes con GNOME Human Interface Guidelines. GimpPreview tambien aparició en 2.2. Hagamos algún primer intento sin el:

          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 ("Modify radius");
        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;
      }
     
    

Añadiendo un GimpPreview

Añadir un GimpPreview es bastante fácil. Primero creamos un GtkWidget con gimp_drawable_preview_new(), entonces le añadimos una señal no válida, que llama a la función blur para actualizar el previo. Tambien, añadimos un segundo parámetro a MyBlurVals para recordar el estado de activación del previo.

Un método para actualizar facilmente el previo es añadir un parámetro previo en la función blur y si el previo no es NULL, toma los límites GimpPreview. Así cuando llamamos a blur desde run(), fijamos el parámetro de previo a NULL.

Para tomar los límites de GimpPreview, usamos gimp_preview_get_position() y gimp_preview_get_size(), así que podemos qenerar lo que se exhibirá.

Para logralo afinaremos algo el código, no necesitamos actualizar la barra de progreso mientras generamos el previo, y deberíamos decirle a GimpPixelRgn init el tiempo que los mosaicos no se deban enviar a la base.

Finalmente, exhibimos el previo actualizado con la función gimp_drawable_preview_draw_region(). Obtenemos una caja de diálogo que muestra en tiempo real los efectos del plug-in. Gracias a la base del GIMP, nuestro plug-in toma las selecciones en una cuenta.



            Blur dialog, improved

Diálogo Blur mejorado

            Blur una selección

Blur una selección

Aquí las dos funciones en las últimas versiones:

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

Echa un vistazo al código completo tiled, UI or preview .

Conclusión

En estos artículos, vimos conceptos básicos de varios aspectos de un plug-in GIMP. Nos enredamos con el tratamiento de datos mediante un sencillo algoritmo y seguimos una ruta que nos mostró como evitar problemas de funcionamiento. Finalmente, generalizamos el algoritmo y le agregamos parámetros, y lo usamos con widgets GIMP para hacer un interfaz de usuario agradable.

Gracias

Gracias a mi esposa Anne y a David Odin por ayudarme mientras escribía este artículo.

            Creative Commons License

Trabajo bajo licencia Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License.