Contrast Enhancement

Code
import numpy as np
from PIL import Image
import plotly.graph_objs as go
import gradio as gr
from skimage import img_as_float, data

1 Introduction

In this notebook, we explore the technique of contrast enhancement using histogram equalization. This method is particularly effective for improving the visual quality of images with low contrast, where the histogram is uneven and limited to a narrow range.

The core idea of the technique is to redistribute the intensity values to achieve a more uniform histogram, thereby enhancing contrast. This transformation seeks to use the full range of pixel values more effectively.

Setup

If \(X : [0, a] \to \mathbb{R}_+\) is a random variable with a continuous, positive density \(g\), then the transformed variable \(Y = \int_{0}^{X} g(t) \, dt\) will be uniformly distributed in \([0,1]\). For images, we employ a discrete version of this method.

Histogram Equalization Formula

Given an image with a histogram \(h(k)\) for \(k = 0, \dots, 255\) and total pixel count \(N\). The enhanced intensity at pixel \((x,y)\) is expressed as:

\[ I_{\text{enhanced}}(x,y) = \left\lfloor 255 \cdot \sum_{k=0}^{I(x,y)} \frac{h(k)}{N} \right\rfloor. \]

2 Python Implementation

Below is the implementation of histogram equalization, which enhances the contrast by redistributing pixel intensity values:

Code
def contrast_enhancer(img: np.ndarray) -> np.ndarray:
    """
    Enhance the contrast of a grayscale image using histogram equalization.

    The function computes the normalized cumulative histogram (CDF) and maps each pixel
    to its new intensity value based on the CDF.

    Args:
        img (np.ndarray): Input grayscale image (uint8, values in 0-255).

    Returns:
        np.ndarray: Contrast-enhanced image (uint8).
    """
    # Compute the histogram: count of each pixel intensity from 0 to 255
    hist = np.bincount(img.ravel(), minlength=256)

    # Normalize the histogram to obtain the probability distribution
    prob = hist / img.size

    # Compute the cumulative distribution function (CDF)
    cdf = prob.cumsum()

    # Map original intensities through the CDF and scale to [0,255]
    equalized_img = np.round(cdf[img] * 255).astype(np.uint8)

    return equalized_img

The following function creates a responsive Plotly histogram to visualize intensity distributions of both the original and enhanced images:

Code
def plot_histogram(img: np.ndarray, title: str = "Histogram") -> go.Figure:
    """
    Generate a responsive Plotly bar chart of the image's histogram.

    Args:
        img (np.ndarray): Grayscale image.
        title (str): Title of the plot.

    Returns:
        go.Figure: Plotly figure object.
    """
    # Compute histogram: count of each intensity value
    hist = np.bincount(img.ravel(), minlength=256)
    x = list(range(256))

    # Create a responsive Plotly bar chart
    fig = go.Figure(data=[go.Bar(x=x, y=hist, marker_color="steelblue")])
    fig.update_layout(
        title=title,
        xaxis=dict(title="Pixel Intensity", tickmode="linear", dtick=10),
        yaxis=dict(title="Count"),
        margin=dict(l=40, r=40, t=40, b=40),
        template="plotly_white",
        autosize=True,
    )
    return fig

3 Interactive Dashboard

The Gradio dashboard below allows interaction with images for contrast enhancement through histogram equalization. It includes:

  • Original and Enhanced Image Viewer: Enables direct comparison.
  • Plotly-based Dynamic Histograms: Displays intensity distribution before and after enhancement.
  • JSON Statistics: Provides quantitative insights into histogram changes.
Code
DEFAULT_IMAGE = img_as_float(data.camera())
Code
def process_image(
    image: Image.Image,
) -> tuple[Image.Image, Image.Image, go.Figure, go.Figure, dict]:
    """
    Process the input image by converting it to grayscale, enhancing its contrast,
    and generating responsive histograms (using Plotly) for both the original and enhanced images.

    Args:
        image (Image.Image): Input image (in any mode; will be converted to grayscale).

    Returns:
        tuple:
            - Original Image (PIL.Image): The grayscale version of the input image.
            - Enhanced Image (PIL.Image): The contrast-enhanced image.
            - Original Histogram (go.Figure): Responsive Plotly figure of the original histogram.
            - Equalized Histogram (go.Figure): Responsive Plotly figure of the equalized histogram.
            - Histogram Data (dict): Detailed histogram statistics.
    """
    # Convert to grayscale
    img_gray = image.convert("L")
    img_array = np.array(img_gray)

    # Generate histogram for the original image using Plotly
    fig_original = plot_histogram(img_array, title="Original Histogram")

    # Enhance contrast using histogram equalization
    enhanced_array = contrast_enhancer(img_array)
    enhanced_image = Image.fromarray(enhanced_array)

    # Generate histogram for the enhanced image using Plotly
    fig_enhanced = plot_histogram(enhanced_array, title="Equalized Histogram")

    # Prepare detailed histogram data (JSON-friendly)
    original_hist = np.bincount(img_array.ravel(), minlength=256).tolist()
    enhanced_hist = np.bincount(enhanced_array.ravel(), minlength=256).tolist()
    histogram_data = {
        "original_histogram": original_hist,
        "enhanced_histogram": enhanced_hist,
        "original_stats": {
            "min": int(np.min(img_array)),
            "max": int(np.max(img_array)),
            "mean": float(np.mean(img_array)),
        },
        "enhanced_stats": {
            "min": int(np.min(enhanced_array)),
            "max": int(np.max(enhanced_array)),
            "mean": float(np.mean(enhanced_array)),
        },
    }

    return (img_gray, enhanced_image, fig_original, fig_enhanced, histogram_data)


with gr.Blocks(css="""gradio-app {background: #222222 !important}""") as demo:
    gr.Markdown(
        """
        # Contrast Enhancement and Histogram Equalization
        Upload an image to perform contrast enhancement via histogram equalization.
        """
    )

    input_image = gr.Image(label="Input Image", type="pil", value=DEFAULT_IMAGE)
    submit_button = gr.Button("Enhance Contrast")

    with gr.Row():
        original_image = gr.Image(label="Original Image")
        enhanced_image = gr.Image(label="Enhanced Image")

    original_hist = gr.Plot(label="Original Histogram")
    equalized_hist = gr.Plot(label="Equalized Histogram")

    # JSON output placed in a narrow row to minimize dashboard height.
    hist_json = gr.JSON(label="Histogram Data", elem_classes="gr-json")

    kwargs = dict(
        fn=process_image,
        inputs=input_image,
        outputs=[
            original_image,
            enhanced_image,
            original_hist,
            equalized_hist,
            hist_json,
        ],
    )
    submit_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 from PIL import Image import plotly.graph_objs as go import gradio as gr from skimage import img_as_float, data def contrast_enhancer(img: np.ndarray) -> np.ndarray: """ Enhance the contrast of a grayscale image using histogram equalization. The function computes the normalized cumulative histogram (CDF) and maps each pixel to its new intensity value based on the CDF. Args: img (np.ndarray): Input grayscale image (uint8, values in 0-255). Returns: np.ndarray: Contrast-enhanced image (uint8). """ # Compute the histogram: count of each pixel intensity from 0 to 255 hist = np.bincount(img.ravel(), minlength=256) # Normalize the histogram to obtain the probability distribution prob = hist / img.size # Compute the cumulative distribution function (CDF) cdf = prob.cumsum() # Map original intensities through the CDF and scale to [0,255] equalized_img = np.round(cdf[img] * 255).astype(np.uint8) return equalized_img def plot_histogram(img: np.ndarray, title: str = "Histogram") -> go.Figure: """ Generate a responsive Plotly bar chart of the image's histogram. Args: img (np.ndarray): Grayscale image. title (str): Title of the plot. Returns: go.Figure: Plotly figure object. """ # Compute histogram: count of each intensity value hist = np.bincount(img.ravel(), minlength=256) x = list(range(256)) # Create a responsive Plotly bar chart fig = go.Figure(data=[go.Bar(x=x, y=hist, marker_color="steelblue")]) fig.update_layout( title=title, xaxis=dict(title="Pixel Intensity", tickmode="linear", dtick=10), yaxis=dict(title="Count"), margin=dict(l=40, r=40, t=40, b=40), template="plotly_white", autosize=True, ) return fig DEFAULT_IMAGE = img_as_float(data.camera()) def process_image( image: Image.Image, ) -> tuple[Image.Image, Image.Image, go.Figure, go.Figure, dict]: """ Process the input image by converting it to grayscale, enhancing its contrast, and generating responsive histograms (using Plotly) for both the original and enhanced images. Args: image (Image.Image): Input image (in any mode; will be converted to grayscale). Returns: tuple: - Original Image (PIL.Image): The grayscale version of the input image. - Enhanced Image (PIL.Image): The contrast-enhanced image. - Original Histogram (go.Figure): Responsive Plotly figure of the original histogram. - Equalized Histogram (go.Figure): Responsive Plotly figure of the equalized histogram. - Histogram Data (dict): Detailed histogram statistics. """ # Convert to grayscale img_gray = image.convert("L") img_array = np.array(img_gray) # Generate histogram for the original image using Plotly fig_original = plot_histogram(img_array, title="Original Histogram") # Enhance contrast using histogram equalization enhanced_array = contrast_enhancer(img_array) enhanced_image = Image.fromarray(enhanced_array) # Generate histogram for the enhanced image using Plotly fig_enhanced = plot_histogram(enhanced_array, title="Equalized Histogram") # Prepare detailed histogram data (JSON-friendly) original_hist = np.bincount(img_array.ravel(), minlength=256).tolist() enhanced_hist = np.bincount(enhanced_array.ravel(), minlength=256).tolist() histogram_data = { "original_histogram": original_hist, "enhanced_histogram": enhanced_hist, "original_stats": { "min": int(np.min(img_array)), "max": int(np.max(img_array)), "mean": float(np.mean(img_array)), }, "enhanced_stats": { "min": int(np.min(enhanced_array)), "max": int(np.max(enhanced_array)), "mean": float(np.mean(enhanced_array)), }, } return (img_gray, enhanced_image, fig_original, fig_enhanced, histogram_data) with gr.Blocks(css="""gradio-app {background: #222222 !important}""") as demo: gr.Markdown( """ # Contrast Enhancement and Histogram Equalization Upload an image to perform contrast enhancement via histogram equalization. """ ) input_image = gr.Image(label="Input Image", type="pil", value=DEFAULT_IMAGE) submit_button = gr.Button("Enhance Contrast") with gr.Row(): original_image = gr.Image(label="Original Image") enhanced_image = gr.Image(label="Enhanced Image") original_hist = gr.Plot(label="Original Histogram") equalized_hist = gr.Plot(label="Equalized Histogram") # JSON output placed in a narrow row to minimize dashboard height. hist_json = gr.JSON(label="Histogram Data", elem_classes="gr-json") kwargs = dict( fn=process_image, inputs=input_image, outputs=[ original_image, enhanced_image, original_hist, equalized_hist, hist_json, ], ) submit_button.click(**kwargs) demo.load(**kwargs) demo.launch(pwa=True, show_api=False, show_error=True)