Denoising

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 Any

1 Introduction

In 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.

2 Python Implementation

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.

Code
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 noisy

The median_filter function implements the denoising technique by applying a median operation over a locally sliding window. This function is crucial for preserving edges.

Code
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 output

3 Interactive Dashboard

The dashboard below provides allows for experimentation with noise levels and filter window sizes on uploaded or default images.

Components include:

  • Image Upload and Processing: You can add custom images or use a default one.
  • Adjustable Noise Ratio & Filter Size: Controls to vary noise and window parameters.
  • Visual Comparison: Side-by-side comparison of the noisy and denoised outputs.
  • Processing Insights: JSON output details the parameters and steps applied.
Code
DEFAULT_IMAGE = img_as_float(data.camera())
Code
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)
Code
demo.launch(pwa=True, show_api=False, show_error=True)
Code
# Output of this cell set dynamically in Quarto filter step
import micropip await micropip.install('plotly==5.24.1'); import numpy as np import plotly.express as px import gradio as gr from skimage import color, data, img_as_float from typing import Any 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 noisy 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 output DEFAULT_IMAGE = img_as_float(data.camera()) 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) demo.launch(pwa=True, show_api=False, show_error=True)