Code
import numpy as np
import plotly.express as px
import gradio as gr
from skimage import color, data, img_as_float
from typing import AnyIn this notebook, we explore the concept of Denoising with a focus on mitigating Salt & Pepper noise using a Median Filter.
Salt & Pepper Noise
This noise manifests as random pixel corruption, where a fraction \(\sigma\) of pixels in a grayscale image \(f\) are unexpectedly set to either 0 (“pepper”) or 1 (“salt”). Specifically, out of \(N\) total pixels, \(N \cdot \sigma\) will be affected. This noise drastically affects image quality without altering structural content.
Median Filtering
We employ a median filter over a sliding window of size \(s\). For each pixel location \((i,j)\), the filter replaces its value with the median from the surrounding pixel values:
\[ g(i,j) = \operatorname{median}\{f(k,l) \mid (k,l) \in W_{ij}\}. \]
This method is effective in noise reduction while preserving significant edges and details, due to the non-linear nature of the median statistic.
The following implementation illustrates how we inject salt & pepper noise into an image. The add_salt_and_pepper_noise function generates noise by random pixel corruption.
def add_salt_and_pepper_noise(image: np.ndarray, noise_ratio: float) -> np.ndarray:
"""
Adds salt & pepper noise to a grayscale image.
Parameters:
image (np.ndarray): 2D numpy array with values in [0,1].
noise_ratio (float): Fraction of pixels to be corrupted.
Returns:
np.ndarray: Noisy image.
"""
noisy = image.copy()
total_pixels = image.size
num_noisy = int(total_pixels * noise_ratio)
# Generate random indices for the pixels to corrupt
coords = np.unravel_index(
np.random.choice(total_pixels, num_noisy, replace=False), image.shape
)
# Randomly assign salt (1.0) or pepper (0.0)
noisy[coords] = np.random.choice([0.0, 1.0], size=num_noisy)
return noisyThe median_filter function implements the denoising technique by applying a median operation over a locally sliding window. This function is crucial for preserving edges.
def median_filter(image: np.ndarray, window_size: int) -> np.ndarray:
"""
Applies a median filter to a grayscale image.
Parameters:
image (np.ndarray): 2D numpy array with values in [0,1].
window_size (int): Size of the square window (must be odd).
Returns:
np.ndarray: Denoised image.
"""
if window_size % 2 == 0:
raise ValueError("Window size must be odd.")
pad_width = window_size // 2
padded = np.pad(image, pad_width, mode="edge")
output = np.zeros_like(image)
for i in range(output.shape[0]):
for j in range(output.shape[1]):
window = padded[i : i + window_size, j : j + window_size]
output[i, j] = np.median(window)
return outputThe dashboard below provides allows for experimentation with noise levels and filter window sizes on uploaded or default images.
Components include:
def process_image(
image: np.ndarray, noise_ratio: float, window_size: int
) -> dict[str, Any]:
"""
Process the uploaded image by adding salt & pepper noise and then denoising it.
Parameters:
image (np.ndarray): Uploaded image in numpy array format.
noise_ratio (float): Fraction of pixels to corrupt with noise.
window_size (int): Median filter window size (must be odd).
Returns:
dict: Dictionary containing Plotly figures for the noisy and denoised images,
along with JSON details of the process.
"""
# Convert image to float and grayscale if necessary
if image.ndim == 3:
image_gray = color.rgb2gray(image)
else:
image_gray = image.copy()
image_gray = img_as_float(image_gray)
# Ensure window size is odd
if window_size % 2 == 0:
window_size += 1
# Apply noise and denoising
noisy_img = add_salt_and_pepper_noise(image_gray, noise_ratio)
denoised_img = median_filter(noisy_img, window_size)
# Create Plotly figures
fig_noisy = px.imshow(noisy_img, color_continuous_scale="gray", title="Noisy Image")
fig_denoised = px.imshow(
denoised_img, color_continuous_scale="gray", title="Denoised Image"
)
details = {
"Noise Ratio": noise_ratio,
"Median Filter Window Size": window_size,
"Original Image Shape": image_gray.shape,
}
return {
"Noisy Image": fig_noisy,
"Denoised Image": fig_denoised,
"Details": details,
}
with gr.Blocks(
title="Image Denoising via Median Filtering",
css="""gradio-app {background: #222222 !important}""",
) as demo:
image_input = gr.Image(label="Upload Image", value=DEFAULT_IMAGE)
with gr.Row():
noise_ratio_slider = gr.Slider(
minimum=0.0, maximum=0.5, step=0.01, value=0.4, label="Noise Ratio"
)
window_size_slider = gr.Slider(
minimum=3,
maximum=21,
step=2,
value=3,
label="Median Filter Window Size (odd)",
)
process_button = gr.Button("Apply Denoising")
with gr.Row():
output_noisy = gr.Plot(label="Noisy Image")
output_denoised = gr.Plot(label="Denoised Image")
output_details = gr.JSON(label="Processing Details")
def process_wrapper(image, noise_ratio, window_size):
# If no image is uploaded, use a default image from skimage
if image is None:
from skimage import data
image = img_as_float(data.camera())
results = process_image(image, noise_ratio, int(window_size))
return results["Noisy Image"], results["Denoised Image"], results["Details"]
kwargs = dict(
fn=process_wrapper,
inputs=[image_input, noise_ratio_slider, window_size_slider],
outputs=[output_noisy, output_denoised, output_details],
)
process_button.click(**kwargs)
demo.load(**kwargs)