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, dataIn 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. \]
Below is the implementation of histogram equalization, which enhances the contrast by redistributing pixel intensity values:
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_imgThe following function creates a responsive Plotly histogram to visualize intensity distributions of both the original and enhanced images:
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 figThe Gradio dashboard below allows interaction with images for contrast enhancement through histogram equalization. It includes:
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)