Image Filtering

Image Filtering

Overview

Image filtering is the process of transforming pixel values by considering neighboring pixels. Various effects such as noise removal, blur, and sharpening can be achieved. In this document, we'll learn from the concepts of kernels and convolution to various filter functions in OpenCV.

Difficulty: ⭐⭐ (Beginner-Intermediate)

Learning Objectives: - Understand kernel and convolution concepts - Learn various blur filters (blur, GaussianBlur, medianBlur, bilateralFilter) - Edge-preserving smoothing - Implement custom filters and sharpening


Table of Contents

  1. Kernels and Convolution
  2. Average Blur - blur()
  3. Gaussian Blur - GaussianBlur()
  4. Median Blur - medianBlur()
  5. Bilateral Filter - bilateralFilter()
  6. Custom Filter - filter2D()
  7. Sharpening Filter
  8. Practice Problems
  9. Next Steps
  10. References

1. Kernels and Convolution

What is a Kernel?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Kernel                                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   A kernel (or filter, mask) is a small matrix that defines    β”‚
β”‚   the operation to apply to an image. Typically 3x3, 5x5, 7x7. β”‚
β”‚                                                                 β”‚
β”‚   Example: 3x3 average filter kernel                            β”‚
β”‚                                                                 β”‚
β”‚        1/9   1/9   1/9         β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”                   β”‚
β”‚                                β”‚1/9β”‚1/9β”‚1/9β”‚                   β”‚
β”‚        1/9   1/9   1/9    =    β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€                   β”‚
β”‚                                β”‚1/9β”‚1/9β”‚1/9β”‚                   β”‚
β”‚        1/9   1/9   1/9         β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€                   β”‚
β”‚                                β”‚1/9β”‚1/9β”‚1/9β”‚                   β”‚
β”‚                                β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜                   β”‚
β”‚                                                                 β”‚
β”‚   Kernel size meaning:                                          β”‚
β”‚   - Larger size considers wider area                            β”‚
β”‚   - Large kernel = strong effect, slow processing              β”‚
β”‚   - Small kernel = weak effect, fast processing                β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Convolution Operation

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Convolution Operation                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Apply kernel to each pixel of input image to calculate new valueβ”‚
β”‚                                                                 β”‚
β”‚   Input image          3x3 kernel           Output              β”‚
β”‚   β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”   β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”                           β”‚
β”‚   β”‚ 1 β”‚ 2 β”‚ 3 β”‚ 4 β”‚   β”‚1/9β”‚1/9β”‚1/9β”‚                           β”‚
β”‚   β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€   β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€      Result pixel:         β”‚
β”‚   β”‚ 5 β”‚ 6 β”‚ 7 β”‚ 8 β”‚   β”‚1/9β”‚1/9β”‚1/9β”‚   (1+2+3+5+6+7+9+10+11)/9 β”‚
β”‚   β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€   β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€      = 54/9 = 6            β”‚
β”‚   β”‚ 9 β”‚10 β”‚11 β”‚12 β”‚   β”‚1/9β”‚1/9β”‚1/9β”‚                           β”‚
β”‚   β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€   β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜                           β”‚
β”‚   β”‚13 β”‚14 β”‚15 β”‚16 β”‚                                            β”‚
β”‚   β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜                                            β”‚
β”‚                                                                 β”‚
β”‚   Process:                                                      β”‚
β”‚   1. Place kernel over image                                    β”‚
β”‚   2. Multiply corresponding pixels                              β”‚
β”‚   3. Sum all results                                            β”‚
β”‚   4. Move to next pixel and repeat                              β”‚
β”‚                                                                 β”‚
β”‚   Border handling:                                              β”‚
β”‚   - BORDER_CONSTANT: Fill with constant value (default 0)       β”‚
β”‚   - BORDER_REPLICATE: Replicate border pixels                   β”‚
β”‚   - BORDER_REFLECT: Reflect at border                           β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Convolution Visualization

import cv2
import numpy as np

def visualize_convolution(img, kernel):
    """Visualize convolution process (for learning)"""
    h, w = img.shape
    kh, kw = kernel.shape
    pad = kh // 2

    # Add padding
    padded = np.pad(img, pad, mode='constant', constant_values=0)

    # Result array
    result = np.zeros_like(img, dtype=np.float64)

    # Convolution (slow version - for learning)
    for y in range(h):
        for x in range(w):
            region = padded[y:y+kh, x:x+kw]
            result[y, x] = np.sum(region * kernel)

    return result


# Example
img = np.array([
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
    [13, 14, 15, 16]
], dtype=np.float64)

kernel = np.ones((3, 3)) / 9  # Average filter

result = visualize_convolution(img, kernel)
print("Input:\n", img)
print("\nResult:\n", result)

2. Average Blur - blur()

Basic Usage

Average blur is the simplest blur filter, using the average value of the kernel area.

import cv2

img = cv2.imread('image.jpg')

# blur(src, ksize)
# ksize: kernel size in (width, height) format

blur_3x3 = cv2.blur(img, (3, 3))
blur_5x5 = cv2.blur(img, (5, 5))
blur_7x7 = cv2.blur(img, (7, 7))
blur_15x15 = cv2.blur(img, (15, 15))

cv2.imshow('Original', img)
cv2.imshow('3x3 Blur', blur_3x3)
cv2.imshow('5x5 Blur', blur_5x5)
cv2.imshow('15x15 Blur', blur_15x15)
cv2.waitKey(0)
cv2.destroyAllWindows()

Average Blur Kernel

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Average Blur Kernel                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   3x3 average kernel:                                           β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”                                          β”‚
β”‚   β”‚ 1/9 β”‚ 1/9 β”‚ 1/9 β”‚                                          β”‚
β”‚   β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€                                          β”‚
β”‚   β”‚ 1/9 β”‚ 1/9 β”‚ 1/9 β”‚  =  1/9 Γ— [[1, 1, 1],                   β”‚
β”‚   β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€           [1, 1, 1],                    β”‚
β”‚   β”‚ 1/9 β”‚ 1/9 β”‚ 1/9 β”‚           [1, 1, 1]]                    β”‚
β”‚   β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”˜                                          β”‚
β”‚                                                                 β”‚
β”‚   5x5 average kernel:                                           β”‚
β”‚   All values are 1/25                                           β”‚
β”‚                                                                 β”‚
β”‚   Features:                                                     β”‚
β”‚   - Simple and fast                                             β”‚
β”‚   - Edges also get blurred                                      β”‚
β”‚   - Effective for uniform noise removal                         β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

boxFilter()

A generalized version of blur().

import cv2

img = cv2.imread('image.jpg')

# normalize=True (default): Normalize kernel (average filter)
# normalize=False: Sum filter
blur_normalized = cv2.boxFilter(img, -1, (5, 5), normalize=True)
sum_filter = cv2.boxFilter(img, -1, (5, 5), normalize=False)

# Same as blur(img, (5, 5))
print(f"Difference: {np.sum(np.abs(cv2.blur(img, (5, 5)) - blur_normalized))}")  # 0

3. Gaussian Blur - GaussianBlur()

What is Gaussian Filter?

Gaussian filter is a blur filter that gives more weight to the center. It produces a natural blur effect.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Gaussian Kernel                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Gaussian distribution (normal distribution, bell shape):      β”‚
β”‚                                                                 β”‚
β”‚          β–²                                                      β”‚
β”‚          β”‚     β–ˆβ–ˆβ–ˆβ–ˆ                                             β”‚
β”‚          β”‚   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ                                           β”‚
β”‚          β”‚  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ                                          β”‚
β”‚          β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ                                         β”‚
β”‚          β”‚β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ                                        β”‚
β”‚          └──────────────────▢                                   β”‚
β”‚                   Weight decreases away from center             β”‚
β”‚                                                                 β”‚
β”‚   3x3 Gaussian kernel (approximate):                            β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”                                          β”‚
β”‚   β”‚ 1   β”‚ 2   β”‚ 1   β”‚                                          β”‚
β”‚   β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€  Γ—  1/16                                 β”‚
β”‚   β”‚ 2   β”‚ 4   β”‚ 2   β”‚                                          β”‚
β”‚   β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€                                          β”‚
β”‚   β”‚ 1   β”‚ 2   β”‚ 1   β”‚                                          β”‚
β”‚   β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”˜                                          β”‚
β”‚                                                                 β”‚
β”‚   Features:                                                     β”‚
β”‚   - More natural result than average blur                       β”‚
β”‚   - Often used for edge detection preprocessing                β”‚
β”‚   - Control blur strength with sigma (Οƒ) value                 β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Usage

import cv2

img = cv2.imread('image.jpg')

# GaussianBlur(src, ksize, sigmaX, sigmaY=0)
# ksize: Kernel size (must be odd)
# sigmaX: Standard deviation in X direction (0 = auto-calculate from kernel size)
# sigmaY: Standard deviation in Y direction (0 = same as sigmaX)

# Specify kernel size (sigma auto-calculated)
blur1 = cv2.GaussianBlur(img, (5, 5), 0)

# Specify sigma (kernel size auto-adjusted appropriately)
blur2 = cv2.GaussianBlur(img, (0, 0), 3)  # sigma=3

# Specify both kernel size and sigma
blur3 = cv2.GaussianBlur(img, (7, 7), 1.5)

Relationship Between Sigma and Kernel Size

import cv2
import numpy as np

# Generate Gaussian kernel directly to check
def show_gaussian_kernel(ksize, sigma):
    kernel = cv2.getGaussianKernel(ksize, sigma)
    kernel_2d = kernel @ kernel.T  # 1D to 2D
    print(f"Kernel ({ksize}x{ksize}, sigma={sigma}):")
    print(np.round(kernel_2d, 4))
    print(f"Sum: {np.sum(kernel_2d):.4f}\n")


show_gaussian_kernel(3, 0)   # sigma auto-calculated
show_gaussian_kernel(5, 0)
show_gaussian_kernel(5, 1.0)
show_gaussian_kernel(5, 2.0)

# Recommended: sigma = 0.3 * ((ksize - 1) * 0.5 - 1) + 0.8

Average Blur vs Gaussian Blur

import cv2
import matplotlib.pyplot as plt

img = cv2.imread('image.jpg')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Compare with same kernel size
ksize = 15
avg_blur = cv2.blur(img, (ksize, ksize))
gauss_blur = cv2.GaussianBlur(img, (ksize, ksize), 0)

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

axes[0].imshow(img_rgb)
axes[0].set_title('Original')

axes[1].imshow(cv2.cvtColor(avg_blur, cv2.COLOR_BGR2RGB))
axes[1].set_title('Average Blur')

axes[2].imshow(cv2.cvtColor(gauss_blur, cv2.COLOR_BGR2RGB))
axes[2].set_title('Gaussian Blur')

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

plt.tight_layout()
plt.show()

4. Median Blur - medianBlur()

What is Median Filter?

Median filter uses the median value of the kernel area. Very effective for salt-and-pepper noise removal.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Median Filter Operation                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Input region:                                                 β”‚
β”‚   β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”                                             β”‚
β”‚   β”‚ 10 β”‚ 20 β”‚ 30 β”‚                                             β”‚
β”‚   β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€                                             β”‚
β”‚   β”‚ 40 β”‚255 β”‚ 60 β”‚   ← Center 255 is noise (salt)              β”‚
β”‚   β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€                                             β”‚
β”‚   β”‚ 70 β”‚ 80 β”‚ 90 β”‚                                             β”‚
β”‚   β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”˜                                             β”‚
β”‚                                                                 β”‚
β”‚   Sort values: 10, 20, 30, 40, 60, 70, 80, 90, 255             β”‚
β”‚   Median: 60 (5th value)                                        β”‚
β”‚                                                                 β”‚
β”‚   Result:                                                       β”‚
β”‚   β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”                                             β”‚
β”‚   β”‚    β”‚    β”‚    β”‚                                             β”‚
β”‚   β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€                                             β”‚
β”‚   β”‚    β”‚ 60 β”‚    β”‚   ← Noise removed                           β”‚
β”‚   β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€                                             β”‚
β”‚   β”‚    β”‚    β”‚    β”‚                                             β”‚
β”‚   β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”˜                                             β”‚
β”‚                                                                 β”‚
β”‚   Features:                                                     β”‚
β”‚   - Very effective for salt-and-pepper noise                   β”‚
β”‚   - Preserves edges relatively well                            β”‚
β”‚   - Slower than average/Gaussian                                β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Usage

import cv2
import numpy as np

img = cv2.imread('image.jpg')

# Add salt-and-pepper noise (for testing)
def add_salt_pepper_noise(img, amount=0.05):
    noisy = img.copy()
    h, w = img.shape[:2]
    num_pixels = int(amount * h * w)

    # Salt (white)
    for _ in range(num_pixels):
        y, x = np.random.randint(0, h), np.random.randint(0, w)
        noisy[y, x] = 255

    # Pepper (black)
    for _ in range(num_pixels):
        y, x = np.random.randint(0, h), np.random.randint(0, w)
        noisy[y, x] = 0

    return noisy


noisy_img = add_salt_pepper_noise(img, 0.02)

# medianBlur(src, ksize)
# ksize: Only odd numbers allowed (3, 5, 7, ...)
median_3 = cv2.medianBlur(noisy_img, 3)
median_5 = cv2.medianBlur(noisy_img, 5)

# Compare: average blur, Gaussian blur
avg_blur = cv2.blur(noisy_img, (5, 5))
gauss_blur = cv2.GaussianBlur(noisy_img, (5, 5), 0)

cv2.imshow('Noisy', noisy_img)
cv2.imshow('Average Blur', avg_blur)
cv2.imshow('Gaussian Blur', gauss_blur)
cv2.imshow('Median Blur', median_5)
cv2.waitKey(0)
cv2.destroyAllWindows()

5. Bilateral Filter - bilateralFilter()

What is Bilateral Filter?

Bilateral filter smooths while preserving edges. Used for skin retouching, artistic effects, etc.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Bilateral Filter Principle                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Regular Gaussian filter:                                      β”‚
β”‚   - Only considers distance β†’ edges also blurred                β”‚
β”‚                                                                 β”‚
β”‚   Bilateral filter:                                             β”‚
β”‚   - Considers both distance (spatial) + color difference        β”‚
β”‚   - Only includes similar-colored pixels in average             β”‚
β”‚   - Preserves edges (where color difference is large)           β”‚
β”‚                                                                 β”‚
β”‚   Example:                                                      β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚   β”‚ 100  100  100 β”‚ 200  200  200 β”‚          β”‚                   β”‚
β”‚   β”‚ 100  100  100 β”‚ 200  200  200 β”‚  ← Edge  β”‚                   β”‚
β”‚   β”‚ 100  100  100 β”‚ 200  200  200 β”‚          β”‚                   β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                 β”‚
β”‚   Gaussian: 100 and 200 mix to around 150                       β”‚
β”‚   Bilateral: 100 area stays 100, 200 area stays 200             β”‚
β”‚                                                                 β”‚
β”‚   Weight = spatial Gaussian Γ— color Gaussian                    β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Usage

import cv2

img = cv2.imread('portrait.jpg')

# bilateralFilter(src, d, sigmaColor, sigmaSpace)
# d: Filter size (-1 = auto-calculate from sigmaSpace)
# sigmaColor: Sigma in color space (higher = average wider color range)
# sigmaSpace: Sigma in coordinate space (higher = consider wider area)

# Weak effect
bilateral_weak = cv2.bilateralFilter(img, 9, 50, 50)

# Medium effect
bilateral_medium = cv2.bilateralFilter(img, 9, 75, 75)

# Strong effect (painting-like)
bilateral_strong = cv2.bilateralFilter(img, 15, 100, 100)

# Very strong effect
bilateral_extreme = cv2.bilateralFilter(img, 15, 150, 150)

Skin Smoothing Example

import cv2
import numpy as np

def skin_smoothing(img, strength='medium'):
    """Skin smoothing effect"""
    params = {
        'weak': (5, 30, 30),
        'medium': (9, 75, 75),
        'strong': (15, 100, 100),
        'extreme': (20, 150, 150)
    }

    d, sigmaColor, sigmaSpace = params.get(strength, params['medium'])

    # Apply bilateral filter
    smooth = cv2.bilateralFilter(img, d, sigmaColor, sigmaSpace)

    # Blend with original (natural effect)
    alpha = 0.7  # Blending ratio
    result = cv2.addWeighted(smooth, alpha, img, 1 - alpha, 0)

    return result


img = cv2.imread('portrait.jpg')
result = skin_smoothing(img, 'medium')

cv2.imshow('Original', img)
cv2.imshow('Smoothed', result)
cv2.waitKey(0)
cv2.destroyAllWindows()

Blur Filter Comparison

import cv2
import time
import matplotlib.pyplot as plt

img = cv2.imread('image.jpg')

# Compare processing time
filters = []

start = time.time()
avg = cv2.blur(img, (9, 9))
filters.append(('Average', avg, time.time() - start))

start = time.time()
gauss = cv2.GaussianBlur(img, (9, 9), 0)
filters.append(('Gaussian', gauss, time.time() - start))

start = time.time()
median = cv2.medianBlur(img, 9)
filters.append(('Median', median, time.time() - start))

start = time.time()
bilateral = cv2.bilateralFilter(img, 9, 75, 75)
filters.append(('Bilateral', bilateral, time.time() - start))

# Visualization
fig, axes = plt.subplots(2, 2, figsize=(12, 12))
axes = axes.flatten()

for ax, (name, result, elapsed) in zip(axes, filters):
    ax.imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
    ax.set_title(f'{name} ({elapsed*1000:.1f}ms)')
    ax.axis('off')

plt.tight_layout()
plt.show()

6. Custom Filter - filter2D()

filter2D() Usage

filter2D() allows performing convolution with custom-defined kernels.

import cv2
import numpy as np

img = cv2.imread('image.jpg')

# filter2D(src, ddepth, kernel)
# ddepth: Output image depth (-1 = same as input)
# kernel: User-defined kernel

# Create and apply average filter manually
kernel_avg = np.ones((5, 5), np.float32) / 25
avg_custom = cv2.filter2D(img, -1, kernel_avg)

# Same result as blur()
avg_builtin = cv2.blur(img, (5, 5))
print(f"Difference: {np.sum(np.abs(avg_custom - avg_builtin))}")  # 0

Various Custom Kernels

import cv2
import numpy as np

img = cv2.imread('image.jpg')

# 1. Emboss effect
kernel_emboss = np.array([
    [-2, -1, 0],
    [-1,  1, 1],
    [ 0,  1, 2]
])
emboss = cv2.filter2D(img, -1, kernel_emboss) + 128

# 2. Edge detection (Laplacian)
kernel_laplacian = np.array([
    [0,  1, 0],
    [1, -4, 1],
    [0,  1, 0]
])
laplacian = cv2.filter2D(img, -1, kernel_laplacian)

# 3. Sobel X (vertical edges)
kernel_sobel_x = np.array([
    [-1, 0, 1],
    [-2, 0, 2],
    [-1, 0, 1]
])
sobel_x = cv2.filter2D(img, -1, kernel_sobel_x)

# 4. Sobel Y (horizontal edges)
kernel_sobel_y = np.array([
    [-1, -2, -1],
    [ 0,  0,  0],
    [ 1,  2,  1]
])
sobel_y = cv2.filter2D(img, -1, kernel_sobel_y)

Kernel Visualization Tool

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

def apply_and_show_kernel(img, kernel, title):
    """Visualize kernel application result and kernel"""
    result = cv2.filter2D(img, -1, kernel)

    fig, axes = plt.subplots(1, 3, figsize=(15, 5))

    # Original
    axes[0].imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    axes[0].set_title('Original')
    axes[0].axis('off')

    # Kernel visualization
    im = axes[1].imshow(kernel, cmap='RdBu_r', vmin=-2, vmax=2)
    axes[1].set_title(f'Kernel ({kernel.shape[0]}x{kernel.shape[1]})')
    for i in range(kernel.shape[0]):
        for j in range(kernel.shape[1]):
            axes[1].text(j, i, f'{kernel[i,j]:.1f}',
                        ha='center', va='center', fontsize=10)
    plt.colorbar(im, ax=axes[1])

    # Result
    axes[2].imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
    axes[2].set_title(title)
    axes[2].axis('off')

    plt.tight_layout()
    plt.show()


img = cv2.imread('image.jpg')

# Example: Emboss kernel
kernel_emboss = np.array([
    [-2, -1, 0],
    [-1,  1, 1],
    [ 0,  1, 2]
], dtype=np.float32)

apply_and_show_kernel(img, kernel_emboss, 'Emboss')

7. Sharpening Filter

Sharpening Principle

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Sharpening Principle                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Sharpening = Original + (Original - Blur)                     β”‚
β”‚              = Original + High-frequency component              β”‚
β”‚              = Edge enhancement                                 β”‚
β”‚                                                                 β”‚
β”‚   Or directly with kernel:                                      β”‚
β”‚                                                                 β”‚
β”‚   Basic sharpening kernel:                                      β”‚
β”‚   β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”                                             β”‚
β”‚   β”‚  0 β”‚ -1 β”‚  0 β”‚                                             β”‚
β”‚   β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€                                             β”‚
β”‚   β”‚ -1 β”‚  5 β”‚ -1 β”‚   Center = 5 (original weight)              β”‚
β”‚   β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€   Surrounding = -1 (subtract blur)          β”‚
β”‚   β”‚  0 β”‚ -1 β”‚  0 β”‚   Sum = 1 (preserve brightness)             β”‚
β”‚   β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”˜                                             β”‚
β”‚                                                                 β”‚
β”‚   Strong sharpening kernel:                                     β”‚
β”‚   β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”                                             β”‚
β”‚   β”‚ -1 β”‚ -1 β”‚ -1 β”‚                                             β”‚
β”‚   β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€                                             β”‚
β”‚   β”‚ -1 β”‚  9 β”‚ -1 β”‚   Center = 9                                β”‚
β”‚   β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€   Surrounding = -1 Γ— 8 = -8                β”‚
β”‚   β”‚ -1 β”‚ -1 β”‚ -1 β”‚   Sum = 1                                   β”‚
β”‚   β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”˜                                             β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Sharpening Implementation

import cv2
import numpy as np

img = cv2.imread('image.jpg')

# Method 1: Using kernel
kernel_sharpen = np.array([
    [0, -1, 0],
    [-1, 5, -1],
    [0, -1, 0]
])
sharpened1 = cv2.filter2D(img, -1, kernel_sharpen)

# Method 2: Strong sharpening kernel
kernel_sharpen_strong = np.array([
    [-1, -1, -1],
    [-1,  9, -1],
    [-1, -1, -1]
])
sharpened2 = cv2.filter2D(img, -1, kernel_sharpen_strong)

# Method 3: Unsharp Masking
def unsharp_mask(img, kernel_size=(5, 5), sigma=1.0, amount=1.0, threshold=0):
    """
    Sharpening with unsharp masking

    amount: Sharpening strength (1.0 = standard)
    threshold: Edge detection threshold (noise prevention)
    """
    # Blurred image
    blurred = cv2.GaussianBlur(img, kernel_size, sigma)

    # Original - Blur = Edges/Details
    # sharpened = Original + amount Γ— (Original - Blur)
    sharpened = cv2.addWeighted(img, 1 + amount, blurred, -amount, 0)

    if threshold > 0:
        # Keep original for pixels with change below threshold
        diff = cv2.absdiff(img, blurred)
        mask = (diff < threshold).astype(np.uint8) * 255
        sharpened = np.where(mask == 255, img, sharpened)

    return sharpened


sharpened3 = unsharp_mask(img, amount=1.5)

Adaptive Sharpening

import cv2
import numpy as np

def adaptive_sharpening(img, amount=1.0):
    """
    Adaptive sharpening - apply sharpening only to edge regions
    """
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # Edge detection
    edges = cv2.Canny(gray, 50, 150)
    edges = cv2.dilate(edges, np.ones((3, 3), np.uint8), iterations=1)

    # Blur
    blurred = cv2.GaussianBlur(img, (5, 5), 1)

    # Sharpening
    sharpened = cv2.addWeighted(img, 1 + amount, blurred, -amount, 0)

    # Apply sharpening only to edge regions
    edges_3ch = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR) / 255.0
    result = (sharpened * edges_3ch + img * (1 - edges_3ch)).astype(np.uint8)

    return result


img = cv2.imread('image.jpg')
result = adaptive_sharpening(img, amount=2.0)

8. Practice Problems

Exercise 1: Noise Removal Comparison

Generate Gaussian noise and salt-and-pepper noise separately, and compare the removal effects with three blur filters (average, Gaussian, median). Perform quantitative comparison using PSNR values.

# Hint: Add Gaussian noise
def add_gaussian_noise(img, mean=0, var=100):
    noise = np.random.normal(mean, var**0.5, img.shape)
    noisy = np.clip(img + noise, 0, 255).astype(np.uint8)
    return noisy

Exercise 2: Real-Time Blur Intensity Control

Write a program that can adjust blur intensity (kernel size) with a trackbar on webcam video. Allow selection between Gaussian blur and bilateral filter.

Exercise 3: Custom Emboss Directions

Design and test kernels that produce different emboss effects in 8 directions (up, down, left, right, and 4 diagonals).

Exercise 4: Advanced Sharpening

Implement an advanced sharpening function with the following features: 1. Sharpening strength control (amount) 2. Blur radius control (radius) 3. Threshold application - ignore small changes 4. Separate handling for highlights/shadows

Exercise 5: Miniature Effect (Tilt Shift)

Implement tilt-shift miniature effect using Gaussian blur and masks. Keep the center of the image sharp, with progressively more blur at top and bottom.

# Hint
def tilt_shift(img, focus_y, focus_height, blur_amount):
    # Create gradient mask
    # Blend blurred and original images using mask
    pass

9. Next Steps

In 06_Morphology.md, you'll learn morphological operations such as erosion, dilation, opening/closing!

Next topics: - Structuring Element - Erosion and Dilation - Opening and Closing - Noise removal and object separation


10. References

Official Documentation

Folder Related Content
04_Geometric_Transforms.md Image preprocessing
08_Edge_Detection.md Edge detection after filtering

Additional References

to navigate between lessons