;+
; NAME:
;       BLOB_ANALYZER__DEFINE
;
; PURPOSE:
;
;       The purpose of this routine is to create a system for analyzing
;       regions of interest (ROIs) or (more commonly) "blobs" inside images.
;       In particular, given a suitable image (this will require judgement on
;       your part), the program will automatically select "blobs" or connected
;       regions in the image and make it possible for you to analyze these
;       blobs. An example program is provided to show you one way the program
;       can be used.
;
;       The code is a wrapper, essentially, for LABEL_REGION and HISTOGRAM, with
;       a couple of my other image processing routines (FIND_BOUNDARY and FIT_ELLIPSE)
;       used in a supporting role.
;
; AUTHOR:
;
;       FANNING SOFTWARE CONSULTING
;       David Fanning, Ph.D.
;       1645 Sheely Drive
;       Fort Collins, CO 80526 USA
;       Phone: 970-221-0438
;       E-mail: david@idlcoyote.com
;       Coyote's Guide to IDL Programming: http://www.idlcoyote.com
;
; CATEGORY:
;
;       Analysis, Image Processing
;
; CALLING SEQUENCE:
;
;       analyzer = Obj_New("BLOB_ANALYZER", image)
;
; INPUTS:
;
;   image:           A two-dimensional image array. To make this program memory efficient,
;                    a copy of the image is *not* stored in the object. You will be responsible
;                    for image operations outside this program.
;
; KEYWORDS:
;
;   ALL_NEIGHBORS:    Set this keyword to look at all eight neighbors when searching
;                     for connectivity. The default is to look for four neighbors on
;                     each side of the starting pixel. Passed directly to LABEL_REGION.
;
;   MASK:             A two-dimensional array, the same size as image, that identifies the
;                     foreground and background pixels in the image. Applying the mask
;                     should result in a bi-level image of 0s and 1s, where 1 identifies the
;                     blobs you wish to analyze. If a mask is not provided, the mask is created
;                     like this:
;
;                     mask = image > 0
;
;   SCALE:            A one- or two-dimensional given the pixel scaling parameters. By default [1.0, 1.0].
;                     If passed a scalar, the scale parameter is applied to both the X and Y directions of
;                     each pixel. Statistical output is reported with scaling unless the NOSCALE keyword
;                     is set. Scaling also effects the data that is output from the various methods.
;
; OBJECT METHODS:
;
;   The following methods are provided. Please see the documentation header for each method for
;   information on arguments and keywords that can be used with the method.
;
;   FitEllipse:       This method fits an ellipse to the blob. It returns information about the fitted
;                     ellipse, including the points that all the ellipse to be drawn.
;
;   GetIndices:       This method returns the indices for a particular blob in the image.
;
;   GetStats:         This method returns a structure containing statistics for a particular blob in the image.
;                     The structure is defined as follows:
;
;                     stats = {INDEX: indexNumber, $                  ; The index number of this blob.
;                              COUNT: N_Elements(indices), $          ; The number of indices in this blob.
;                              PERIMETER_PTS: boundaryPts, $          ; A [2,n] array of points that describe the blob perimeter.
;                              PIXEL_AREA: pixelArea, $               ; The area as calculated by pixels in the blob.
;                              PERIMETER_AREA: perimeterArea, $       ; The area as calculated by the blob perimeter. (Smaller than pixel area.)
;                              CENTER: centroid[0:1], $               ; The [x,y] array that identifies the centroid of the blob.
;                              PERIMETER_LENGTH: perimeter_length, $  ; The perimenter length (scaled unless the NOSCALE keyword is set).
;                              SCALE: scale, $                        ; The [xscale, yscale] array used in scaling.
;                              MINCOL: Min(xyindices[0,*]), $         ; The minimum column index in the blob.
;                              MAXCOL: Max(xyindices[0,*]), $         ; The maximum column index in the blob.
;                              MINROW: Min(xyindices[1,*]), $         ; The minimum row index in the blob.
;                              MAXROW: Max(xyindices[1,*])}           ; The maximum row index in the blob.
;
;   NumberOfBlobs:     The number of blobs identified in the image.
;
;   ReportStats:       This methods reports statistics on every identified blob in the image. The
;                      report can be sent to the display (the default) or to a file. The format for
;                      the report works for most images, but you may have to change the format or override
;                      this method for your particular image. The reported statistics are basically the
;                      output of the GetStats and FitEllipse methods.
;
;    Here is an example of statistical output from the example program below.
;
;  INDEX   NUM_PIXELS   CENTER_X    CENTER_Y   PIXEL_AREA   PERIMETER_AREA   PERIMETER_LENGTH  MAJOR_AXIS   MINOR_AXIS    ANGLE
;     0        426        107.89       9.78       106.50          98.00            37.56          12.15        11.29       -8.05
;     1        580        151.97      10.22       145.00         134.25            49.21          17.49        11.77       -0.99
;     2        812        266.29      15.36       203.00         190.75            52.56          17.88        14.65     -107.48
;     3       1438        204.53      43.29       359.50         344.13            70.23          21.68        21.12      -76.47
;
; RESTRICTIONS:
;
;       Requires programs from the Coyote Library. At the very least, those below are required.
;       It is *highly* recommended that you install the entire library. FIT_ELLIPSE has been
;       changed specifically for this release, so by sure you get a copy of that with this
;       source code.
;
;       The program currently works only with 2D bi-level images.
;
; EXAMPLE:
;
;       To run an example program. Compile the file and type "example" at the IDL command line.
;
;       IDL> .compile blob_analyzer__define
;       IDL> example
;
; MODIFICATION HISTORY:
;
;       Written by David W. Fanning, Fanning Software Consulting, 17 August 2008.
;       Ideas taken from discussion with Ben Tupper and Ben's program HBB_ANALYZER.
;       Example program rewritten to bring up-to-date with Coyote Library routines. 9 March 2015. DWF.
;-
;******************************************************************************************;
;  Copyright (c) 2008, by Fanning Software Consulting, Inc.                                ;
;  All rights reserved.                                                                    ;
;                                                                                          ;
;  Redistribution and use in source and binary forms, with or without                      ;
;  modification, are permitted provided that the following conditions are met:             ;
;                                                                                          ;
;      * Redistributions of source code must retain the above copyright                    ;
;        notice, this list of conditions and the following disclaimer.                     ;
;      * Redistributions in binary form must reproduce the above copyright                 ;
;        notice, this list of conditions and the following disclaimer in the               ;
;        documentation and/or other materials provided with the distribution.              ;
;      * Neither the name of Fanning Software Consulting, Inc. nor the names of its        ;
;        contributors may be used to endorse or promote products derived from this         ;
;        software without specific prior written permission.                               ;
;                                                                                          ;
;  THIS SOFTWARE IS PROVIDED BY FANNING SOFTWARE CONSULTING, INC. ''AS IS'' AND ANY        ;
;  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES    ;
;  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT     ;
;  SHALL FANNING SOFTWARE CONSULTING, INC. BE LIABLE FOR ANY DIRECT, INDIRECT,             ;
;  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED    ;
;  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;         ;
;  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND             ;
;  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT              ;
;  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS           ;
;  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.                            ;
;******************************************************************************************;

;
; NAME:
;  Blob_Analyzer::FitEllipse
;
; PURPOSE:
;
;   This function fits an ellipse to a particular blob and returns information
;   about the fit to the user.
;
; CALLING SEQUENCE:
;
;   ellipsePts = theBlobs -> FitEllipse(indexNumber)
;
; RETURN VALUE:
;
;     ellipsePts:   A [2,n] array containing the XY points of the fitted ellipse. The values
;                   are in scaled units unless the NOSCALE keyword is set, in which case the
;                   values are in DEVICE units.
;
; ARGUMENTS:
;
;    indexNumber:   The index number of the blob. Indices start at 0 and go to n-1.
;
; INPUT KEYWORDS:
;
;    NOSCALE:       Set this keyword if you would prefer that lengths and positions NOT be
;                   scaled in the output of this function. If not scaled, results are in pixel
;                   or device coordinates. The default is to scale all output.
;
;    NPOINTS:       The number of points in the ellipse. By default, 120.
;
; OUTPUT KEYWORDRS:
;
;    AXES:          A two-element array containing the lengths of the major and minor axes,
;                   respectively. Lenghts are scaled unless the NOSCALE keyword is set.
;
;    CENTER:        A two-element array containing the [x,y] values of the center of the ellipse.
;                   Values are scaled unless the NOSCALE keyword is set.
;
;    ORIENTATION:   The orientation of the ellipse. The value is in degrees counter-clockwise of
;                   the postive X direction.  Note that a value of 60 is the same as a value of 240.
;                   In other words, there is no direction associated with this value.
;
;    SEMIAXES:      A two-element array containing the lengths of the semi-major and semi-minor axes,
;                   respectively. Lenghts are scaled unless the NOSCALE keyword is set. (Half the length
;                   of AXES.
;
;
FUNCTION Blob_Analyzer::FitEllipse, indexNumber, $
    AXES=axes, $
    CENTER=center, $
    NOSCALE=noscale, $
    NPOINTS=npoints, $
    ORIENTATION=orientation, $
    SEMIAXES=semiAxes

    Compile_Opt idl2

    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN, -1
    ENDIF

     ; Argument checking.
    IF N_Elements(indexNumber) EQ 0 THEN indexNumber = 0
    IF indexNumber LT 0 THEN Message, 'Required index number must be positive.'
    IF indexNumber GE self.count THEN Message, 'Index number exceeds total number of blobs.'
    IF N_Elements(npoints) EQ 0 THEN npoints = 120
    IF Keyword_Set(noscale) THEN scale = [1.0, 1.0] ELSE scale = self.scale

    ; Get the indices.
    indices = self ->GetIndices(indexNumber, XSIZE=xsize, YSIZE=ysize)

    ellipsePts = Fit_Ellipse(indices, XSIZE=xsize, YSIZE=ysize, NPOINTS=npoints, $
        AXES=axes, SEMIAXES=semiAxes, ORIENTATION=orientation, SCALE=scale)

    RETURN, ellipsePts
END ; ------------------------------------------------------------------------------


;
; NAME:
;  Blob_Analyzer::GetIndices
;
; PURPOSE:
;
;   This function returns the indices of a blob to the caller.
;
; CALLING SEQUENCE:
;
;   indices = theBlobs -> GetIndices(indexNumber)
;
; RETURN VALUE:
;
;     indices:     A vector of blob indices that describes the blob in the original image.
;
; ARGUMENTS:
;
;    indexNumber:   The index number of the blob. Indices start at 0 and go to n-1.
;
; INPUT KEYWORDS:
;
;    None.
;
; OUTPUT KEYWORDRS:
;
;    COUNT:         The number of indices in the indices vector.
;
;    XSIZE:         The X size of the image from which the blob is taken.
;
;    YSIZE:         The Y size of the image from which the blob is taken.
;
;
FUNCTION Blob_Analyzer::GetIndices, indexNumber, COUNT=count, XSIZE=xsize, YSIZE=ysize

    Compile_Opt idl2

    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN, -1
    ENDIF

    ; Argument checking.
    IF N_Elements(indexNumber) EQ 0 THEN indexNumber = 0
    IF indexNumber LT 0 THEN Message, 'Required index number must be positive.'
    IF indexNumber GE self.count THEN Message, 'Index number exceeds total number of blobs.'

    ; Return the size of the image, if the user asked for them.
    IF Arg_Present(xsize) THEN xsize = self.xsize
    IF Arg_Present(ysize) THEN ysize = self.ysize

    ; Get the indices:
    indices = (*self.ri)[(*self.ri)[indexNumber]:(*self.ri)[indexNumber+1]-1]
    IF Arg_Present(count) THEN count = N_Elements(indices)

    ; Return them.
    RETURN, indices

END ; ------------------------------------------------------------------------------


;
; NAME:
;  Blob_Analyzer::GetStats
;
; PURPOSE:
;
;   This function returns statistics of the blob in question.
;
; CALLING SEQUENCE:
;
;   statistics = theBlobs -> GetStats(indexNumber)
;
; RETURN VALUE:
;
;     statistics:   A structure of statistics that is defined like this.
;
;                     stats = {INDEX: indexNumber, $                  ; The index number of this blob.
;                              COUNT: N_Elements(indices), $          ; The number of indices in this blob.
;                              PERIMETER_PTS: boundaryPts, $          ; A [2,n] array of points that describe the blob perimeter.
;                              PIXEL_AREA: pixelArea, $               ; The area as calculated by pixels in the blob.
;                              PERIMETER_AREA: perimeterArea, $       ; The area as calculated by the blob perimeter. (Smaller than pixel area.)
;                              CENTER: centroid[0:1], $               ; The [x,y] array that identifies the centroid of the blob.
;                              PERIMETER_LENGTH: perimeter_length, $  ; The perimenter length (scaled unless the NOSCALE keyword is set).
;                              SCALE: scale, $                        ; The [xscale, yscale] array used in scaling.
;                              MINCOL: Min(xyindices[0,*]), $         ; The minimum column index in the blob.
;                              MAXCOL: Max(xyindices[0,*]), $         ; The maximum column index in the blob.
;                              MINROW: Min(xyindices[1,*]), $         ; The minimum row index in the blob.
;                              MAXROW: Max(xyindices[1,*])}           ; The maximum row index in the blob.
;
; ARGUMENTS:
;
;    indexNumber:   The index number of the blob. Indices start at 0 and go to n-1.
;
; INPUT KEYWORDS:
;
;    NOSCALE:       Set this keyword if you would prefer that lengths and positions NOT be
;                   scaled in the output of this function. If not scaled, results are in pixel
;                   or device coordinates. The default is to scale all output.
;
; OUTPUT KEYWORDRS:
;
;    INDICES:       A vector of blob indices that describes the blob in the original image.
;
;    XYINDICES:     A 2xN array of column/row indices that describes teh blob in the original image.
;
; NOTES:
;
;     The statistics are calculated by calling FIND_BOUNDARY from the Coyote Library. This program
;     uses a chain-code algorithm to calculate the perimeter and report the blob area using either of
;     two methods: a strict pixel area (counts the number of pixels in the blob times the scale factor
;     and takes the total), or it uses the perimeter to calculate an area using the method described in
;     Russ, The Image Processing Handbook, 2nd Edition, pp490+. The perimeter area is almost always less
;     than the pixel area.
;
FUNCTION Blob_Analyzer::GetStats, indexNumber, INDICES=indices, NOSCALE=noscale, XYINDICES=xyindices

    Compile_Opt idl2

    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN, -1
    ENDIF

    ; Argument checking.
    IF N_Elements(indexNumber) EQ 0 THEN indexNumber = 0
    IF indexNumber LT 0 THEN Message, 'Required index number must be positive.'
    IF indexNumber GE self.count THEN Message, 'Index number exceeds total number of blobs.'
    IF Keyword_Set(noscale) THEN scale = [1.0D, 1.0D] ELSE scale = self.scale

    ; Get the indices.
    indices = self ->GetIndices(indexNumber, XSIZE=xsize, YSIZE=ysize)

    ; Calculate the indices in terms of col/row coordinates.
    xyindices = Array_Indices([xsize,ysize], indices, /DIMENSIONS)

    ; Calculate statistics.
    boundaryPts = Find_Boundary(indices, $
        SCALE=scale, $
        XSIZE=xsize, $
        YSIZE=ysize, $
        AREA=pixelArea, $
        PERIM_AREA=perimeterArea, $
        CENTER=centroid, $
        PERIMETER=perimeter_length)

    ; Report them.
    stats = {INDEX: indexNumber, $                   ; The index number of thisblob.
             COUNT: N_Elements(indices), $           ; The number of indices in this blob.
             PERIMETER_PTS: boundaryPts, $           ; A [2,n] array of points that describe the blob perimeter.
             PIXEL_AREA: pixelArea, $                ; The area as calculated by pixels in the blob.
             PERIMETER_AREA: perimeterArea, $        ; The area as calculated by the blob perimeter. (Smaller than pixel area.)
             CENTER: centroid[0:1], $                ; The [x,y] array that identifies the centroid of the blob.
             PERIMETER_LENGTH: perimeter_length, $   ; The perimenter length (scaled unless the NOSCALE keyword is set).
             SCALE: scale, $                         ; The [xscale, yscale] array used in scaling.
             MINCOL: Min(xyindices[0,*]), $          ; The minimum column index in the blob.
             MAXCOL: Max(xyindices[0,*]), $          ; The maximum column index in the blob.
             MINROW: Min(xyindices[1,*]), $          ; The minimum row index in the blob.
             MAXROW: Max(xyindices[1,*])}            ; The maximum row index in the blob.

     RETURN, stats

END ; ------------------------------------------------------------------------------


;
; NAME:
;  Blob_Analyzer::NumberOfBlobs
;
; PURPOSE:
;
;   This function returns the number of blobs in the input image.
;
; CALLING SEQUENCE:
;
;   numBlobs = theBlobs -> NumberOfBlobs()
;
; RETURN VALUE:
;
;     numBlobs:   The number of blobs in the input image.
;
; ARGUMENTS:
;
;    None.
;
; KEYWORDS:
;
;    None.
;
FUNCTION Blob_Analyzer::NumberOfBlobs
    RETURN, self.count
END ; ------------------------------------------------------------------------------


;
; NAME:
;  Blob_Analyzer::ReportStats
;
; PURPOSE:
;
;   This function reports statistics on blobs in the image.
;
; CALLING SEQUENCE:
;
;   theBlobs -> ReportStats
;
; ARGUMENTS:
;
;    None.
;
; INPUT KEYWORDS:
;
;    FILENAME:      The name of a file to contain the statistical output.
;
;    NOSCALE:       Set this keyword if you would prefer that lengths and positions NOT be
;                   scaled in the output of this function. If not scaled, results are in pixel
;                   or device coordinates. The default is to scale all output.
;
;    TOFILE:         Normally the report is sent to standard ouput. If this keyword is set,
;                    the output is sent to a file.
;
; OUTPUT KEYWORDRS:
;
;    None.
;
; EXAMPLE:
;
;    Here is an example of statistical output from the example program below.
;
;  INDEX   NUM_PIXELS   CENTER_X    CENTER_Y   PIXEL_AREA   PERIMETER_AREA   PERIMETER_LENGTH  MAJOR_AXIS   MINOR_AXIS    ANGLE
;     0        426        107.89       9.78       106.50          98.00            37.56          12.15        11.29       -8.05
;     1        580        151.97      10.22       145.00         134.25            49.21          17.49        11.77       -0.99
;     2        812        266.29      15.36       203.00         190.75            52.56          17.88        14.65     -107.48
;     3       1438        204.53      43.29       359.50         344.13            70.23          21.68        21.12      -76.47
;
PRO Blob_Analyzer::ReportStats, NOSCALE=noscale, TOFILE=tofile, FILENAME=filename

    Compile_Opt idl2

    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        IF N_Elements(lun) NE 0 THEN Free_Lun, lun
        RETURN
    ENDIF

     ; Argument checking.
     tofile = Keyword_Set(tofile)
     IF tofile THEN BEGIN
        IF N_Elements(filename) EQ 0 THEN BEGIN
            filename = Dialog_Pickfile(Title='Select file for statistical ouput.')
            IF filename EQ "" THEN RETURN
        ENDIF

            ; Open the file for writing.
            OpenW, lun, filename, /GET_LUN, WIDTH=100
     ENDIF

     ; Get the stats and output them to the display or to a file.
     header = '  INDEX   NUM_PIXELS   CENTER_X    CENTER_Y   PIXEL_AREA   PERIMETER_AREA   PERIMETER_LENGTH  MAJOR_AXIS   MINOR_AXIS    ANGLE'

     ; Write the header.
     IF tofile THEN $
        PrintF, lun, header ELSE $
        Print, header

     ; Write the rest of the information.
     format = '(x,I5, 6x,I5, 4x,F10.2, 1x,F10.2, 3x,F10.2, 5x,F10.2, 7x,F10.2, 5x,F10.2, 3x,F10.2, 2x,F10.2)'
     FOR j=0, self.count-1 DO BEGIN
        stats = self -> GetStats(j, NOSCALE=noscale)
        void = self -> FitEllipse(j, AXES=axes, ORIENTATION=angle, NOSCALE=noscale)
         IF tofile THEN BEGIN
            PrintF, lun, StrTrim(j,2), stats.count, stats.center[0], stats.center[1], $
                stats.pixel_area, stats.perimeter_area,  $
                stats.perimeter_length, axes[0], axes[1], angle, FORMAT=format
         ENDIF ELSE BEGIN
            Print, StrTrim(j,2), stats.count, stats.center[0], stats.center[1], $
                stats.pixel_area, stats.perimeter_area,  $
                stats.perimeter_length, axes[0], axes[1], angle, FORMAT=format
         ENDELSE

     ENDFOR

     ; Close the file, if open.
     IF tofile THEN Free_Lun, lun

END ; ------------------------------------------------------------------------------


PRO Blob_Analyzer::CLEANUP
    Ptr_Free, self.ri
END ; ------------------------------------------------------------------------------


;
; NAME:
;  Blob_Analyzer::INIT
;
; PURPOSE:
;
;   This function initializes the Blob_Analyzer object.
;
; CALLING SEQUENCE:
;
;   theBlobs = Obj_New('Blob_Analyzer', image)
;
; ARGUMENTS:
;
;   image:           A two-dimensional image array. To make this program memory efficient,
;                    a copy of the image is *not* stored in the object. You will be responsible
;                    for image operations outside this program.
;
; KEYWORDS:
;
;   ALL_NEIGHBORS:    Set this keyword to look at all eight neighbors when searching
;                     for connectivity. The default is to look for four neighbors on
;                     each side of the starting pixel. Passed directly to LABEL_REGION.
;
;   MASK:             A two-dimensional array, the same size as image, that identifies the
;                     foreground and background pixels in the image. Applying the mask
;                     should result in a bi-level image of 0s and 1s, where 1 identifies the
;                     blobs you wish to analyze. If a mask is not provided, the mask is created
;                     like this:
;
;                     mask = image > 0
;
;   SCALE:            A one- or two-dimensional given the pixel scaling parameters. By default [1.0, 1.0].
;                     If passed a scalar, the scale parameter is applied to both the X and Y directions of
;                     each pixel. Statistical output is reported with scaling unless the NOSCALE keyword
;                     is set. Scaling also effects the data that is output from the various methods.
;
FUNCTION Blob_Analyzer::INIT, inputImage, $
    ALL_NEIGHBORS=all_neighbors, $
    MASK=mask, $
    SCALE=scale

    Compile_Opt idl2

    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN, 0
    ENDIF

    ; Do you have an image. It is required.
    IF N_Elements(inputImage) EQ 0 THEN Message, 'An input image is a required argument.'

    ; Is the image 2D?
    ndims = Size(inputImage, /N_DIMENSIONS)
    IF ndims NE 2 THEN Message, 'The BLOB_ANALYZER only works with 2D images.'

    ; Check keywords.
    IF N_Elements(mask) EQ 0 THEN BEGIN
        image = inputImage GT 0
    ENDIF ELSE BEGIN

        ; Is the mask the same size as the image?
        IF Total(Size(inputImage, /DIMENSIONS) EQ Size(mask, /DIMENSIONS)) NE 2 THEN $
            Message, 'The image mask is not the same size as the input image.'

        ; Apply the mask to the image.
        image = inputImage * mask
    ENDELSE

    ; Check other keywords.
    IF N_Elements(scale) EQ 0 THEN scale = [1.0, 1.0]

    ; Get the size of the image.
    s = Size(image, /DIMENSIONS)
    xsize = s[0]
    ysize = s[1]
    type = Size(image, /TYPE)

    ; Label the regions.
    bigImage = Make_Array(s+2, TYPE=type)
    bigImage[1,1] = Temporary(image)
    labeledImage = Label_Region(bigImage, ALL_NEIGHBORS=Keyword_Set(all_neighbors))
    image = (Temporary(labeledImage))[1:xsize, 1:ysize]

    ; Get the indices of the label image by taking its histogram.
    h = Histogram(image, REVERSE_INDICES=ri, BINSIZE=1.0, MIN=1)
    count = N_Elements(h)

    ; Populate the object.
    self.count = count
    self.ri = Ptr_New(ri, /NO_COPY)
    self.xsize = xsize
    self.ysize = ysize
    self.scale = scale

    RETURN, 1

END ; ------------------------------------------------------------------------------


PRO Blob_Analyzer_Example_Program

  ; Get a file for analysis.
  file = FILEPATH('r_seeberi.jpg', SUBDIRECTORY = ['examples', 'data'])
  READ_JPEG, file, image, /GRAYSCALE

  ; Define a structuring kernal for an opening operation on the image.
  radius = 5
  kernel = SHIFT(DIST(2*radius+1), radius, radius) LE radius

  ; Apply the opening operator to the image.
  openImage = MORPH_OPEN(image, kernel, /GRAY)

  ; Threshold the image to prepare to remove background noise.
  ; Notice that changing this value can produce more or less
  ; artifacts. You will have to decide what you can live with
  ; in your analysis. It requires some judgement on your part.
  mask = openImage GE 150

  ; Do the analysis.
  blobs = Obj_New('blob_analyzer', openImage, MASK=mask)

  ; Display the original image
  s = Size(image, /DIMENSIONS)
  cgDisplay, 800, 800, Aspect=image, Title='Blob Analyzer Example'
  cgLoadCT, 0
  cgImage, image, Position=[0.0, 0.5, 0.5, 1.0]

  ; Display the opened image beside it.
  cgImage, openImage, Position=[0.5, 0.5, 1.0, 1.0], /NoErase

  ; Display the blobs we located with LABEL_REGION.
  count = blobs -> NumberOfBlobs()
  blank = BytArr(s[0], s[1])
  FOR j=0,count-1 DO BEGIN
    blobIndices = blobs -> GetIndices(j)
    blank[blobIndices] = image[blobIndices]
  ENDFOR
  cgImage, blank, Position=[0.0, 0.0, 0.5, 0.5], /NoErase


  ; Display the original image, with blob outlined and labelled.
  cgImage, image, Position=[0.5, 0.0, 1.0, 0.5], /NoErase, $
     /Save, XRange=[0,1], YRange=[0,1]

  FOR j=0,count-1 DO BEGIN

      ; Convert the perimeter points into normalized data coordinates.
      stats = blobs -> GetStats(j, /NoScale)
      xpts = Reform(stats.perimeter_pts[0,*]) / Float(s[0])
      ypts = Reform(stats.perimeter_pts[1,*]) / Float(s[1])
      xcenter = stats.center[0] / Float(s[0])
      ycenter = stats.center[1] / Float(s[1])
      cgPlots, xpts, ypts, COLOR='dodger blue', /DATA
      cgText, xcenter, ycenter, StrTrim(j,2), $
        COLOR='red', ALIGNMENT=0.5, CHARSIZE=0.75, /DATA
  ENDFOR

  ; Add labels for captions.
  cgText, 0.05, 0.95, 'A', FONT=0, ALIGNMENT=0.5, COLOR='Yellow', /NORMAL
  cgText, 0.55, 0.95, 'B', FONT=0, ALIGNMENT=0.5, COLOR='Yellow', /NORMAL
  cgText, 0.05, 0.45, 'C', FONT=0, ALIGNMENT=0.5, COLOR='Yellow', /NORMAL
  cgText, 0.55, 0.45, 'D', FONT=0, ALIGNMENT=0.5, COLOR='Yellow', /NORMAL

  ; Report stats.
  blobs -> ReportStats

  ; Destroy the object.
  Obj_Destroy, blobs

END ; ------------------------------------------------------------------------------


PRO Blob_Analyzer__DEFINE, class

    class = { BLOB_ANALYZER, $
              count: 0L, $        ; The total number of blobs being analyzed.
              ri: Ptr_New(), $    ; The reverse indices from HISTOGRAM. Used to get the blob indices.
              xsize: 0L, $        ; The X size of the image the indices originate in.
              ysize: 0L, $        ; The Y size of the image the indices originate in.
              scale: FltArr(2) $  ; The X and Y pixel scale for the indices. Set to [1,1] by default.
           }

END ; ------------------------------------------------------------------------------
