Image Erosion Explained in Depth using NumPy

Image Erosion Explained in Depth using NumPy

Introduction

Erosion operation is one of the important morphological operations (morphological transformations) that follows a technique of mathematical morphology for the analysis and processing of geometrical structures.

To get a general idea of what erosion has to do with images, we can think of this as an operation in which it tries to reduce the shape that is contained in the input image. It is just like the erosion of soil but just that this operation erodes the boundaries of the foreground object.

Credits of Cover Image - Photo by Laura Colquitt on Unsplash

To represent this operation mathematically -

$$A \circleddash B$$

where -

  • A → Input Image
  • B → Structuring element or kernel

The resultant of the above formula gives the eroded image. The structuring element is basically a kernel where the image matrix is operated as a 2D convolution.

Note - This blog post covers the erosion process done on binary images. Also, it is often preferred to use binary images for morphological transformation.

Concept of Erosion

As discussed, we only use the binary images that consist of pixels either 0 or 1 (0 or 255 to be more precise). The structuring element or kernel is either a subset of the image matrix or not which also, is a binary representation that is mostly a square matrix.

Let us consider A as the image matrix and B as the kernel. We have conditions as follows:

  • We have to position the center element of B to the element iteratively taken in the image A.

  • We consider the submatrix of A to be the size B and check if the submatrix is exactly equivalent to B.

  • If yes, replace the pixel value to be 1 or 255 otherwise 0.

We need to do this for all the elements of A with B.

Imagine the image matrix A as -

first-iter.png

and structuring element or kernel B as -

kernel.png

The binary image of the matrix A would be something like below.

rimg-10.png

For easy calculation, we shall pad the image by a pad_width equal to (kernel size - 2) with which the submatrix can be selected easily. The GIF can be seen below to visually know the inner working of the convolution.

image-erosion.gif

Now that we know what to do, let's code the same using the library as well as from scratch.

Time to Code

The packages that we mainly use are:

  • NumPy
  • Matplotlib
  • OpenCV → It is only used for reading the image (in this article).

python_packages.png

import the Packages

import numpy as np
import cv2
import json
from matplotlib import pyplot as plt

Read the Image

Since we do the morphological transformations on binary images, we shall make sure whatever image we read is binarized. Therefore, we have the following function.

def read_this(image_file):
    image_src = cv2.imread(image_file, 0)
    return image_src

def convert_binary(image_src, thresh_val):
    color_1 = 255
    color_2 = 0
    initial_conv = np.where((image_src <= thresh_val), image_src, color_1)
    final_conv = np.where((initial_conv > thresh_val), initial_conv, color_2)
    return final_conv

def binarize_this(image_file, thresh_val=127):
    image_src = read_this(image_file=image_file)
    image_b = convert_binary(image_src=image_src, thresh_val=thresh_val)
    return image_b

Note - By default we are reading the image in grayscale mode.

Code Implementation with Library

For this, we will be using a different image, and for the implementation, we will use the method erode() available in the module cv2. The parameters are as follows:

  • image_file → The image that we want to apply the transformation.

  • level → Basically the erosion level with which the structuring element or kernel's size is decided.

  • with_plot → To obtain the results of both the original image and the transformed image.

def erode_lib(image_file, level=3, with_plot=True):
    level = 3 if level < 3 else level
    image_src = binarize_this(image_file=image_file)
    # library method
    image_eroded = cv2.erode(src=image_src, kernel=np.ones((level, level)), iterations=1)

    if with_plot:
        cmap_val = 'gray'
        fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))

        ax1.axis("off")
        ax1.title.set_text('Original')

        ax2.axis("off")
        ax2.title.set_text("Eroded - {}".format(level))

        ax1.imshow(image_src, cmap=cmap_val)
        ax2.imshow(image_eroded, cmap=cmap_val)
        plt.show()
        return True
    return image_eroded

Let's test the above function -

erode_lib(image_file='wish.jpg', level=3, with_plot=True)

wish_erode_lib.png

Clearly, we can see the some of the pixels got reduced showing the pixel erosion.

Code Implementation from Scratch

As explained earlier, we need to carefully choose the pad_width depending upon the erosion_level. We normally take (kernel size - 2) or (erosion_level - 2) and here, the kernel is always square matrix.

After this, we shall also take the submatrices to position the center element of the kernel with each element of the image matrix iteratively. We make sure that the submatrix size is equal to kernel size. Hence we first pad the matrix with pad_width.

Let's code the erosion function from scratch.

def erode_this(image_file, erosion_level=3, with_plot=False):
    erosion_level = 3 if erosion_level < 3 else erosion_level

    structuring_kernel = np.full(shape=(erosion_level, erosion_level), fill_value=255)
    image_src = binarize_this(image_file=image_file)

    orig_shape = image_src.shape
    pad_width = erosion_level - 2

    # pad the matrix with `pad_width`
    image_pad = np.pad(array=image_src, pad_width=pad_width, mode='constant')
    pimg_shape = image_pad.shape
    h_reduce, w_reduce = (pimg_shape[0] - orig_shape[0]), (pimg_shape[1] - orig_shape[1])

    # sub matrices of kernel size
    flat_submatrices = np.array([
        image_pad[i:(i + erosion_level), j:(j + erosion_level)]
        for i in range(pimg_shape[0] - h_reduce) for j in range(pimg_shape[1] - w_reduce)
    ])

    # condition to replace the values - if the kernel equal to submatrix then 255 else 0
    image_erode = np.array([255 if (i == structuring_kernel).all() else 0 for i in flat_submatrices])
    image_erode = image_erode.reshape(orig_shape)

    if with_plot:
        cmap_val = 'gray'
        fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))

        ax1.axis("off")
        ax1.title.set_text('Original')

        ax2.axis("off")
        ax2.title.set_text("Eroded - {}".format(erosion_level))

        ax1.imshow(image_src, cmap=cmap_val)
        ax2.imshow(image_erode, cmap=cmap_val)
        plt.show()
        return True
    return image_erode

Let's test the above function -

erode_this(image_file='wish.jpg', erosion_level=3, with_plot=True)

wish_erode_scratch.png

Clearly, we can see the reduction in the pixel values showcasing the pixel erosion. This, we implemented by convolution 2D technique totally from scratch. When compared to the speed of the algorithm, it is a bit slow compared to that of the library method.

References