Mathematical Morphology - Learning Reflection

Author: Tony Fu
Date: August 19, 2023
Device: MacBook Pro 16-inch, Late 2021 (M1 Pro)
Code: GitHub
Reference: Chapter 4 Digital Image Processing with C++: Implementing Reference Algorithms with the CImg Library by Tschumperlé, Tilmant, Barra

1. Erosion and Dilation

I found the book's explanation of erosion and dilation to be a bit confusing. I would recommend watching this video for a more intuitive explanation of the concepts.

Mathematical morphology operations have a straightforward definition when dealing with binary images. For grayscale images, the definitions become more complex:

Binary Images

For binary images and a structuring element , the operations can be defined as follows:

  1. Dilation:

  2. Erosion:

Gray-Level Images

For grayscale images and a structuring element , the definitions change slightly:

  1. Dilation:

Dilation causes bright regions to expand and dark regions to contract.

  1. Erosion:

Erosion causes bright regions to contract and dark regions to expand.

In CImg, we can perform erosion and dilation using the erode() and dilate() functions. Both functions take a structuring element B as an argument.

CImg<unsigned char> img("coins.png");
CImg<> lum = img.get_norm().blur(0.75f);
lum.threshold(lum.median()).normalize(0, 255);

CImg<unsigned char> B = CImg<unsigned char>(3, 3).fill(1);
CImg<unsigned char>
    imgErode = lum.get_erode(B),   // Erosion
    imgDilate = lum.get_dilate(B); // Dilation"
  • Original:

original

  • Binarized Luminance:

threshold

  • Then Apply Erosion:

erosion

  • ... or Dilation:

dilation

2. Opening and Closing

The opening and closing operations are defined as follows (works for both binary and grayscale images):

  1. Opening:

  2. Closing:

CImg<unsigned char> B = CImg<unsigned char>(3, 3).fill(1);
CImg<unsigned char>
    imgErode = lum.get_erode(B),   // Erosion
    imgDilate = lum.get_dilate(B), // Dilation"
    imgOpen = imgErode.get_dilate(B), // Opening
    imgClose = imgDilate.get_erode(B); // Closing
  • Opening: Erode, then dilate (removes small objects and smooths larger ones)

opening

  • Closing: Dilate, then erode (closes small holes and joins nearby objects)

closing

3. Kramer-Bruckner Filter

The Kramer-Bruckner filter is a specific morphological filter used to enhance contrast and reduce noise in an image. It performs a combination of dilation and erosion, typically using a circular structuring element. In math, it can be expressed as the following two formulas:

  1. Compute the mid-value for each pixel:

  2. Assign the output image values based on the input image and mid-value:

Here, is the input image, is the structuring element, denotes erosion, and denotes dilation.

CImg<> KramerBruckner(CImg<> &imgIn, int n)
{
    CImg<>
        imgOut(imgIn),
        mask = CImg<>(n, n).fill(1),
        imgErode = imgIn.get_erode(mask),
        imgDilate = imgIn.get_dilate(mask); // Dilation
    cimg_forXY(imgOut, x, y)
    {
        float M = 0.5f * (imgErode(x, y) + imgDilate(x, y));
        imgOut(x, y) = (imgIn(x, y) <= M ? imgErode(x, y) : imgDilate(x, y));
    }
    return imgOut;
}

kramer-bruckner

4. Alternating Sequential Filters (ASF):

Alternating Sequential Filters are a series of morphological operations that are applied sequentially, often involving both erosions and dilations with increasing sizes of structuring elements.

  • ASF (n = 1): Erosion, then dilation

asf1

  • ASF (n = 3): (Erosion, then dilation) * 3

asf3

  • ASF (n = 11):

asf7

5. Other Morphological Operations

1. Morphological Gradients

Morphological gradients measure the difference between dilation and erosion of an image. In this code, two gradients are computed: the erosion gradient and the dilation gradient.

  • Erosion Gradient ():
  • Dilation Gradient ():

Gradient E:

gradientE

Gradient D:

gradientD

2. Beucher Gradient

The Beucher gradient is another way of expressing the difference between dilation and erosion. It's calculated as the difference between the dilated and eroded images.

  • Beucher Gradient:

beucher

3. Top Hat Transformations

Top hat transformations emphasize differences between the original image and its morphological opening or closing.

  • White Top Hat:
  • Black Top Hat:

White Top Hat:

whiteTopHat

Black Top Hat:

blackTopHat

4. Edge Detector (contourMin and contourMax)

These operations compute the minimum and maximum between the erosion and dilation gradients, respectively.

  • Contour Min:
  • Contour Max:

Contour Min (this is blank because there is no overlap between gradEand gradD)

contourMin

Contour Max:

contourMax

5. Nonlinear Laplacian

The nonlinear Laplacian is the difference between the dilation and erosion gradients.

  • Nonlinear Laplacian:

laplacian

6. Skeletonization

Here we implement the Zhang-Suen skeletonization algorithm using the CImg library in C++. Very Important Note: this method requires that the input image is binary (i.e., pixels are either 0 or 1).

Iterative Skeletonization Function

The main function for skeletonization is IterSkeleton. This function takes a binary image as input and performs two passes, removing certain edge pixels in each pass.

Initializing Variables

CImg<unsigned char> D(imgIn.width(), imgIn.height(), 1, 1, 0);
CImg_3x3(N, unsigned char);

Here, we use D to tag pixels for removal. Whenever a pixel is tagged, we set its value to 1.

Note that CImg_3x3 has the macro format CImg_3x3(I,T):

#define CImg_3x3(I,T) T I[9]; \
        T& I##pp = I[0]; T& I##cp = I[1]; T& I##np = I[2]; \
        T& I##pc = I[3]; T& I##cc = I[4]; T& I##nc = I[5]; \
        T& I##pn = I[6]; T& I##cn = I[7]; T& I##nn = I[8]; \
        I##pp = I##cp = I##np = \
        I##pc = I##cc = I##nc = \
        I##pn = I##cn = I##nn = 0

So that in the subsequent code, we can use N to access the 8 neighbors of a pixel. The variables Npp, Ncp, Nnp, Npc, Nnc, Npn, Ncn, Nnn represent the 8 neighboring pixels around the central pixel (x, y). The variable names correspond to their relative positions:

  • Npp: Previous row, previous column.
  • Ncp: Current row, previous column.
  • Nnp: Next row, previous column.
  • Npc: Previous row, current column.
  • Nnc: Next row, current column.
  • Npn: Previous row, next column.
  • Ncn: Current row, next column.
  • Nnn: Next row, next column.
  • The central pixel itself is referred to as Ncc.

cimg_for3x3_grid

Pass 1

The first pass checks each pixel and its 8 neighbors to determine if it should be removed:

Here, C is the the number of 0-1 transitions in the 8-neighborhood of the pixel. The book's description "C(N), called connectivity number, expressing how many binary components are connected to the central pixel (x,y)" appears to be incorrect.

The conditions for removal are:

Intuitively, this means that the pixel should be removed if:

  • It has 2-6 neighbors. (Because if B is 0 or 1, then the pixel is an end point or an isolated point.)
  • It has exactly 1 0-1 transition in its 8-neighborhood because it is likely to be a boundary pixel.
  • and for , and and for : These are specific conditions for Zhang-Suen thinning. They ensure that no spur pixels (pixels that stick out from the object) are created during the thinning process.

    For , these conditions make sure that the pixel is not an endpoint in the south and east directions, and that removing it won't break connectivity. The conditions for (see below) do a similar thing, but in the north and west directions. By separating the thinning process into two stages, the algorithm avoids the possibility of over-thinning or under-thinning.

// Pass 1
int n1 = 0;
cimg_for3x3(imgIn, x, y, 0, 0, N, unsigned char)
{
    if (imgIn(x, y))
    {
        // Compute B and C here...
        bool R1 = B >= 2 && B <= 6 && C == 1 && Ncn * Nnc * Ncp == 0 && Npc * Ncn * Nnc == 0;
        if (R1)
        {
            // Tag (x,y)
            D(x, y) = 1;
            ++n1;
        }
    }
}

Removing Tagged Pixels from Pass 1

cimg_forXY(imgIn, x, y)
    imgIn(x, y) -= (n1 > 0) * D(x, y);

Pass 2

The second pass is similar to the first, but with different conditions for removal:

Removing Tagged Pixels from Pass 2

Similar to the removal in Pass 1.

Return the Total Number of Removed Pixels

return n1 + n2;

Iterative Skeletonization

int num_removed;
do
{
    num_removed = IterSkeleton(lum);
} while (num_removed > 0);

Here, we keep calling IterSkeleton until no pixels are removed in a pass.

Results

  • Original:

original

  • Skeleton:

skeleton