在數位影像的世界裡,影像雜訊是無可避免的問題。無論是來自於光線不足、感光元件的熱雜訊,或是傳輸過程中的失真,雜訊都會降低影像品質。本篇將介紹常見的影像雜訊類型,以及如何使用濾波(filtering)技術來去除或平滑這些雜訊,從而提升影像的品質。
影像雜訊是指在影像中出現的不期望的、隨機的像素值變化。根據其產生原因和特性,雜訊可以有多種形式,這邊僅介紹圖像處理中常見的兩種。
椒鹽雜訊,顧名思義,就像在圖片上撒了一把白色(鹽)和黑色(胡椒)的顆粒。它表現為影像中隨機出現的純白或純黑的像素點。成因通常是由於影像感測器中的壞點、類比數位轉換過程中的錯誤,或數據傳輸錯誤所導致。
高斯雜訊是另一種非常普遍的雜訊,它的強度變化遵循常態分佈。這意味著每個像素的強度都在其原始值的基礎上,隨機增加或減少了一個符合常態分佈的微小量。成因主要是感光元件在低光照或高溫環境下產生的電子雜訊。
要對抗雜訊,最主要的方法就是「濾波」。濾波的本質,是透過一個稱為濾波核 (kernel) 或濾波器的小型矩陣,在影像上滑動,並根據核內的權重和鄰近像素的值,來重新計算中心像素的新值。這個過程可以平滑影像、去除雜訊,甚至增強邊緣。
在介紹主流濾波方法前,我們先快速認識一下型態學處理。這是一系列基於「形狀」的非線性操作,常用於二值化影像。其中最基礎的是侵蝕 (erosion) 和膨脹 (dilation)。
侵蝕:會「侵蝕」掉物體(通常是白色部分)的邊界。其效果是讓物體變小,可以有效去除小的白色噪點(鹽點)。
膨脹:會「擴張」物體的邊界,讓物體變大。可以用來填補物體內部的小黑洞(胡椒點)。
卷積 (convolution) 是線性濾波的核心數學運算。可以想像我們拿著一個小的「濾鏡」,覆蓋在影像的某個區域上,然後將濾鏡上的每個數值與其對應的影像像素值相乘,最後將所有乘積加總,得到的值就是這個區域中心點的新像素值。
盒式濾波是最簡單的線性濾波器,也稱為均值濾波 (averaging filter)。它的濾波核中所有元素的值都相等(通常是1),最後再除以元素的總數。
例如,一個 3x3 的盒式濾波核就是:
它的效果就是將中心像素的值,替換為其鄰近區域內所有像素的平均值。這會讓影像變得平滑模糊,但效果比較生硬,容易產生一些不自然的邊緣。
高斯濾波是一種更精緻的線性濾波。它的濾波核權重不是平均分配的,而是根據高斯分佈來生成。這意味著,距離中心點越近的像素,會被賦予越高的權重。
高斯濾波主要的參數是標準差 sigma,當 sigma 越大,模糊效果越強。
相對於盒式濾波,高斯濾波處理結果更自然,也常用於處理高斯雜訊。
與前面兩者不同,中值濾波是一種非線性濾波。它的運作方式非常直觀:
用一個滑動窗口(濾波核)掃過影像。
讀取窗口內所有的像素值。
對這些像素值進行排序。
用排序後的中位數 (median) 來取代中心像素的值。
因為是取中位數,中值濾波不太受椒鹽雜訊這種離群值影響,因此很適合用於這方面的處理。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# --- Step 0: 定義圖片顯示函式 ---
def show_images(images, titles):
plt.figure(figsize=(15, 10))
for i, (image, title) in enumerate(zip(images, titles)):
plt.subplot(2, 3, i + 1)
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.title(title)
plt.axis('off')
plt.tight_layout()
plt.show()
# --- Step 1: 讀取圖片並定義雜訊函式 ---
# 為了方便,我們直接使用灰階影像來觀察
img = np.fromfunction(lambda y, x: x + y, (256, 256), dtype=np.uint8)
def add_salt_and_pepper_noise(image, amount=0.05):
""" 為圖片添加椒鹽雜訊 """
noisy_img = image.copy()
num_salt = np.ceil(amount * image.size * 0.5)
coords = [np.random.randint(0, i - 1, int(num_salt)) for i in image.shape]
noisy_img[coords[0], coords[1]] = 255
num_pepper = np.ceil(amount * image.size * 0.5)
coords = [np.random.randint(0, i - 1, int(num_pepper)) for i in image.shape]
noisy_img[coords[0], coords[1]] = 0
return noisy_img
def add_gaussian_noise(image, mean=0, sigma=25):
""" 為圖片添加高斯雜訊 """
row, col = image.shape
gauss = np.random.normal(mean, sigma, (row, col))
noisy_img = image + gauss
# 將像素值限制在 0-255 範圍內
noisy_img = np.clip(noisy_img, 0, 255)
return noisy_img.astype(np.uint8)
# --- Step 2: 產生帶有雜訊的影像 ---
sp_noise_img = add_salt_and_pepper_noise(img)
gaussian_noise_img = add_gaussian_noise(img)
# 將灰階轉回 BGR 以便顯示
original_bgr = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
sp_noise_bgr = cv2.cvtColor(sp_noise_img, cv2.COLOR_GRAY2BGR)
gaussian_noise_bgr = cv2.cvtColor(gaussian_noise_img, cv2.COLOR_GRAY2BGR)
# 顯示原始影像和兩種雜訊影像
print("--- 原始影像與兩種雜訊 ---")
show_images([original_bgr, sp_noise_bgr, gaussian_noise_bgr],
['Original', 'Salt & Pepper Noise', 'Gaussian Noise'])
# --- Step 3: 應用濾波器 ---
# 使用 5x5 的濾波核
kernel_size = 5
# 對椒鹽雜訊影像進行濾波
sp_gaussian_filtered = cv2.GaussianBlur(sp_noise_img, (kernel_size, kernel_size), 0)
sp_median_filtered = cv2.medianBlur(sp_noise_img, kernel_size)
# 對高斯雜訊影像進行濾波
gs_gaussian_filtered = cv2.GaussianBlur(gaussian_noise_img, (kernel_size, kernel_size), 0)
gs_median_filtered = cv2.medianBlur(gaussian_noise_img, kernel_size)
# --- Step 4: 顯示與比較結果 ---
print("\n--- 處理椒鹽雜訊 ---")
show_images([sp_noise_bgr, cv2.cvtColor(sp_gaussian_filtered, cv2.COLOR_GRAY2BGR), cv2.cvtColor(sp_median_filtered, cv2.COLOR_GRAY2BGR)],
['Salt & Pepper Noise', 'Gaussian Filtered', 'Median Filtered'])
print("\n--- 處理高斯雜訊 ---")
show_images([gaussian_noise_bgr, cv2.cvtColor(gs_gaussian_filtered, cv2.COLOR_GRAY2BGR), cv2.cvtColor(gs_median_filtered, cv2.COLOR_GRAY2BGR)],
['Gaussian Noise', 'Gaussian Filtered', 'Median Filtered'])