Introduction to Image Processing with Python — Dilation and Erosion for Beginners | by Tonichi Edeza | Towards Data Science

In a previous article, we briefly discussed the idea of adjusting an image with the use of kernels. For this article, we shall go over the finer points of this concept. In this article we shall stick to artificially generated images. Though this is a step away from how I usually discuss a topic, I believe that for this particular lesson it would be preferable. Applying erosion and dilation to images of the real world can make it difficult to perceive the very subtle changes and understand what is actually going on.
Let’s get started!
Again, first we must import the required Python Libraries.
import numpy as npimport matplotlib.pyplot as pltfrom skimage.io import imread, imshowfrom skimage.draw import circlefrom skimage.morphology import erosion, dilation
First let us create a shape that is easy to work with. A circle perhaps.
circ_image = np.zeros((100, 100))circ_image[circle(50, 50, 25)] = 1imshow(circ_image);
Great! Now let us define a kernel to apply to it.
cross = np.array([[0,1,0],                  [1,1,1],                  [0,1,0]])imshow(cross, cmap = 'gray');
Now, one of the first things we shall do is to apply the erosion function to the circle.
eroded_circle = erosion(circ_image, cross)imshow(eroded_circle);
So what’s new? The image looks practically the same. To truly see the difference we must view the images side by side.
linecolor = 'red'fig, ax = plt.subplots(1, 2, figsize=(12, 5))ax[0].imshow(circ_image, cmap = 'gray');ax[0].set_title('Original', fontsize = 19)ax[0].axvline(x = 25, color = linecolor)ax[0].axvline(x = 75, color = linecolor)ax[0].axhline(y = 25, color = linecolor)ax[0].axhline(y = 75, color = linecolor)ax[1].imshow(eroded_circle, cmap = 'gray');ax[1].set_title('Eroded', fontsize = 19)ax[1].axvline(x = 25, color = linecolor)ax[1].axvline(x = 75, color = linecolor)ax[1].axhline(y = 25, color = linecolor)ax[1].axhline(y = 75, color = linecolor)fig.tight_layout()
We see that the eroded circle has slightly shrunk. This is what it means to erode an image. If we were to iterate over the erosion function, its effect becomes quite clear.
def multi_erosion(image, kernel, iterations):    for i in range(iterations):        image = erosion(image, kernel)    return imageites = [2,4,6,8,10,12,14,16,18,20]fig, ax = plt.subplots(2, 5, figsize=(17, 5))for n, ax in enumerate(ax.flatten()):    ax.set_title(f'Iterations : {ites[n]}', fontsize = 16)    new_circle = multi_erosion(circ_image, cross, ites[n])    ax.imshow(new_circle, cmap = 'gray');    ax.axis('off')fig.tight_layout()
Excellent, the figure above clearly shows how the image is actually being eroded. Now let us try changing the kernel, what if instead of a cross kernel we use a horizontal line as well as a vertical line kernel.
h_line = np.array([[0,0,0],                  [1,1,1],                  [0,0,0]])v_line = np.array([[0,1,0],                  [0,1,0],                  [0,1,0]])fig, ax = plt.subplots(1, 2, figsize=(15, 5))ax[0].imshow(h_line, cmap='gray');ax[1].imshow(v_line, cmap='gray');fig.tight_layout()
ites = [2,4,6,8,10,12,14,16,18,20]fig, ax = plt.subplots(2, 5, figsize=(17, 5))for n, ax in enumerate(ax.flatten()):    ax.set_title(f'Horizontal Iterations : {ites[n]}', fontsize = 12)    new_circle = multi_erosion(circ_image, h_line, ites[n])    ax.imshow(new_circle, cmap = 'gray');    ax.axis('off')fig.tight_layout()fig, ax = plt.subplots(2, 5, figsize=(17, 5))for n, ax in enumerate(ax.flatten()):    ax.set_title(f'Vertical Iterationss : {ites[n]}', fontsize = 12)    new_circle = multi_erosion(circ_image, v_line, ites[n])    ax.imshow(new_circle, cmap = 'gray');    ax.axis('off')fig.tight_layout()
As we can see, the horizontal and vertical erosions affected the image in very different ways. Using the horizontal kernel gives us a slimmer circle, while using the vertical kernel gives us a flatter circle. For some of you this may seem odd.
“I am using a vertical kernel, why is my circle getting flatter?”. You may ask.
Remember that we are eroding the image. It is looking for vertical and horizontal lines and slowly chipping away at them. This point will be made clearer when we do image dilation.
The below function will allow you to set the image you want to dilate, the dilation kernel, and the number of iterations.
def multi_dilation(image, kernel, iterations):    for i in range(iterations):        image = dilation(image, kernel)    return image
To illustrate this function, let us once again dilate the circle and see the difference.
dilated_circle = multi_dilation(circ_image, cross, 1)linecolor = 'red'fig, ax = plt.subplots(1, 2, figsize=(12, 5))ax[0].imshow(circ_image, cmap = 'gray');ax[0].set_title('Original', fontsize = 19)ax[0].axvline(x = 25, color = linecolor)ax[0].axvline(x = 75, color = linecolor)ax[0].axhline(y = 25, color = linecolor)ax[0].axhline(y = 75, color = linecolor)ax[1].imshow(dilated_circle, cmap = 'gray');ax[1].set_title('Dilated', fontsize = 19)ax[1].axvline(x = 25, color = linecolor)ax[1].axvline(x = 75, color = linecolor)ax[1].axhline(y = 25, color = linecolor)ax[1].axhline(y = 75, color = linecolor)fig.tight_layout()
We can clearly see that the circle now crosses over the red line, a clear indication that it has grown. Let us now run an iteration of horizontal and vertical dilations.
ites = [2,4,6,8,10,12,14,16,18,20]fig, ax = plt.subplots(2, 5, figsize=(17, 5))for n, ax in enumerate(ax.flatten()):    ax.set_title(f'Horizontal Iterations : {ites[n]}', fontsize =                  12)    new_circle = multi_dilation(circ_image, h_line, ites[n])    ax.imshow(new_circle, cmap = 'gray');    ax.axis('off')fig.tight_layout()fig, ax = plt.subplots(2, 5, figsize=(17, 5))for n, ax in enumerate(ax.flatten()):    ax.set_title(f'Vertical Iterationss : {ites[n]}', fontsize = 12)    new_circle = multi_dilation(circ_image, v_line, ites[n])    ax.imshow(new_circle, cmap = 'gray');    ax.axis('off')fig.tight_layout()
We now see very clearly that the horizontal dilation increases the images width while the vertical dilation increases its height.
“But how can you apply this?”. You may ask. The key is in understanding how the kernel work on a fundamental level. To better appreciate this let us look at a relatively complex image.
complex_image = imread('complex_image.png')imshow(complex_image);
In the image above we have a mixture of horizontal lines, vertical lines, and circles. We can view each of these shapes in isolation by using the dilation and erosion functions.
To isolate for circles we can first erode the lines, we can confidently do this as the circles are much more rotund than the lines and can therefore can “survive” more erosion iterations. I arrived at the rest by playing around with the iterations. One thing to remember however is to dilate your image at the end. I dilated the image by the same kernels and iterations as I eroded them with. That should return the circle to approximately the same size. I know that technically I could have nested all these functions into one line, but for pedagogical reasons I chose to break them up into multiple lines.
step_1 = multi_erosion(complex_image, h_line,3)step_2 = multi_erosion(step_1, v_line,3)step_3 = multi_dilation(step_2, h_line,3)step_4 = multi_dilation(step_3, v_line,3)steps = [step_1, step_2, step_3, step_4]names = ['Step 1', 'Step 2', 'Step 3', 'Step 4']fig, ax = plt.subplots(2, 2, figsize=(10, 10))for n, ax in enumerate(ax.flatten()):    ax.set_title(f'{names[n]}', fontsize = 22)    ax.imshow(steps[n], cmap = 'gray');    ax.axis('off')fig.tight_layout()
Likewise, the below codes will filter for the horizontal lines.
step_1 = multi_erosion(complex_image, cross, 20)step_2 = multi_dilation(step_1, h_line, 20)step_3 = multi_dilation(step_2, v_line,2)steps = [step_1, step_2, step_3]names = ['Step 1', 'Step 2', 'Step 3']fig, ax = plt.subplots(1, 3, figsize=(10, 10))for n, ax in enumerate(ax.flatten()):    ax.set_title(f'{names[n]}', fontsize = 22)    ax.imshow(steps[n], cmap = 'gray');    ax.axis('off')fig.tight_layout()
We see that the number of steps taken and number of iterations depend entirely the image and what we are trying to filter. To filter for the the vertical lines, I found it necessary to create a new kernel.
long_v_line = np.array([[0,1,0],                        [0,1,0],                        [0,1,0],                        [0,1,0],                        [0,1,0]])step_1 = multi_erosion(complex_image, long_v_line, 10)step_2 = multi_dilation(step_1 ,long_v_line, 10)steps = [step_1, step_2]names = ['Step 1', 'Step 2']fig, ax = plt.subplots(1, 2, figsize=(10, 10))for n, ax in enumerate(ax.flatten()):    ax.set_title(f'{names[n]}', fontsize = 22)    ax.imshow(steps[n], cmap = 'gray');    ax.axis('off')fig.tight_layout()
Notice how this filtering only took two iterations. This was because I defined the kernel to have features that only the vertical lines had. This insight is crucial. Do not constrain yourself to the kernels you find online or in your textbook. Some images can be easily filtered if you define specific kernels for them.
In Conclusion
Kernal erosion and dilation are fundamental concepts to understand in the world of Image Processing. They may even be one of the first lessons on any image processing module. Understanding them intuitively will be key to your success in this field later on. This article barely scratch the surface of the many different nuances of erosion and dilation (I only had one example of a non-3x3 matrix!), but I hope it gave you enough to at least continue studying the topic on your own.