Image Histogram Equalization
Implement histogram equalization to enhance image contrast using NumPy.
Histogram equalization redistributes pixel intensities to achieve a more uniform histogram, improving contrast in images with concentrated intensity ranges.
Algorithm:
- Compute histogram h[i] = count of pixels with intensity i
- Compute cumulative distribution function: CDF[i]=βj=0iβh[j]
- Normalize CDF to range [0, 255]: CDFnormβ[i]=(MΓN)βCDFminβCDF[i]βCDFminββΓ255
- Map each pixel: output[x,y]=CDFnormβ[input[x,y]]
Where MΓN is the total number of pixels and CDFminβ is the minimum non-zero CDF value.
Example:
image = [[50, 50], [50, 50]]
[[255, 255], [255, 255]]
Step 1: Histogram Only intensity 50 appears, count = 4
Step 2: CDF CDF[50] = 4, all others = 0 or 4
Step 3: Normalize CDF_min = 4, total pixels = 4 CDF_norm[50] = (4-4)/(4-4) Γ 255 = undefined β use 255 when all same
Step 4: Map All pixels map to 255 (maximum) since they all have the same value.
When all pixels are identical, equalization maps them to maximum intensity.
Constraints:
- image: 2D numpy array with values in [0, 255]
- Return: Equalized image with same shape, values in [0, 255]
- Output values should be integers
More from CV: Introduction to Computer Vision
Histogram equalization is a global contrast enhancement technique that operates purely on pixel intensities, not spatial structure. You treat the image as a collection of gray levels (0β255 for 8-bit), look at how often each intensity occurs (the histogram), and then remap these intensities so that their cumulative distribution becomes approximately linear. This spreads out frequently occurring intensities over a wider range, making dark regions lighter and bright regions darker where needed, thereby increasing overall contrast.
Mathematically, you build a cumulative distribution function (CDF) from the histogram and then use this CDF as a mapping function from old intensities to new ones. To avoid mapping all low intensities to zero, you subtract the smallest non-zero CDF and normalize by the total number of pixels, scaling the result back to the range [0,255]. The key idea: pixels with similar original intensities get mapped to more diverse output intensities, flattening the histogram and revealing more detail in low-contrast regions.
1. Background Knowledge (Key Concepts)
-
Histogram: For a grayscale image with intensities iβ{0,1,β¦,255}, the histogram h[i] is the count of pixels having value i. This describes how intensities are distributed across the image.
-
CDF for intensities: The cumulative distribution function is
It tells you how many pixels have intensity β€i. Normalizing this CDF gives a function that maps original intensities to new ones.
- Equalization formula: Given total pixels MΓN, and CDFminβ as the smallest non-zero CDF value, the normalized CDF is
This produces a mapping into [0,255] that you apply to every pixel.
2. Algorithm / Approach Pattern
This problem fits a common βprecompute mapping, then apply itβ pattern:
- Analyze distribution: compute histogram and CDF from the input image.
- Create a lookup table (LUT): use the CDF-based formula to compute a new intensity value for each possible input intensity (0β255).
- Remap pixels via lookup: replace each pixelβs value with its mapped value from the LUT.
In NumPy terms, you:
- Compute histogram over possible values.
- Compute cumulative sum to get CDF.
- Normalize CDF into 0β255.
- Use the result as an indexing array to transform the whole image at once (vectorized).
3. Step-by-Step Strategy (Implementation Outline)
Assume input is a 2D NumPy array img with dtype uint8 and values in [0, 255].
- Get image size and flatten if convenient
M, N = img.shape
total = M * N
- Compute histogram h[i]
- Use np.bincount or manual counting, with length 256:
hist = np.bincount(img.ravel(), minlength=256)
- Compute CDF
cdf = hist.cumsum()
- Find CDFminβ (minimum non-zero CDF)
cdf_nonzero = cdf[cdf > 0]
cdf_min = cdf_nonzero # or cdf[cdf > 0].min()
- Normalize CDF to [0, 255]
- Follow the given formula, watch the types:
cdf_norm = (cdf - cdf_min) / (total - cdf_min) * 255
# Clip and cast to uint8
cdf_norm = np.clip(cdf_norm, 0, 255).astype(np.uint8)
- Create the mapping (lookup table)
- cdf_norm already has length 256, so itβs a LUT: mapping[i] = cdf_norm[i].
- Apply mapping to the image
- Use the image values as indices:
out = cdf_norm[img]
- out is your equalized image.
4. Common Pitfalls
- Integer vs float division: If you do the normalization with integer arrays, the division will truncate and most values may become 0. Ensure at least one operand is float:
cdf_norm = (cdf - cdf_min) * 255.0 / (total - cdf_min)
-
Forgetting to handle zero CDF: If you do not subtract CDFminβ, many low intensities might map to 0, causing loss of detail in dark regions. Always use the minimum non-zero CDF as specified.
-
Not clipping or casting: The normalized CDF can contain out-of-range values due to numerical issues; always clip to [0, 255] and cast to uint8.
-
Assuming non-8-bit input: This formula assumes 8-bit grayscale (256 levels). If the image has a different range, you must adjust the number of bins and scale accordingly.
-
Using loops instead of vectorization: Looping over each pixel in Python is slow. Use the LUT with NumPy indexing (out = lut[img]) for efficiency.
5. Time & Space Complexity
Let L be the number of gray levels (256 for 8-bit) and N=MΓN be total pixels.
-
Time complexity:
-
Histogram computation: O(N)
-
CDF computation: O(L)
-
Mapping pixels using LUT: O(N) Overall: O(N+L)βO(N) for 8-bit images.
-
Space complexity:
-
Histogram, CDF, and LUT arrays: O(L)
-
Output image: O(N) Additional over input: O(N+L), which is dominated by O(N) for large images.