Morphological Operations

Morphological Operations

Overview

Morphological Operations are operations based on the shape of binary or grayscale images. They are mainly used for noise removal, object separation, hole filling, and more. This document covers everything from the concept of structuring elements to various applications of morphological operations.

Difficulty: ⭐⭐ (Beginner-Intermediate)

Learning Objectives: - Understanding Structuring Elements - Erosion and Dilation operations - Opening and Closing operations - Gradient, Top-hat, and Black-hat operations - Noise removal and object separation applications


Table of Contents

  1. Morphological Operations Overview
  2. Structuring Element - getStructuringElement()
  3. Erosion - erode()
  4. Dilation - dilate()
  5. Opening and Closing - morphologyEx()
  6. Gradient, Top-hat, Black-hat
  7. Practical Applications
  8. Exercises
  9. Next Steps
  10. References

1. Morphological Operations Overview

What is Morphology?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Morphological Operations Overview               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Morphology = Study of shape                                   β”‚
β”‚   Operations based on the shape of images                       β”‚
β”‚                                                                 β”‚
β”‚   Main Uses:                                                    β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚   β”‚  1. Noise removal     - Remove small noise dots          β”‚   β”‚
β”‚   β”‚  2. Hole filling      - Fill holes inside objects        β”‚   β”‚
β”‚   β”‚  3. Object separation - Separate connected objects       β”‚   β”‚
β”‚   β”‚  4. Object connection - Connect disconnected parts       β”‚   β”‚
β”‚   β”‚  5. Edge detection    - Morphological gradient           β”‚   β”‚
β”‚   β”‚  6. Skeletonization  - Extract object skeleton           β”‚   β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                 β”‚
β”‚   Basic Operations:                                             β”‚
β”‚   - Erosion: Shrink objects                                     β”‚
β”‚   - Dilation: Expand objects                                    β”‚
β”‚                                                                 β”‚
β”‚   Combined Operations:                                          β”‚
β”‚   - Opening = Erosion β†’ Dilation                                β”‚
β”‚   - Closing = Dilation β†’ Erosion                                β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Operating Principle

Morphological operations determine pixel values by moving a small mask called a Structuring Element across the image.


2. Structuring Element - getStructuringElement()

What is a Structuring Element?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Structuring Element                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Structuring Element = A small binary matrix used in operationsβ”‚
β”‚                                                                 β”‚
β”‚   Main Shapes:                                                  β”‚
β”‚                                                                 β”‚
β”‚   MORPH_RECT (Rectangle)   MORPH_CROSS (Cross)    MORPH_ELLIPSE β”‚
β”‚   β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”           β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”        β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”  β”‚
β”‚   β”‚ 1 β”‚ 1 β”‚ 1 β”‚           β”‚ 0 β”‚ 1 β”‚ 0 β”‚        β”‚ 0 β”‚ 1 β”‚ 0 β”‚  β”‚
β”‚   β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€           β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€        β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€  β”‚
β”‚   β”‚ 1 β”‚ 1 β”‚ 1 β”‚           β”‚ 1 β”‚ 1 β”‚ 1 β”‚        β”‚ 1 β”‚ 1 β”‚ 1 β”‚  β”‚
β”‚   β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€           β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€        β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€  β”‚
β”‚   β”‚ 1 β”‚ 1 β”‚ 1 β”‚           β”‚ 0 β”‚ 1 β”‚ 0 β”‚        β”‚ 0 β”‚ 1 β”‚ 0 β”‚  β”‚
β”‚   β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜           β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜        β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜  β”‚
β”‚   All directions          Vertical/Horizontal   Elliptical      β”‚
β”‚                                                                 β”‚
β”‚   Effect by Size:                                               β”‚
β”‚   - Small size (3x3): Fine processing                           β”‚
β”‚   - Large size (7x7, 9x9): Strong effect                        β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Creating Structuring Elements

import cv2
import numpy as np

# getStructuringElement(shape, ksize, anchor=(-1,-1))
# shape: Structuring element shape
# ksize: (width, height) size
# anchor: Reference point (default: center)

# Rectangle
rect_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
print("RECT (5x5):\n", rect_kernel)

# Cross
cross_kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
print("\nCROSS (5x5):\n", cross_kernel)

# Ellipse
ellipse_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
print("\nELLIPSE (5x5):\n", ellipse_kernel)

# Custom structuring element
custom_kernel = np.array([
    [0, 1, 0],
    [1, 1, 1],
    [0, 1, 0]
], dtype=np.uint8)

Visualizing Structuring Elements

import cv2
import numpy as np
import matplotlib.pyplot as plt

shapes = [
    ('RECT', cv2.MORPH_RECT),
    ('CROSS', cv2.MORPH_CROSS),
    ('ELLIPSE', cv2.MORPH_ELLIPSE)
]

sizes = [(5, 5), (7, 7), (11, 11)]

fig, axes = plt.subplots(len(shapes), len(sizes), figsize=(12, 10))

for i, (name, shape) in enumerate(shapes):
    for j, size in enumerate(sizes):
        kernel = cv2.getStructuringElement(shape, size)
        axes[i, j].imshow(kernel, cmap='gray')
        axes[i, j].set_title(f'{name} {size}')
        axes[i, j].axis('off')

plt.tight_layout()
plt.show()

3. Erosion - erode()

Erosion Operation Principle

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Erosion                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Principle:                                                    β”‚
β”‚   - Move the structuring element across the image               β”‚
β”‚   - Set center pixel to 1 only if all pixels under the         β”‚
β”‚     structuring element are 1                                   β”‚
β”‚   - If any pixel is 0, center pixel becomes 0                   β”‚
β”‚                                                                 β”‚
β”‚   Effect:                                                       β”‚
β”‚   - Shrinks foreground (white) area                             β”‚
β”‚   - Removes small noise                                         β”‚
β”‚   - Separates connected objects                                 β”‚
β”‚   - Smooths boundaries                                          β”‚
β”‚                                                                 β”‚
β”‚   Example:                                                      β”‚
β”‚   Original:           After Erosion (3x3):                      β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚     β”‚   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  β”‚                          β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚ ──▢ β”‚   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  β”‚                          β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚     β”‚   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  β”‚                          β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚     β”‚             β”‚                          β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚   Borders shrink by 1 pixel                                     β”‚
β”‚                                                                 β”‚
β”‚   Noise Removal:                                                β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚   β”‚ β–ˆβ–ˆ  β–   β–ˆβ–ˆβ–ˆβ–ˆ β”‚     β”‚ β–ˆβ–ˆ     β–ˆβ–ˆβ–ˆ  β”‚                          β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆ  β–ˆβ–ˆβ–ˆβ–ˆ  β”‚ ──▢ β”‚  β–ˆβ–ˆ    β–ˆβ–ˆ   β”‚  Small dots (β– ) removed  β”‚
β”‚   β”‚    β–   β–ˆβ–ˆβ–ˆβ–ˆ  β”‚     β”‚       β–ˆβ–ˆβ–ˆ   β”‚                          β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Using Erosion

import cv2
import numpy as np

# Prepare binary image
img = cv2.imread('binary_image.png', cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

# Create structuring element
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

# erode(src, kernel, iterations=1)
# iterations: Number of iterations (default 1)

eroded_1 = cv2.erode(binary, kernel, iterations=1)
eroded_2 = cv2.erode(binary, kernel, iterations=2)
eroded_3 = cv2.erode(binary, kernel, iterations=3)

cv2.imshow('Original', binary)
cv2.imshow('Eroded 1x', eroded_1)
cv2.imshow('Eroded 2x', eroded_2)
cv2.imshow('Eroded 3x', eroded_3)
cv2.waitKey(0)
cv2.destroyAllWindows()

Creating Erosion Test Images

import cv2
import numpy as np

# Create test image
img = np.zeros((300, 400), dtype=np.uint8)

# Large rectangle
cv2.rectangle(img, (50, 50), (150, 150), 255, -1)

# Small noise dots
for _ in range(50):
    x, y = np.random.randint(200, 350), np.random.randint(50, 250)
    cv2.circle(img, (x, y), 2, 255, -1)

# Connected circles
cv2.circle(img, (280, 150), 40, 255, -1)
cv2.circle(img, (320, 150), 40, 255, -1)

# Apply erosion
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
eroded = cv2.erode(img, kernel, iterations=1)

cv2.imshow('Original', img)
cv2.imshow('Eroded', eroded)
cv2.waitKey(0)
cv2.destroyAllWindows()

4. Dilation - dilate()

Dilation Operation Principle

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Dilation                                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Principle:                                                    β”‚
β”‚   - Move the structuring element across the image               β”‚
β”‚   - Set center pixel to 1 if any pixel under the               β”‚
β”‚     structuring element is 1                                    β”‚
β”‚   - Opposite of erosion                                         β”‚
β”‚                                                                 β”‚
β”‚   Effect:                                                       β”‚
β”‚   - Expands foreground (white) area                             β”‚
β”‚   - Fills holes                                                 β”‚
β”‚   - Connects broken parts                                       β”‚
β”‚   - Emphasizes objects                                          β”‚
β”‚                                                                 β”‚
β”‚   Example:                                                      β”‚
β”‚   Original:           After Dilation (3x3):                     β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚   β”‚   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    β”‚     β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚                          β”‚
β”‚   β”‚   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    β”‚ ──▢ β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚                          β”‚
β”‚   β”‚   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    β”‚     β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚                          β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚   Borders expand by 1 pixel                                     β”‚
β”‚                                                                 β”‚
β”‚   Connect Broken Parts:                                         β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚   β”‚ β–ˆβ–ˆ      β–ˆβ–ˆ  β”‚     β”‚ β–ˆβ–ˆβ–ˆβ–ˆ    β–ˆβ–ˆβ–ˆβ–ˆβ”‚                          β”‚
β”‚   β”‚ β–ˆβ–ˆ  ..  β–ˆβ–ˆ  β”‚ ──▢ β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  β”‚  Dotted line connected  β”‚
β”‚   β”‚ β–ˆβ–ˆ      β–ˆβ–ˆ  β”‚     β”‚ β–ˆβ–ˆβ–ˆβ–ˆ    β–ˆβ–ˆβ–ˆβ–ˆβ”‚                          β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Using Dilation

import cv2
import numpy as np

# Prepare binary image
img = cv2.imread('binary_image.png', cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

# dilate(src, kernel, iterations=1)
dilated_1 = cv2.dilate(binary, kernel, iterations=1)
dilated_2 = cv2.dilate(binary, kernel, iterations=2)
dilated_3 = cv2.dilate(binary, kernel, iterations=3)

cv2.imshow('Original', binary)
cv2.imshow('Dilated 1x', dilated_1)
cv2.imshow('Dilated 2x', dilated_2)
cv2.imshow('Dilated 3x', dilated_3)
cv2.waitKey(0)
cv2.destroyAllWindows()

Comparing Erosion and Dilation

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Test image
img = np.zeros((200, 200), dtype=np.uint8)
cv2.rectangle(img, (50, 50), (150, 150), 255, -1)
cv2.circle(img, (100, 100), 20, 0, -1)  # Inner hole

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7))

eroded = cv2.erode(img, kernel, iterations=1)
dilated = cv2.dilate(img, kernel, iterations=1)

fig, axes = plt.subplots(1, 3, figsize=(12, 4))

axes[0].imshow(img, cmap='gray')
axes[0].set_title('Original')

axes[1].imshow(eroded, cmap='gray')
axes[1].set_title('Eroded (Shrink)')

axes[2].imshow(dilated, cmap='gray')
axes[2].set_title('Dilated (Expand)')

for ax in axes:
    ax.axis('off')

plt.tight_layout()
plt.show()

5. Opening and Closing - morphologyEx()

Opening

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Opening                                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Opening = Erosion β†’ Dilation                                  β”‚
β”‚                                                                 β”‚
β”‚   Effect:                                                       β”‚
β”‚   - Removes small noise (dots)                                  β”‚
β”‚   - Maintains overall object size approximately                 β”‚
β”‚   - Breaks thin connections                                     β”‚
β”‚                                                                 β”‚
β”‚   Original    Erosion      Dilation (Opening result)            β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”                              β”‚
β”‚   β”‚β–ˆβ–ˆ β–  β–ˆβ”‚    β”‚β–ˆ     β”‚    β”‚β–ˆβ–ˆ   β–ˆβ”‚                              β”‚
β”‚   β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚ ─▢ β”‚ β–ˆβ–ˆβ–ˆβ–ˆ β”‚ ─▢ β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚                              β”‚
β”‚   β”‚  β–  β–ˆβ–ˆβ”‚    β”‚    β–ˆ β”‚    β”‚    β–ˆβ–ˆβ”‚                              β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”˜                              β”‚
β”‚   Small dots (β– ) removed                                        β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Closing

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Closing                                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Closing = Dilation β†’ Erosion                                  β”‚
β”‚                                                                 β”‚
β”‚   Effect:                                                       β”‚
β”‚   - Fills small holes                                           β”‚
β”‚   - Maintains overall object size approximately                 β”‚
β”‚   - Connects broken parts                                       β”‚
β”‚                                                                 β”‚
β”‚   Original    Dilation     Erosion (Closing result)             β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”                              β”‚
β”‚   β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚    β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚    β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚                              β”‚
β”‚   β”‚β–ˆβ–ˆβ—‹ β–ˆβ–ˆβ”‚ ─▢ β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚ ─▢ β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚                              β”‚
β”‚   β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚    β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚    β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚                              β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”˜                              β”‚
β”‚   Inner hole (β—‹) filled                                         β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Using morphologyEx()

import cv2
import numpy as np

img = cv2.imread('binary_image.png', cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

# morphologyEx(src, op, kernel, iterations=1)
# op: Operation type

# Opening: Noise removal
opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)

# Closing: Hole filling
closing = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)

# Opening then Closing (Noise removal + Hole filling)
clean = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
clean = cv2.morphologyEx(clean, cv2.MORPH_CLOSE, kernel)

cv2.imshow('Original', binary)
cv2.imshow('Opening', opening)
cv2.imshow('Closing', closing)
cv2.imshow('Open + Close', clean)
cv2.waitKey(0)
cv2.destroyAllWindows()

Comparing Opening and Closing Test

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Test image: Rectangle with noise + holes
img = np.zeros((200, 200), dtype=np.uint8)
cv2.rectangle(img, (50, 50), (150, 150), 255, -1)

# Add noise (small dots)
noise = img.copy()
for _ in range(30):
    x, y = np.random.randint(10, 45), np.random.randint(10, 190)
    cv2.circle(noise, (x, y), 2, 255, -1)
for _ in range(30):
    x, y = np.random.randint(155, 190), np.random.randint(10, 190)
    cv2.circle(noise, (x, y), 2, 255, -1)

# Add holes (inside object)
holes = noise.copy()
for _ in range(10):
    x, y = np.random.randint(60, 140), np.random.randint(60, 140)
    cv2.circle(holes, (x, y), 3, 0, -1)

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))

opening = cv2.morphologyEx(holes, cv2.MORPH_OPEN, kernel)
closing = cv2.morphologyEx(holes, cv2.MORPH_CLOSE, kernel)
both = cv2.morphologyEx(opening, cv2.MORPH_CLOSE, kernel)

fig, axes = plt.subplots(2, 2, figsize=(10, 10))

axes[0, 0].imshow(holes, cmap='gray')
axes[0, 0].set_title('Original (Noise + Holes)')

axes[0, 1].imshow(opening, cmap='gray')
axes[0, 1].set_title('Opening (Noise Removed)')

axes[1, 0].imshow(closing, cmap='gray')
axes[1, 0].set_title('Closing (Holes Filled)')

axes[1, 1].imshow(both, cmap='gray')
axes[1, 1].set_title('Open + Close')

for ax in axes.flatten():
    ax.axis('off')

plt.tight_layout()
plt.show()

6. Gradient, Top-hat, Black-hat

Morphological Gradient

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Morphological Gradient                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Gradient = Dilation - Erosion                                 β”‚
β”‚                                                                 β”‚
β”‚   Effect: Extract object outline (boundary)                     β”‚
β”‚                                                                 β”‚
β”‚   Original          Dilation           Erosion                  β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆ β”‚         β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚         β”‚  β–ˆβ–ˆ  β”‚                   β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆ β”‚    -    β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚    =    β”‚  β–ˆβ–ˆ  β”‚                   β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆ β”‚         β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚         β”‚  β–ˆβ–ˆ  β”‚                   β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                 β”‚
β”‚   Gradient Result:                                              β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”                                                      β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆ β”‚  β†’ Only outline remains                              β”‚
β”‚   β”‚ β–ˆ  β–ˆ β”‚                                                      β”‚
β”‚   β”‚ β–ˆβ–ˆβ–ˆβ–ˆ β”‚                                                      β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”˜                                                      β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Top-hat and Black-hat

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Top-hat / Black-hat                           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Top-hat = Original - Opening                                  β”‚
β”‚   - Extract small bright parts from bright areas                β”‚
β”‚   - Detect small objects brighter than background               β”‚
β”‚                                                                 β”‚
β”‚   Black-hat = Closing - Original                                β”‚
β”‚   - Extract small dark parts from dark areas                    β”‚
β”‚   - Detect small holes/objects darker than background           β”‚
β”‚                                                                 β”‚
β”‚   Applications:                                                 β”‚
β”‚   - Correct images with uneven illumination                     β”‚
β”‚   - Remove shadows from document images                         β”‚
β”‚   - Detect small defects                                        β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementation and Usage

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 9))

# Morphological gradient
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)

# Top-hat
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)

# Black-hat
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)

# Manual calculation (for verification)
dilated = cv2.dilate(img, kernel)
eroded = cv2.erode(img, kernel)
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

gradient_manual = dilated - eroded
tophat_manual = img - opening
blackhat_manual = closing - img

# Visualization
fig, axes = plt.subplots(2, 3, figsize=(15, 10))

axes[0, 0].imshow(img, cmap='gray')
axes[0, 0].set_title('Original')

axes[0, 1].imshow(gradient, cmap='gray')
axes[0, 1].set_title('Gradient (Edge)')

axes[0, 2].imshow(tophat, cmap='gray')
axes[0, 2].set_title('Top Hat (Bright spots)')

axes[1, 0].imshow(blackhat, cmap='gray')
axes[1, 0].set_title('Black Hat (Dark spots)')

# Enhance contrast using top-hat + black-hat
enhanced = cv2.add(img, tophat)
enhanced = cv2.subtract(enhanced, blackhat)
axes[1, 1].imshow(enhanced, cmap='gray')
axes[1, 1].set_title('Enhanced (Top+Black Hat)')

for ax in axes.flatten():
    ax.axis('off')
axes[1, 2].axis('off')

plt.tight_layout()
plt.show()

Summary of All Morphological Operations

import cv2

# List of operations available in morphologyEx()
operations = {
    cv2.MORPH_ERODE: "Erode",
    cv2.MORPH_DILATE: "Dilate",
    cv2.MORPH_OPEN: "Open (Erode + Dilate)",
    cv2.MORPH_CLOSE: "Close (Dilate + Erode)",
    cv2.MORPH_GRADIENT: "Gradient (Dilate - Erode)",
    cv2.MORPH_TOPHAT: "Top Hat (Src - Open)",
    cv2.MORPH_BLACKHAT: "Black Hat (Close - Src)",
    cv2.MORPH_HITMISS: "Hit-Miss (Pattern Matching)"
}

for op, name in operations.items():
    print(f"{op}: {name}")

7. Practical Applications

Noise Removal Pipeline

import cv2
import numpy as np

def remove_noise_morphology(binary_img, noise_size=3):
    """
    Remove noise using morphological operations

    Parameters:
    - binary_img: Binary image
    - noise_size: Maximum size of noise to remove
    """
    # Kernel size = noise size * 2 + 1
    kernel_size = noise_size * 2 + 1
    kernel = cv2.getStructuringElement(
        cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)
    )

    # Opening to remove small noise dots
    cleaned = cv2.morphologyEx(binary_img, cv2.MORPH_OPEN, kernel)

    # Closing to fill small holes
    cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel)

    return cleaned


# Usage example
img = cv2.imread('noisy_document.png', cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
cleaned = remove_noise_morphology(binary, noise_size=2)

Object Separation

import cv2
import numpy as np

def separate_objects(binary_img, erosion_iterations=3):
    """
    Separate connected objects
    """
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))

    # Erosion to shrink objects (break connections)
    eroded = cv2.erode(binary_img, kernel, iterations=erosion_iterations)

    # Distance transform to find center points (optional)
    dist_transform = cv2.distanceTransform(eroded, cv2.DIST_L2, 5)
    _, sure_fg = cv2.threshold(
        dist_transform, 0.5 * dist_transform.max(), 255, 0
    )
    sure_fg = np.uint8(sure_fg)

    return eroded, sure_fg


# Usage example
img = cv2.imread('connected_circles.png', cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
separated, centers = separate_objects(binary)

Document Image Preprocessing

import cv2
import numpy as np

def preprocess_document(img):
    """
    Document image preprocessing (shadow removal + binarization)
    """
    # Grayscale conversion
    if len(img.shape) == 3:
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    else:
        gray = img

    # Top-hat to extract bright background
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))

    # Black-hat to correct shadows/dark areas
    blackhat = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, kernel)

    # Subtract black-hat from original (shadow removal effect)
    no_shadow = cv2.add(gray, blackhat)

    # Adaptive binarization
    binary = cv2.adaptiveThreshold(
        no_shadow, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY, 21, 15
    )

    # Noise removal
    kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
    binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_small)
    binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel_small)

    return binary


# Usage example
img = cv2.imread('document_with_shadow.jpg')
result = preprocess_document(img)

Skeletonization

import cv2
import numpy as np

def skeletonize(img):
    """
    Extract skeleton using morphological operations
    """
    skeleton = np.zeros_like(img)
    temp = img.copy()

    kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))

    while True:
        # Opening operation
        opened = cv2.morphologyEx(temp, cv2.MORPH_OPEN, kernel)

        # Calculate difference
        diff = cv2.subtract(temp, opened)

        # Erosion
        temp = cv2.erode(temp, kernel)

        # Add to skeleton
        skeleton = cv2.bitwise_or(skeleton, diff)

        # Stop if no more white pixels
        if cv2.countNonZero(temp) == 0:
            break

    return skeleton


# Usage example
img = cv2.imread('character.png', cv2.IMREAD_GRAYSCALE)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
skeleton = skeletonize(binary)

8. Exercises

Exercise 1: Compare Structuring Element Effects

Apply erosion and dilation to the same binary image using three different structuring elements (RECT, CROSS, ELLIPSE) and analyze the differences in results.

Exercise 2: Adjust Character Thickness

Write a function to adjust the thickness of characters in handwriting images: - Positive value: Thicken with dilation - Negative value: Thin with erosion

def adjust_stroke_width(img, amount):
    """
    amount > 0: Thicken
    amount < 0: Thin
    """
    pass

Exercise 3: Compare Boundary Extraction

Extract object boundaries using three different methods and compare: 1. Morphological gradient 2. Canny edge detection 3. findContours

Exercise 4: Braille Recognition Preprocessing

Design a preprocessing pipeline to detect individual dots in braille images. (Hint: Use erosion to separate dots)

Exercise 5: Cell Separation (Watershed Preprocessing)

Implement preprocessing to separate connected cells in microscope cell images: 1. Binarization 2. Noise removal (opening/closing) 3. Find sure background area (dilation) 4. Find sure foreground area (distance transform + threshold)


9. Next Steps

In 07_Thresholding.md, you'll learn various binarization methods and thresholding techniques!

Next Topics: - Global thresholding (cv2.threshold) - OTSU automatic threshold - Adaptive thresholding - HSV-based thresholding


10. References

Official Documentation

File Related Content
05_Image_Filtering.md Filtering basics
09_Contours.md Contour detection after preprocessing

Additional References

to navigate between lessons