Edge Detection

Code
import numpy as np
import gradio as gr
from skimage import color, img_as_float, data
import plotly.graph_objects as go

1 Introduction

This notebook provides detailed implementations of key image processing techniques, focusing on enhancing and extracting image features through:

Prewitt and Sobel Edge Detection

These operators are essential tools for edge detection through gradient approximation. Edge detection aims to identify boundaries within an image by applying filters in the horizontal and vertical directions.

The convolutional operation is utilized as follows:

\[ (f * h)(n) = \sum_{k \in \mathbb{Z}^2} f(k)h(n-k), \]

where the gradient magnitude highlights edges:

\[ E = \sqrt{(f_x)^2 + (f_y)^2}. \]

Histogram Equalization

Enhances image contrast by reallocating pixel intensity distributions. For a pixel intensity distribution with levels \(r_k\) and probabilities:

\[ p_k = \frac{N_k}{N}, \]

the new intensity mapping utilizes the cumulative distribution:

\[ F(i,j) = \sum_{l=1}^{k} p_l \quad \text{for } (i,j)\in \Omega_k. \]

2 Python Implementation

To understand the image’s features, the convolve2d function helps perform a 2D convolution, an essential step for applying filtering operations to images.

Code
def convolve2d(image: np.ndarray, kernel: np.ndarray) -> np.ndarray:
    """
     Perform a 2D convolution between an image and a kernel.

     The kernel is flipped to implement convolution as defined by
     $$
     (f * h)(n) = \sum_{k \in \mathbb{Z}^2} f(k)h(n-k).
    $$

     Parameters:
         image (np.ndarray): 2D input image.
         kernel (np.ndarray): 2D filter kernel.

     Returns:
         np.ndarray: The convolved image with the same shape as input.
    """
    h, w = image.shape
    kh, kw = kernel.shape
    pad_h, pad_w = kh // 2, kw // 2
    padded = np.pad(image, ((pad_h, pad_h), (pad_w, pad_w)), mode="edge")
    output = np.zeros_like(image, dtype=float)
    # Flip the kernel for convolution
    kernel_flip = np.flipud(np.fliplr(kernel))
    for i in range(h):
        for j in range(w):
            region = padded[i : i + kh, j : j + kw]
            output[i, j] = np.sum(region * kernel_flip)
    return output

The Gaussian kernel and smoothing functions allow us to apply a Gaussian filter, smoothing the image to reduce noise before edge detection.

Code
def gaussian_kernel(size: int, sigma: float) -> np.ndarray:
    """
    Generate a 2D Gaussian kernel.

    Parameters:
        size (int): The size of the kernel (should be odd).
        sigma (float): Standard deviation of the Gaussian.

    Returns:
        np.ndarray: Normalized Gaussian kernel.
    """
    ax = np.arange(-size // 2 + 1, size // 2 + 1)
    xx, yy = np.meshgrid(ax, ax)
    kernel = np.exp(-(xx**2 + yy**2) / (2 * sigma**2))
    kernel = kernel / np.sum(kernel)
    return kernel


def smooth_image(image: np.ndarray, kernel_size: int, sigma: float) -> np.ndarray:
    """
    Smooth an image using a Gaussian filter.

    Parameters:
        image (np.ndarray): 2D input image.
        kernel_size (int): Size of the Gaussian kernel (odd integer).
        sigma (float): Standard deviation for the Gaussian.

    Returns:
        np.ndarray: Smoothed image.
    """
    kernel = gaussian_kernel(kernel_size, sigma)
    return convolve2d(image, kernel)

Thresholding aids in converting edge detected images to binary, accentuating the detected edges.

Code
def threshold_image(image: np.ndarray, threshold: float) -> np.ndarray:
    """
    Threshold an image to create a binary image.

    Parameters:
        image (np.ndarray): Input image.
        threshold (float): Threshold value.

    Returns:
        np.ndarray: Binary image (values 0 or 1).
    """
    return (image >= threshold).astype(float)

We define Prewitt and Sobel edge detection filters, both acting on pixel intensity gradients.

Code
prewitt_x = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]], dtype=float)
prewitt_y = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=float)

sobel_x = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=float)
sobel_y = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=float)

We utilize the filters for edge detection by combining the above-defined functions in prewitt_edge_detection and sobel_edge_detection.

Code
def prewitt_edge_detection(image: np.ndarray) -> np.ndarray:
    """
    Apply the Prewitt edge detection method.

    Parameters:
        image (np.ndarray): 2D grayscale image.

    Returns:
        np.ndarray: Edge magnitude image.
    """
    gx = convolve2d(image, prewitt_x)
    gy = convolve2d(image, prewitt_y)
    return np.sqrt(gx**2 + gy**2)


def sobel_edge_detection(image: np.ndarray) -> np.ndarray:
    """
    Apply the Sobel edge detection method.

    Parameters:
        image (np.ndarray): 2D grayscale image.

    Returns:
        np.ndarray: Edge magnitude image.
    """
    gx = convolve2d(image, sobel_x)
    gy = convolve2d(image, sobel_y)
    return np.sqrt(gx**2 + gy**2)

We also implement histogram equalization to enhance contrast in images.

Code
def histogram_equalization(image: np.ndarray) -> np.ndarray:
    """
    Perform histogram equalization to enhance image contrast.

    Parameters:
        image (np.ndarray): 2D grayscale image with values in [0, 1].

    Returns:
        np.ndarray: Contrast-enhanced image.
    """
    # Compute histogram and cumulative distribution function (CDF)
    hist, bins = np.histogram(image.flatten(), bins=256, range=[0, 1])
    cdf = hist.cumsum()
    cdf_normalized = cdf / cdf[-1]
    # Use linear interpolation of the CDF to map the original gray levels
    equalized = np.interp(image.flatten(), bins[:-1], cdf_normalized).reshape(
        image.shape
    )
    return equalized

3 Interactive Dashboard

Code
DEFAULT_IMAGE = img_as_float(data.camera())
Code
def compute_histogram(image: np.ndarray, bins: int = 256) -> dict:
    """
    Compute the histogram of an image.

    Parameters:
        image (np.ndarray): Input image
        bins (int): Number of histogram bins

    Returns:
        dict: Dictionary containing histogram data
    """
    hist, bin_edges = np.histogram(image, bins=bins, range=(0, 1))
    bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2

    return {
        "counts": hist.tolist(),
        "bin_edges": bin_edges.tolist(),
        "bin_centers": bin_centers.tolist(),
    }


def create_histogram_plot(hist_data: dict) -> go.Figure:
    """
    Create a Plotly histogram figure from the histogram data.

    Parameters:
        hist_data (dict): Dictionary containing histogram data

    Returns:
        go.Figure: Plotly figure object
    """
    fig = go.Figure(
        data=[
            go.Bar(
                x=hist_data["bin_centers"],
                y=hist_data["counts"],
                name="Pixel Intensity Distribution",
                marker_color="rgb(55, 83, 109)",
            )
        ]
    )

    fig.update_layout(
        title="Image Histogram",
        xaxis_title="Pixel Intensity",
        yaxis_title="Frequency",
        bargap=0.01,
        template="plotly_white",
        showlegend=False,
    )

    return fig


def process_image(
    image: np.ndarray,
    method: str,
    threshold: float = 0.1,
    kernel_size: int = 3,
    sigma: float = 1.0,
) -> tuple[np.ndarray, go.Figure]:
    """
    Process the input image based on the selected method.

    Parameters:
        image (np.ndarray): Input image (RGB or grayscale). Expected values in [0,1].
        method (str): Processing method.
        threshold (float): Threshold value for binary edge detection.
        kernel_size (int): Kernel size for Gaussian smoothing (odd integer).
        sigma (float): Standard deviation for Gaussian smoothing.

    Returns:
        tuple[np.ndarray, go.Figure]: A tuple containing the processed image and the Plotly figure
    """
    # Convert to float image in [0,1]
    if image.ndim == 3:
        image_gray = color.rgb2gray(image)
    else:
        image_gray = image.copy()

    # Process according to the selected method
    if method == "Prewitt Edge Detection":
        proc_img = prewitt_edge_detection(image_gray)
    elif method == "Sobel Edge Detection":
        proc_img = sobel_edge_detection(image_gray)
    elif method == "Edge Thresholding (Sobel + Threshold)":
        edges = sobel_edge_detection(image_gray)
        proc_img = threshold_image(edges, threshold)
    elif method == "Histogram Equalization":
        proc_img = histogram_equalization(image_gray)
    elif method == "Gaussian Smoothing":
        proc_img = smooth_image(image_gray, kernel_size, sigma)
    else:
        proc_img = image_gray  # Fallback: return original image

    # Normalize the processed image to [0, 1] for display (if not already binary)
    if proc_img.max() > 1 or proc_img.min() < 0:
        proc_img = (proc_img - proc_img.min()) / (
            proc_img.max() - proc_img.min() + 1e-8
        )

    hist_data = compute_histogram(proc_img)
    plot_fig = create_histogram_plot(hist_data)
    return proc_img, plot_fig


with gr.Blocks(
    css="""gradio-app {background: #222222 !important}""",
    title="Edge Detection & Histogram Equalization",
) as demo:
    with gr.Row():
        image_input = gr.Image(label="Input Image", type="numpy", value=DEFAULT_IMAGE)
        output_image = gr.Image(label="Processed Image")

    process_button = gr.Button("Process Image")

    method_choice = gr.Dropdown(
        choices=[
            "Prewitt Edge Detection",
            "Sobel Edge Detection",
            "Edge Thresholding (Sobel + Threshold)",
            "Histogram Equalization",
            "Gaussian Smoothing",
        ],
        label="Processing Method",
        value="Prewitt Edge Detection",
    )
    threshold_slider = gr.Slider(
        minimum=0.0,
        maximum=1.0,
        step=0.01,
        label="Threshold (for Edge Thresholding)",
        value=0.1,
    )
    kernel_slider = gr.Slider(
        minimum=3, maximum=15, step=2, label="Gaussian Kernel Size", value=3
    )
    sigma_slider = gr.Slider(
        minimum=0.1, maximum=5.0, step=0.1, label="Gaussian Sigma", value=1.0
    )
    output_histogram = gr.Plot(label="Histogram")

    kwargs = dict(
        fn=process_image,
        inputs=[
            image_input,
            method_choice,
            threshold_slider,
            kernel_slider,
            sigma_slider,
        ],
        outputs=[output_image, output_histogram],
    )
    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 gradio as gr from skimage import color, img_as_float, data import plotly.graph_objects as go def convolve2d(image: np.ndarray, kernel: np.ndarray) -> np.ndarray: """ Perform a 2D convolution between an image and a kernel. The kernel is flipped to implement convolution as defined by $$ (f * h)(n) = \sum_{k \in \mathbb{Z}^2} f(k)h(n-k). $$ Parameters: image (np.ndarray): 2D input image. kernel (np.ndarray): 2D filter kernel. Returns: np.ndarray: The convolved image with the same shape as input. """ h, w = image.shape kh, kw = kernel.shape pad_h, pad_w = kh // 2, kw // 2 padded = np.pad(image, ((pad_h, pad_h), (pad_w, pad_w)), mode="edge") output = np.zeros_like(image, dtype=float) # Flip the kernel for convolution kernel_flip = np.flipud(np.fliplr(kernel)) for i in range(h): for j in range(w): region = padded[i : i + kh, j : j + kw] output[i, j] = np.sum(region * kernel_flip) return output def gaussian_kernel(size: int, sigma: float) -> np.ndarray: """ Generate a 2D Gaussian kernel. Parameters: size (int): The size of the kernel (should be odd). sigma (float): Standard deviation of the Gaussian. Returns: np.ndarray: Normalized Gaussian kernel. """ ax = np.arange(-size // 2 + 1, size // 2 + 1) xx, yy = np.meshgrid(ax, ax) kernel = np.exp(-(xx**2 + yy**2) / (2 * sigma**2)) kernel = kernel / np.sum(kernel) return kernel def smooth_image(image: np.ndarray, kernel_size: int, sigma: float) -> np.ndarray: """ Smooth an image using a Gaussian filter. Parameters: image (np.ndarray): 2D input image. kernel_size (int): Size of the Gaussian kernel (odd integer). sigma (float): Standard deviation for the Gaussian. Returns: np.ndarray: Smoothed image. """ kernel = gaussian_kernel(kernel_size, sigma) return convolve2d(image, kernel) def threshold_image(image: np.ndarray, threshold: float) -> np.ndarray: """ Threshold an image to create a binary image. Parameters: image (np.ndarray): Input image. threshold (float): Threshold value. Returns: np.ndarray: Binary image (values 0 or 1). """ return (image >= threshold).astype(float) prewitt_x = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]], dtype=float) prewitt_y = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=float) sobel_x = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=float) sobel_y = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=float) def prewitt_edge_detection(image: np.ndarray) -> np.ndarray: """ Apply the Prewitt edge detection method. Parameters: image (np.ndarray): 2D grayscale image. Returns: np.ndarray: Edge magnitude image. """ gx = convolve2d(image, prewitt_x) gy = convolve2d(image, prewitt_y) return np.sqrt(gx**2 + gy**2) def sobel_edge_detection(image: np.ndarray) -> np.ndarray: """ Apply the Sobel edge detection method. Parameters: image (np.ndarray): 2D grayscale image. Returns: np.ndarray: Edge magnitude image. """ gx = convolve2d(image, sobel_x) gy = convolve2d(image, sobel_y) return np.sqrt(gx**2 + gy**2) def histogram_equalization(image: np.ndarray) -> np.ndarray: """ Perform histogram equalization to enhance image contrast. Parameters: image (np.ndarray): 2D grayscale image with values in [0, 1]. Returns: np.ndarray: Contrast-enhanced image. """ # Compute histogram and cumulative distribution function (CDF) hist, bins = np.histogram(image.flatten(), bins=256, range=[0, 1]) cdf = hist.cumsum() cdf_normalized = cdf / cdf[-1] # Use linear interpolation of the CDF to map the original gray levels equalized = np.interp(image.flatten(), bins[:-1], cdf_normalized).reshape( image.shape ) return equalized DEFAULT_IMAGE = img_as_float(data.camera()) def compute_histogram(image: np.ndarray, bins: int = 256) -> dict: """ Compute the histogram of an image. Parameters: image (np.ndarray): Input image bins (int): Number of histogram bins Returns: dict: Dictionary containing histogram data """ hist, bin_edges = np.histogram(image, bins=bins, range=(0, 1)) bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 return { "counts": hist.tolist(), "bin_edges": bin_edges.tolist(), "bin_centers": bin_centers.tolist(), } def create_histogram_plot(hist_data: dict) -> go.Figure: """ Create a Plotly histogram figure from the histogram data. Parameters: hist_data (dict): Dictionary containing histogram data Returns: go.Figure: Plotly figure object """ fig = go.Figure( data=[ go.Bar( x=hist_data["bin_centers"], y=hist_data["counts"], name="Pixel Intensity Distribution", marker_color="rgb(55, 83, 109)", ) ] ) fig.update_layout( title="Image Histogram", xaxis_title="Pixel Intensity", yaxis_title="Frequency", bargap=0.01, template="plotly_white", showlegend=False, ) return fig def process_image( image: np.ndarray, method: str, threshold: float = 0.1, kernel_size: int = 3, sigma: float = 1.0, ) -> tuple[np.ndarray, go.Figure]: """ Process the input image based on the selected method. Parameters: image (np.ndarray): Input image (RGB or grayscale). Expected values in [0,1]. method (str): Processing method. threshold (float): Threshold value for binary edge detection. kernel_size (int): Kernel size for Gaussian smoothing (odd integer). sigma (float): Standard deviation for Gaussian smoothing. Returns: tuple[np.ndarray, go.Figure]: A tuple containing the processed image and the Plotly figure """ # Convert to float image in [0,1] if image.ndim == 3: image_gray = color.rgb2gray(image) else: image_gray = image.copy() # Process according to the selected method if method == "Prewitt Edge Detection": proc_img = prewitt_edge_detection(image_gray) elif method == "Sobel Edge Detection": proc_img = sobel_edge_detection(image_gray) elif method == "Edge Thresholding (Sobel + Threshold)": edges = sobel_edge_detection(image_gray) proc_img = threshold_image(edges, threshold) elif method == "Histogram Equalization": proc_img = histogram_equalization(image_gray) elif method == "Gaussian Smoothing": proc_img = smooth_image(image_gray, kernel_size, sigma) else: proc_img = image_gray # Fallback: return original image # Normalize the processed image to [0, 1] for display (if not already binary) if proc_img.max() > 1 or proc_img.min() < 0: proc_img = (proc_img - proc_img.min()) / ( proc_img.max() - proc_img.min() + 1e-8 ) hist_data = compute_histogram(proc_img) plot_fig = create_histogram_plot(hist_data) return proc_img, plot_fig with gr.Blocks( css="""gradio-app {background: #222222 !important}""", title="Edge Detection & Histogram Equalization", ) as demo: with gr.Row(): image_input = gr.Image(label="Input Image", type="numpy", value=DEFAULT_IMAGE) output_image = gr.Image(label="Processed Image") process_button = gr.Button("Process Image") method_choice = gr.Dropdown( choices=[ "Prewitt Edge Detection", "Sobel Edge Detection", "Edge Thresholding (Sobel + Threshold)", "Histogram Equalization", "Gaussian Smoothing", ], label="Processing Method", value="Prewitt Edge Detection", ) threshold_slider = gr.Slider( minimum=0.0, maximum=1.0, step=0.01, label="Threshold (for Edge Thresholding)", value=0.1, ) kernel_slider = gr.Slider( minimum=3, maximum=15, step=2, label="Gaussian Kernel Size", value=3 ) sigma_slider = gr.Slider( minimum=0.1, maximum=5.0, step=0.1, label="Gaussian Sigma", value=1.0 ) output_histogram = gr.Plot(label="Histogram") kwargs = dict( fn=process_image, inputs=[ image_input, method_choice, threshold_slider, kernel_slider, sigma_slider, ], outputs=[output_image, output_histogram], ) process_button.click(**kwargs) demo.load(**kwargs) demo.launch(pwa=True, show_api=False, show_error=True)