BlobStats: blobstats-1.0.c

File blobstats-1.0.c, 25.7 kB (added by powell, 3 years ago)

BlobStats 1.0 release C source file

Line 
1/* BlobStats -- GIMP plug-in to display and report image blob statistics
2 * Version 1.0 Released June 11, 2009
3 *
4 * By Adam C. Powell, IV of Opennovation
5 * Copyright (C) 2008-2009 ZPower, Inc.
6 * All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 *    notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 *    notice, this list of conditions and the following disclaimer in the
15 *    documentation and/or other materials provided with the distribution.
16 * 3. Neither the name of the ZPower nor the names of its contributors
17 *    may be used to endorse or promote products derived from this software
18 *    without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY ZPOWER AND CONTRIBUTORS ``AS IS'' AND
21 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 * ARE DISCLAIMED.  IN NO EVENT SHALL ZPOWER OR CONTRIBUTORS BE LIABLE
24 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 * SUCH DAMAGE.
31 *
32 * The GIMP -- an image manipulation program
33 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
34 *
35 * BlobStats is a GIMP plug-in for calculating and reporting statistics on
36 * "blobs" in images.  Roughly speaking, it does the following:
37 *
38 *  - Find each individual "blob" with intensity above a given threshold.
39 *  - Calculate the zeroth (area), first (centroid) and second (width) moments
40 *    of each blob.
41 *  - Create different colored shapes for each blob against a transparent
42 *    background (if drawable is RGB not grayscale).
43 *  - Draw lines using the current brush, foreground color and opacity
44 *    representing the major and minor axes of the ellipsoid fitting each blob
45 *    based on its second moments, and centered at its centroid.
46 *  - Optionally save each blob's moment statistics, along with derived
47 *    properties such as aspect ratio and orientation angle, in a CSV (comma-
48 *    separated values) file for later processing using spreadsheet software.
49 *
50 * This has many potential uses in materials science, for example:
51 *
52 *  - Calculate the density and spatial distribution of precipitates or grains
53 *    (if grain boundaries are sufficiently clear).
54 *  - Calculate particle size mean and standand deviation, or plot a histogram.
55 *  - Statistics on 2-D aspect ratios correlate with those of 3-D particles
56 *    etc.
57 *
58 * Embedding this within GIMP brings along all of its tools, for example:
59 *
60 *  - One can set the foreground color, brush and opacity to any values, these
61 *    are used to draw the ellipsoid axes.
62 *  - It can be helpful to duplicate a layer before using BlobStats; the
63 *    resulting blobs and axes look compelling against the original background.
64 *  - Grainy images can probably go through a blurring process before using
65 *    BlobStats (see Filters -> Blur for some examples).
66 *  - To find dark blobs in a bright background, use Colors -> Invert before
67 *    using BlobStats.
68 *  - Use other Edge-Detect filters to convert the detected blobs to outlines.
69 *
70 * COMPILATION INSTRUCTIONS:
71 *
72 * On Debian, Ubuntu and other Debian-based systems, install the packages
73 * libgimp2.0-dev, libc6-dev and gcc.  Then type:
74 *
75 * gimptool-2.0 --install blobstats.c
76 *
77 * (Ignore the "no previous prototype" warnings, this single-file plugin
78 * doesn't need prototypes.)
79 * When GIMP next starts, the Filters -> Edge-Detect menu should now have an
80 * entry for BlobStats.  You can re-compile and re-install it on the fly
81 * without quitting GIMP.
82 *
83 * BUGS:
84 *
85 *  - I can't for the life of me figure out how to draw the axes on the preview
86 *    window.
87 *
88 * FUTURE POSSIBILITIES:
89 *
90 *  - Select the channel for finding blobs and calculating moments from
91 *    intensity, alpha, red, green, blue.
92 *  - Have blob parameters, such as orientation, aspect ratio and size,
93 *    determine the blob color.
94 *  - Replace string entry with a file dialog to select the CSV file name.
95 */
96
97#include <libgimp/gimp.h>
98#include <libgimp/gimpui.h>
99#include <libgimp/gimptypes.h>
100#include <gtk/gtkstyle.h>
101#include <glib/gmacros.h>
102#include <glib/gtypes.h>
103#include <string.h>
104#include <stdio.h>
105#include <stdlib.h>
106#include <math.h>
107
108
109G_BEGIN_DECLS
110
111#define PLUG_IN_BINARY  "blobstats"
112#define PLUG_IN_PROC    "plug-in-blobstats"
113
114#define TILE_WIDTH  gimp_tile_width()
115#define TILE_HEIGHT gimp_tile_height()
116
117G_END_DECLS
118
119struct bs_savable {
120  gfloat                threshold;
121  gfloat                color_opacity;
122  gboolean              save_csv;
123  gchar                 filename [256];
124};
125
126typedef struct {
127  gint32                drawable_ID;
128  struct bs_savable     p;
129} BlobStatsParam;
130
131BlobStatsParam *blobstats_param_new (gint32 drawable_ID)
132{
133  BlobStatsParam *param;
134
135  if (!(param = g_new0 (BlobStatsParam, 1)))
136    return NULL;
137
138  param->drawable_ID      = drawable_ID;
139  param->p.threshold      = 0.5;
140  param->p.color_opacity  = 0.4;
141  param->p.save_csv       = FALSE;
142  strcpy (param->p.filename, "output.csv");
143
144  return param;
145}
146
147gboolean blobstats_param_set (BlobStatsParam *param, gint gimp_nparams,
148                              const GimpParam *gimp_param)
149{
150  g_assert (param);
151  g_assert (gimp_param);
152
153  if (gimp_nparams != 9)
154    return FALSE;
155
156  param->p.threshold     = CLAMP (gimp_param[3].data.d_float, 0., 1.);
157  param->p.color_opacity = CLAMP (gimp_param[6].data.d_float, 0., 1.);
158  param->p.save_csv      = (gimp_param[7].data.d_int32 != -1) ? TRUE : FALSE;
159  return TRUE;
160}
161
162gboolean blobstats_param_restore (BlobStatsParam *param)
163{
164  const gsize size = sizeof (struct bs_savable);
165
166  g_assert (param);
167
168  if (gimp_procedural_db_get_data_size (PLUG_IN_PROC) != size)
169    return FALSE;
170
171  return gimp_procedural_db_get_data (PLUG_IN_PROC, &(param->p));
172}
173
174gboolean blobstats_param_store (BlobStatsParam *param)
175{
176  g_assert (param);
177
178  return gimp_procedural_db_set_data (PLUG_IN_PROC, &(param->p),
179                                      sizeof (struct bs_savable));
180}
181
182typedef struct {
183  gdouble area;
184  gdouble xavg, yavg;
185  gdouble xxwid, yywid, xywid;
186  gdouble majaxis, minaxis;
187  gdouble aspect_ratio;
188  gdouble angle;
189} BlobMoments;
190
191// Calculate the moments of a guint8 field
192BlobMoments blobstats_moments (guint8 *field, int pixskip, int rowskip,
193                               int width, int height)
194{
195  int x, y;
196  BlobMoments moments = { 0.,0.,0.,0.,0.,0.,0.,0.,0.,0. };
197
198  // Calculate zero and first moments
199  for (y=0; y<height; y++)
200    for (x=0; x<width; x++)
201      {
202        double pixel = (double)(field [y*rowskip + x*pixskip])/255.;
203        moments.area += pixel;
204        moments.xavg += pixel * x;
205        moments.yavg += pixel * y;
206      }
207  moments.xavg /= moments.area;
208  moments.yavg /= moments.area;
209
210  // Calculate second moments
211  for (y=0; y<height; y++)
212    for (x=0; x<width; x++)
213      {
214        double pixel = (double)(field [y*rowskip + x*pixskip])/255.;
215        double xoff = pixel * (moments.xavg - x);
216        double yoff = pixel * (moments.yavg - y);
217        moments.xxwid += xoff * xoff;
218        moments.yywid += yoff * yoff;
219        moments.xywid += xoff * yoff;
220      }
221  moments.xxwid = sqrt (moments.xxwid/moments.area);
222  moments.yywid = sqrt (moments.yywid/moments.area);
223  // This looks funny, but it preserves the sign of xywid
224  moments.xywid /= sqrt (fabs(moments.xywid*moments.area));
225
226  return moments;
227}
228
229// This calculates the moments of all pixels equal to "value"
230BlobMoments blobstats_moments_value (guint8 *field, guint8 value, int pixskip,
231                                     int rowskip, int width, int height)
232{
233  int x, y;
234  BlobMoments moments = { 0.,0.,0.,0.,0.,0.,0.,0.,0.,0. };
235
236  // Calculate zero and first moments
237  for (y=0; y<height; y++)
238    for (x=0; x<width; x++)
239      if (field [y*rowskip + x*pixskip] == value)
240        {
241          moments.area += 1.;
242          moments.xavg += x;
243          moments.yavg += y;
244        }
245  moments.xavg /= moments.area;
246  moments.yavg /= moments.area;
247
248  // Calculate second moments
249  for (y=0; y<height; y++)
250    for (x=0; x<width; x++)
251      if (field [y*rowskip + x*pixskip] == value)
252        {
253          double xoff = (moments.xavg - x);
254          double yoff = (moments.yavg - y);
255          moments.xxwid += xoff * xoff;
256          moments.yywid += yoff * yoff;
257          moments.xywid += xoff * yoff;
258      }
259
260  // Calculate axes, ratio, and angle
261  moments.majaxis = (moments.xxwid+moments.yywid) +
262    sqrt ((moments.xxwid-moments.yywid) * (moments.xxwid-moments.yywid) +
263          4. * moments.xywid * moments.xywid);
264  moments.minaxis = (moments.xxwid+moments.yywid) -
265    sqrt ((moments.xxwid-moments.yywid) * (moments.xxwid-moments.yywid) +
266          4. * moments.xywid * moments.xywid);
267  moments.angle = 0.5 * atan2 (2.*moments.xywid, moments.xxwid-moments.yywid);
268
269  // Now normalize them all
270  moments.xxwid = sqrt (moments.xxwid/moments.area);
271  moments.yywid = sqrt (moments.yywid/moments.area);
272  // This looks funny, but it preserves the sign of xywid
273  if (moments.xywid != 0.)
274    moments.xywid /= sqrt (fabs(moments.xywid*moments.area));
275
276  moments.majaxis = sqrt (2.*moments.majaxis/moments.area);
277  moments.minaxis = sqrt (2.*moments.minaxis/moments.area);
278  moments.aspect_ratio = moments.majaxis / moments.minaxis;
279
280  return moments; 
281}
282
283// Pixel values in the "meaning field"
284#define EMPTY   0
285#define FILLED  1
286#define NEWBLOB 2
287#define COLOR0  16 // red
288#define COLOR1  17 // green
289#define COLOR2  18 // blue
290#define COLOR3  19 // yellow
291#define COLOR4  20 // cyan
292#define COLOR5  21 // magenta
293#define COLOR6  22 // pink
294#define COLOR7  23 // lightgreen
295#define COLOR8  24 // lightblue
296#define COLOR9  25 // orange
297
298#define COLORLIST                                                       \
299  { 1.,0.,0.,   /* red */                                               \
300      0.,1.,0., /* green */                                             \
301      0.,0.,1., /* blue */                                              \
302      1.,1.,0., /* yellow */                                            \
303      1.,0.,1., /* magenta */                                           \
304      0.,1.,1., /* cyan */                                              \
305      1.,.5,.5, /* pink */                                              \
306      .5,1.,.5, /* lightgreen */                                        \
307      .5,.5,1., /* lightblue */                                         \
308      1.,.5,0.  /* orange */ }
309
310#define COLORNAMES { "Red", "Green", "Blue", "Yellow", "Magenta", \
311      "Cyan", "Pink", "Light Green", "Light Blue", "Orange" }
312
313#define FIELD(x,y) (field [(y)*rowskip + (x)*pixskip])
314
315// Recursive function which fills a region of value "value" with "new_value"
316//  starting at a point
317void blobline (guint8 *field, guint8 value, int new_value, int pixskip,
318               int rowskip, int width, int height, int xstart, int y)
319{
320  int x1;
321
322  if (FIELD (xstart,y) != value)
323    {
324      printf ("Blobline algorithm error\n");
325      return;
326    }
327
328  // Fill in this contiguous blob line with new_value
329  while (xstart>0 && FIELD (xstart-1,y) == value)
330    xstart--;
331  for (x1=xstart; x1<width && FIELD (x1,y) == value; x1++)
332    FIELD (x1,y) = new_value;
333
334  // Check previous and next lines for value, blobline them if it's there
335  for (x1=xstart; x1<width && FIELD (x1,y) == new_value; x1++)
336    {
337      if (y>0)
338        if (FIELD (x1,y-1) == value)
339          blobline (field, value, new_value, pixskip, rowskip, width, height,
340                    x1, y-1);
341
342      if (y<height-1)
343        if (FIELD (x1,y+1) == value)
344          blobline (field, value, new_value, pixskip, rowskip, width, height,
345                    x1, y+1);
346    }
347}
348
349// This locates a contiguous region of value "value", and replaces it with
350// "new_value"; returns TRUE if it found a blob, FALSE otherwise.
351gboolean find_blob (guint8 *field, guint8 value, int new_value,
352                    int pixskip, int rowskip, int width, int height)
353{
354  int x=width, y;
355
356  // Look for value and return FALSE if not found
357  for (y=0; y<height && x == width; y++)
358    for (x=0; x<width && FIELD (x,y) != value; x++)
359      ;
360  if (--y == height-1 && x == width)
361    return FALSE;
362
363  // Fill in the blob using the recursive blobline function
364  blobline (field, value, new_value, pixskip, rowskip, width, height, x, y);
365
366  return TRUE;
367}
368
369gboolean blobstats_analyze (GimpPreview *preview, BlobStatsParam *bs_param)
370{
371  int blobnum=0, win_width, win_height, x,y, x1,x2,y1,y2, pixskip,rowskip, i,
372    num_moms=10;
373  guint8 *source_data, color=COLOR0;
374  gboolean has_selection;
375  FILE *csv_file = NULL;
376  GimpDrawable *drawable;
377  GimpPixelRgn source_region, output_region;
378  BlobMoments *mom;
379
380  if (!(mom = malloc (num_moms * sizeof (BlobMoments))))
381    return FALSE;
382
383  if (!(drawable = (GimpDrawable *) gimp_drawable_get (bs_param->drawable_ID)))
384    return FALSE;
385  has_selection = gimp_drawable_mask_bounds (bs_param->drawable_ID,
386                                             &x1, &y1, &x2, &y2);
387  pixskip = drawable->bpp;
388  rowskip = (x2-x1) * pixskip;
389
390  // Get source region and data
391  if (!(source_data = g_new (guchar, pixskip * (x2-x1) * (y2-y1))))
392    return FALSE;
393  gimp_pixel_rgn_init (&source_region, drawable, x1, y1, x2-x1, y2-y1, FALSE,
394                       FALSE);
395  gimp_pixel_rgn_get_rect (&source_region, source_data, x1, y1, x2-x1, y2-y1);
396
397  if (preview)
398    {
399      int px1, py1, px2, py2;
400
401      gimp_preview_get_position (GIMP_PREVIEW (preview), &px1, &py1);
402      gimp_preview_get_size (GIMP_PREVIEW (preview), &win_width, &win_height);
403      px2 = px1 + win_width;
404      py2 = py1 + win_height;
405
406      if (px2 <= x1 || px1 >= x2 || py2 <= y1 || py1 >= y2)
407        return TRUE;
408
409      x1 = MAX (x1, px1);
410      x2 = MIN (x2, px2);
411      y1 = MAX (y1, py1);
412      y2 = MIN (y2, py2);
413    }
414
415  win_width = x2-x1;
416  win_height = y2-y1;
417
418  if (!preview && bs_param->p.save_csv)
419    {
420      if (!(csv_file = fopen (bs_param->p.filename, "w")))
421        printf ("Error: cannot open CSV file %s\n", bs_param->p.filename);
422      else
423        {
424          fprintf (csv_file, "BlobStats GIMP plug-in output\n\n");
425          fprintf (csv_file, "Blob #,Color,Area,Center X,Center Y,"
426                   "Width XX,Width YY,Width XY,Maj Axis,Min Axis,"
427                   "Asp Ratio,Orientation\n");
428        }
429    }
430
431  // Identify the blobs
432  for (y=0; y<win_height; y++)
433    {
434    for (x=0; x<win_width; x++)
435      {
436        guint8 source_intensity =
437          (source_data [(y1+y)*rowskip + (x1+x)*pixskip] +
438           source_data [(y1+y)*rowskip + (x1+x)*pixskip + 1] +
439           source_data [(y1+y)*rowskip + (x1+x)*pixskip + 2]) /3;
440
441        source_data [(y1+y)*rowskip + (x1+x)*pixskip] =
442          (source_intensity > bs_param->p.threshold * 255) ? FILLED : EMPTY;
443        /*if (csv_file)
444          fprintf (csv_file, "%c%c%c%c ",
445                   'a'+source_data [(y1+y)*rowskip + (x1+x)*pixskip]/10,
446                   'a'+source_data [(y1+y)*rowskip + (x1+x)*pixskip + 1]/10,
447                   'a'+source_data [(y1+y)*rowskip + (x1+x)*pixskip + 2]/10,
448                   'a'+source_intensity/10);*/
449      }
450    //if (csv_file)
451    //  fprintf (csv_file, "\n");
452    }
453
454  // Mark contiguous blobs and save their data
455  while (find_blob (source_data + y1*rowskip + x1*pixskip, FILLED, NEWBLOB,
456                    pixskip, rowskip, win_width, win_height))
457    {
458      char *color_names [10] = COLORNAMES;
459
460      if (blobnum >= num_moms)
461        if (!(mom = realloc (mom, (num_moms *= 2) * sizeof (BlobMoments))))
462          return FALSE;
463
464      mom [blobnum] = blobstats_moments_value
465        (source_data + y1*rowskip + x1*pixskip, NEWBLOB, pixskip, rowskip,
466         win_width, win_height);
467
468      for (y=0; y<win_height; y++)
469        for (x=0; x<win_width; x++)
470          if (source_data [(y1+y)*rowskip + (x1+x)*pixskip] == NEWBLOB)
471            source_data [(y1+y)*rowskip + (x1+x)*pixskip] = color;
472
473      // Ignore single-point blobs (many of which are often off-window)
474      if (mom[blobnum].area > 1)
475        {
476          if (csv_file)
477            fprintf (csv_file, "%d,%s,%g,%g,%g,%g,%g,%g,%g,%g,%g,%g\n",
478                     blobnum, color_names [color-COLOR0], mom[blobnum].area,
479                     mom[blobnum].xavg, mom[blobnum].yavg,
480                     mom[blobnum].xxwid, mom[blobnum].yywid, mom[blobnum].xywid,
481                     mom[blobnum].majaxis, mom[blobnum].minaxis,
482                     mom[blobnum].aspect_ratio,
483                     gimp_rad_to_deg(mom[blobnum].angle));
484
485          blobnum++;
486          if (++color > COLOR9)
487            color = COLOR0;
488        }
489    }
490
491  if (csv_file)
492    fclose (csv_file);
493
494  // Put blob colors in source data
495  for (y=0; y<source_region.h; y++)
496    for (x=0; x<source_region.w; x++)
497      {
498        guint8 pixel = source_data [(y1+y)*rowskip + (x1+x)*pixskip];
499        if (pixel >= COLOR0 && pixel <= COLOR9)
500          {
501            float colorlist [30] = COLORLIST;
502
503            source_data [(y1+y)*rowskip + (x1+x)*pixskip] =
504              255 * colorlist [(pixel - COLOR0) *3];
505            source_data [(y1+y)*rowskip + (x1+x)*pixskip + 1] =
506              255 * colorlist [(pixel - COLOR0) *3 +1];
507            source_data [(y1+y)*rowskip + (x1+x)*pixskip + 2] =
508              255 * colorlist [(pixel - COLOR0) *3 +2];
509            if (pixskip == 4)
510              source_data [(y1+y)*rowskip + (x1+x)*pixskip + 3] =
511                255 * bs_param->p.color_opacity;
512          }
513        else
514          {
515            source_data [(y1+y)*rowskip + (x1+x)*pixskip] =
516              source_data [(y1+y)*rowskip + (x1+x)*pixskip + 1] =
517              source_data [(y1+y)*rowskip + (x1+x)*pixskip + 2] = 0;
518            if (pixskip == 4)
519              source_data [(y1+y)*rowskip + (x1+x)*pixskip + 3] = 0;
520          }
521      }
522
523  // Create and draw to output region
524  gimp_pixel_rgn_init (&output_region, drawable, x1, y1, win_width, win_height,
525                       (preview == NULL), TRUE);
526
527  gimp_pixel_rgn_set_rect
528    (&output_region, source_data + y1*rowskip, 0, y1,
529     rowskip/pixskip, win_height);
530
531  // Draw region into preview, or update
532  if (preview)
533    gimp_drawable_preview_draw_region (GIMP_DRAWABLE_PREVIEW (preview),
534                                       &output_region);
535  else
536    {
537      gimp_drawable_flush (drawable);
538      gimp_drawable_merge_shadow (bs_param->drawable_ID, TRUE);
539      gimp_drawable_update (bs_param->drawable_ID, x1, y1, win_width, win_height);
540
541      // Draw the axes onto the image
542      // Can't for the life of me figure out how to do this for the preview...
543      for (i=0; i<blobnum; i++)
544        {
545          const gdouble linepoints [8] = {
546            mom [i].xavg+x1 - mom[i].majaxis*cos(mom[i].angle),
547            mom [i].yavg+y1 - mom[i].majaxis*sin(mom[i].angle),
548            mom [i].xavg+x1 + mom[i].majaxis*cos(mom[i].angle),
549            mom [i].yavg+y1 + mom[i].majaxis*sin(mom[i].angle),
550            mom [i].xavg+x1 - mom[i].minaxis*sin(mom[i].angle),
551            mom [i].yavg+y1 + mom[i].minaxis*cos(mom[i].angle),
552            mom [i].xavg+x1 + mom[i].minaxis*sin(mom[i].angle),
553            mom [i].yavg+y1 - mom[i].minaxis*cos(mom[i].angle) } ;
554
555          gimp_context_set_paint_mode (GIMP_NORMAL_MODE);
556          gimp_pencil (bs_param->drawable_ID, 4, linepoints);
557          gimp_pencil (bs_param->drawable_ID, 4, linepoints+4);
558        }
559    }
560
561  free (mom);
562  // Not sure why I can't free this, but it segfaults if I do...
563  //g_free (source_data);
564
565  return TRUE;
566}
567
568void sensitize_filename_hbox (GtkToggleButton *csv_toggle,
569                                     GtkWidget *filename_hbox)
570{
571  gtk_widget_set_sensitive (filename_hbox,
572                            gtk_toggle_button_get_active (csv_toggle));
573}
574
575gboolean blobstats_dialog (BlobStatsParam *bs_param)
576{
577  GtkWidget *dialog, *main_vbox, *preview, *entry_table, *csv_toggle,
578    *filename_hbox, *file_label, *filename_entry;
579  GtkObject *threshold, *color_opacity;
580  GimpDrawable *drawable;
581  gint run;
582
583  if (!(drawable = (GimpDrawable *) gimp_drawable_get (bs_param->drawable_ID)))
584    return 0;
585
586  gimp_ui_init (PLUG_IN_BINARY, TRUE);
587
588  // Dialog window
589  dialog = gimp_dialog_new ("BlobStats", PLUG_IN_BINARY, NULL, 0,
590                            gimp_standard_help_func, PLUG_IN_PROC,
591                            GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
592                            GTK_STOCK_OK, GTK_RESPONSE_OK, NULL);
593
594  gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
595  gimp_window_set_transient (GTK_WINDOW (dialog));
596
597  // Main vbox
598  main_vbox = gtk_vbox_new (FALSE, 4);
599  gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 5);
600  gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog) -> vbox), main_vbox);
601  gtk_widget_show (main_vbox);
602
603  // Effect preview
604  preview = gimp_drawable_preview_new (drawable, NULL);
605  gtk_box_pack_start (GTK_BOX (main_vbox), preview, FALSE, TRUE, 0);
606  gtk_widget_show (preview);
607
608  g_signal_connect (G_OBJECT (preview), "invalidated",
609                    G_CALLBACK (blobstats_analyze),
610                    bs_param);
611  /* g_signal_connect (G_OBJECT (gimp_preview_get_area (GIMP_PREVIEW (preview))),
612                    "button_press_event",
613                    G_CALLBACK (blobstats_preview_button_press),
614                    param); */
615
616  // Float parameter entry table
617  entry_table = gtk_table_new (4, 3, FALSE);
618  gtk_table_set_col_spacings (GTK_TABLE (entry_table), 3);
619  gtk_table_set_row_spacings (GTK_TABLE (entry_table), 3);
620  gtk_box_pack_start (GTK_BOX (main_vbox), entry_table, FALSE, FALSE, 0);
621  gtk_widget_show (entry_table);
622
623  threshold = gimp_scale_entry_new (GTK_TABLE (entry_table), 0, 0,
624                                    "Threshold", 1, 0, bs_param->p.threshold,
625                                    0., 1., 0.01, 0.1, 2, TRUE, 0, 0,
626                                    NULL, NULL);
627  g_signal_connect (G_OBJECT (threshold), "value-changed",
628                    G_CALLBACK (gimp_float_adjustment_update),
629                    &(bs_param->p.threshold));
630  g_signal_connect_swapped (G_OBJECT (threshold), "value-changed",
631                            G_CALLBACK (gimp_preview_invalidate), preview);
632
633  color_opacity = gimp_scale_entry_new (GTK_TABLE (entry_table), 0, 3,
634                                        "Blob color opacity", 1, 0,
635                                        bs_param->p.color_opacity,
636                                        0., 1., 0.01, 0.1, 2, TRUE, 0, 0,
637                                        NULL, NULL);
638  g_signal_connect (G_OBJECT (color_opacity), "value-changed",
639                    G_CALLBACK (gimp_float_adjustment_update),
640                    &(bs_param->p.color_opacity));
641  g_signal_connect_swapped (G_OBJECT (color_opacity), "value-changed",
642                            G_CALLBACK (gimp_preview_invalidate), preview);
643
644  // CSV file toggle
645  csv_toggle = gtk_check_button_new_with_mnemonic ("Save CSV file");
646  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (csv_toggle),
647                                bs_param->p.save_csv);
648  gtk_box_pack_start (GTK_BOX (main_vbox), csv_toggle, FALSE, FALSE, 0);
649  gtk_widget_show (csv_toggle);
650  g_signal_connect (G_OBJECT (csv_toggle), "toggled",
651                    G_CALLBACK (gimp_toggle_button_update),
652                    &(bs_param->p.save_csv));
653
654  // CSV file name entry (no signals, get its value when done)
655  filename_hbox = gtk_hbox_new (FALSE, 2);
656  gtk_container_border_width (GTK_CONTAINER (filename_hbox), 3);
657  gtk_box_pack_start (GTK_BOX (main_vbox), filename_hbox, FALSE, FALSE, 0);
658  gtk_widget_show(filename_hbox);
659
660  file_label = gtk_label_new ("CSV file name:");
661  gtk_box_pack_start (GTK_BOX (filename_hbox), file_label, FALSE, FALSE, 0);
662  gtk_widget_show(file_label);
663
664  filename_entry = gtk_entry_new ();
665  gtk_box_pack_start (GTK_BOX (filename_hbox), filename_entry, TRUE, TRUE, 0);
666  if (bs_param->p.filename[0])
667    gtk_entry_set_text (GTK_ENTRY (filename_entry), bs_param->p.filename);
668  gtk_widget_set_sensitive (filename_hbox, bs_param->p.save_csv);
669  gtk_widget_show(filename_entry);
670
671  g_signal_connect (G_OBJECT (csv_toggle), "toggled",
672                    G_CALLBACK (sensitize_filename_hbox),
673                    filename_hbox);
674
675  // Display, run, and destroy the dialog
676  gtk_widget_show (dialog);
677  run = ((gimp_dialog_run (GIMP_DIALOG (dialog))) == GTK_RESPONSE_OK);
678
679  strncpy (bs_param->p.filename,
680           gtk_entry_get_text (GTK_ENTRY (filename_entry)), 254);
681  bs_param->p.filename [255] = '\0';
682
683  gtk_widget_destroy (dialog);
684
685  return run;
686}
687
688static void query (void)
689{
690  static const GimpParamDef args[] = {
691    {GIMP_PDB_INT32,    "run_mode",      "Interactive, non-interactive"},
692    {GIMP_PDB_IMAGE,    "image",         "Input image"},
693    {GIMP_PDB_DRAWABLE, "drawable",      "Input drawable"},
694    {GIMP_PDB_FLOAT,    "threshold",     "Blob threshold"},
695    {GIMP_PDB_INT32,    "axes_width",    "Blob axes width"},
696    {GIMP_PDB_FLOAT,    "axes_opacity""Blob axes opacity"},
697    {GIMP_PDB_FLOAT,    "color_opacity", "Blob color opacity"},
698    {GIMP_PDB_INT8,     "save_csv",      "Save CSV output file (TRUE,FALSE)"},
699    {GIMP_PDB_STRING,   "filename",      "CSV output filename"},
700  };
701
702  gimp_install_procedure (PLUG_IN_PROC,
703                          "Display and report image blob statistics",
704                          "This plug-in analyzes, displays, and reports the "
705                          "area, centroid, and ellipsoid fit major/minor axes "
706                          "(based on second moments) of blobs whose intensity "
707                          "exceeds the threshold value.",
708                          "Adam C. Powell, IV <apowell@opennovation.com>",
709                          "Adam C. Powell, IV",
710                          "2008-2009",
711                          "BlobStats...",
712                          "RGB*, GRAY*",
713                          GIMP_PLUGIN,
714                          G_N_ELEMENTS (args), 0,
715                          args,
716                          NULL);
717
718  gimp_plugin_menu_register (PLUG_IN_PROC, "<Image>/Filters/Edge-Detect");
719}
720
721static void run (const gchar *name, gint nparams, const GimpParam *param,
722                 gint *nreturn_vals, GimpParam **return_vals)
723{
724  static GimpParam   values[1];
725  GimpRunMode        run_mode;
726  GimpPDBStatusType  status;
727  gint32             drawable_ID;
728  BlobStatsParam     *bs_param;
729
730  run_mode    = param[0].data.d_int32;
731  drawable_ID = param[2].data.d_drawable;
732
733  // Make sure drawable is RGB
734  if (!gimp_drawable_is_rgb (drawable_ID))
735    {
736      status = GIMP_PDB_EXECUTION_ERROR;
737      goto blobstats_exit;
738    }
739
740  // Create new parameter structure
741  if (! (bs_param = blobstats_param_new (drawable_ID)))
742    {
743      status = GIMP_PDB_EXECUTION_ERROR;
744      goto blobstats_exit;
745    }
746
747  switch (run_mode)
748    {
749    case GIMP_RUN_INTERACTIVE:
750      blobstats_param_restore (bs_param);
751
752      if (! blobstats_dialog (bs_param))
753        {
754          status = GIMP_PDB_SUCCESS;
755          goto blobstats_exit;
756        }
757      break;
758
759    case GIMP_RUN_NONINTERACTIVE:
760      if (! blobstats_param_set (bs_param, nparams, param))
761        {
762          status = GIMP_PDB_CALLING_ERROR;
763          goto blobstats_exit;
764        }
765      break;
766
767    case GIMP_RUN_WITH_LAST_VALS:
768      if (! blobstats_param_restore (bs_param))
769        {
770          status = GIMP_PDB_CALLING_ERROR;
771          goto blobstats_exit;
772        }
773      break;
774    }
775
776  // Run the algorithm
777  if (blobstats_analyze (NULL, bs_param))
778    status = GIMP_PDB_SUCCESS;
779  else
780    status = GIMP_PDB_EXECUTION_ERROR;
781
782  if (run_mode == GIMP_RUN_INTERACTIVE)
783    blobstats_param_store (bs_param);
784
785  if (run_mode != GIMP_RUN_NONINTERACTIVE)
786    gimp_displays_flush ();
787
788 blobstats_exit:
789
790  values[0].type          = GIMP_PDB_STATUS;
791  values[0].data.d_status = status;
792  *nreturn_vals = G_N_ELEMENTS (values);
793  *return_vals  = values;
794}
795
796const GimpPlugInInfo PLUG_IN_INFO = { NULL, NULL, query, run };
797
798// GIMP plug-in MAIN function from libgimp/gimp.h
799MAIN()