Introduction to Image Processing with Python
By: Tonichi Edeza
Tags: Lighting, Image Processing, Data Science, Adjustment, Python

In this lesson, we learn how to adjust images by interpolating the Cumulative Distribution Function of their greyscale intensities. This is a vital tool for any data scientist working with images.

Let’s begin!

As always, first we must import the required libraries.

import numpy as np

import matplotlib.pyplot as plt

import pandas as pd from skimage.io import imshow, imread

from skimage.color import rgb2gray

from skimage import img_as_ubyte, img_as_float

from skimage.exposure import histogram, cumulative_distribution

Let’s begin by loading an image with obvious lighting issues.

Dark Street (Image by Author)

The above image has some clear lighting issues, but we should be able to correct this using the power of Python.

One common way of handling lighting issues is through Histogram Manipulation. In essence, what we do is first convert the image to greyscale.

plt.figure(num=None, figsize=(8, 6), dpi=80)

dark_image_grey = img_as_ubyte(rgb2gray(image_dark))

imshow(dark_image_grey);

Image converted to greyscale

We then have to extract the image’s value histogram.

freq, bins = histogram(dark_image_grey) plt.figure(num=None, figsize=(8, 6), dpi=100, facecolor='white')

freq, bins = histogram(dark_image_grey)

plt.step(bins, freq/freq.sum())

plt.xlabel('intensity value', fontsize = 12)

plt.ylabel('fraction of pixels', fontsize = 12);

Intensity Values of Image

It is very clear that this histogram does not resemble a normal distribution. You might be tempted to try and snap this distribution into a normal distribution. However there is a slightly more intuitive way to handle this issue.

Remember that the theoretical Cumulative Distribution Function (CDF) for a normal distribution is a straight line. This being the case, it is better to snap the CDF of our image into a straight line.

Actual CDF of the Image

To do this, we can make use of the interpolate function in NumPy.

interpolation = np.interp(freq, target_freq, target_bins)

Interpolation of the CDF

We can now then use the interpolation to help us adjust the actual CDF.

dark_image_eq = img_as_ubyte(interpolation[dark_image_grey].astype(int))

Adjusted CDF

As we can see, the CDF is much closer to the target CDF. However, to see if this was truly helpful we must view the actual image.

imshow(dark_image_eq);

CDF Adjusted Image

We see that the adjustment we made to the CDF has had quite a significant effect on the image. We can now see details that were previously hidden in the shadows. To wrap this all up, let us create a function which will adjust the CDF of any image we feed it.

def histogram_adjuster(image):

dark_image_grey = img_as_ubyte(rgb2gray(image))

freq, bins = cumulative_distribution(dark_image_grey) target_bins = np.arange(255)

target_freq = np.linspace(0, 1, len(target_bins)) interpolation = np.interp(freq, target_freq, target_bins)

dark_image_eq =

img_as_ubyte(interpolation[dark_image_grey].astype(int))

freq_adj, bins_adj = cumulative_distribution(dark_image_eq)





fig, axes = plt.subplots(1, 2, figsize=(15,7));

imshow(dark_image_grey, ax = axes[0]);

imshow(dark_image_eq, ax = axes[1]);



axes[0].axis('off')

axes[1].axis('off')

axes[0].set_title('Unadjusted Image', fontsize = 17)

axes[1].set_title('Adjusted Image', fontsize = 17)



fig, axes = plt.subplots(1, 1, figsize=(19,7));

plt.step(bins, freq, c='blue', label='Actual CDF')

plt.step(bins_adj, freq_adj, c='purple', label='Adjusted CDF')

plt.plot(target_bins,

target_freq,

c='red',

label='Target CDF',

linestyle = '--')



plt.legend(prop={'size': 14})

plt.xlim(0, 255)

plt.ylim(0, 1)

plt.xlabel('Intensity values', fontsize = 15)

plt.ylabel('Cumulative fraction of pixels', fontsize = 15);

Side by Side Comparison

Function Output

Since we have created a function, we can now simply call it and feed it any image that we want to adjust.

Side by Side Comparison (Image by Author)

Function Output

Note that we can also bypass the greyscale conversion all together and adjust the colored image directly.

def histogram_adjuster_color(image):

freq, bins = cumulative_distribution(image) target_bins = np.arange(255)

target_freq = np.linspace(0, 1, len(target_bins)) interpolation = np.interp(freq, target_freq, target_bins)

image_eq = img_as_ubyte(interpolation[image].astype(int))

freq_adj, bins_adj = cumulative_distribution(image_eq)





fig, axes = plt.subplots(1, 2, figsize=(15,7));

imshow(image, ax = axes[0]);

imshow(image_eq, ax = axes[1]);



axes[0].axis('off')

axes[1].axis('off')

axes[0].set_title('Unadjusted Image', fontsize = 17)

axes[1].set_title('Adjusted Image', fontsize = 17)



fig, axes = plt.subplots(1, 1, figsize=(19,7));

plt.step(bins, freq, c='blue', label='Actual CDF')

plt.step(bins_adj, freq_adj, c='purple', label='Adjusted CDF')

plt.plot(target_bins,

target_freq,

c='red',

label='Target CDF',

linestyle = '--')



plt.legend(prop={'size': 15})

plt.xlim(0, 255)

plt.ylim(0, 1)

plt.xlabel('Intensity values', fontsize = 17)

plt.ylabel('Cumulative fraction of pixels', fontsize = 17);

Colored Adjustment

Colored CDF

We should note however that it may not be as effective compared to the greyscale converted image. However from a purely aesthetic standpoint it is quite interesting.

In Conclusion

Histogram manipulation can be a tricky processes. Snapping the CDF of your image to a straight line is just one of many ways to make the image more palatable for our human eyes. On that note, that there is no hard rule that states that you must convert the CDF into a straight line. Sometimes it may be best to snap it into a sinusoidal line or even a sawtooth line. The key is that the image becomes easier for us humans to understand.

To practice, I would encourage you to try it out on some images you have taken. I was amazed at how many little details the machine could actually pick up on.