Introduction to Image Processing with Python — Color Channel Histogram Manipulation for Beginners
By: Tonichi Edeza
Tags: Histograms, Image Processing, Data Science, Bookstores, Python

Introduction to Image Processing with Python — Color Channel Histogram Manipulation for Beginners

RGB Adjusted Bookstore (Image by Author)

Previously, I wrote an article regarding Histogram Manipulation. In it I discussed the basics of adjusting grayscale image values to make the overall image more palatable for humans. I briefly touched on the idea of adjusting the Cumulative Distribution Function for colored images, but stopped short of discussing the best way to do it. In this article we shall tackle just that.

Let’s get started!

As per usual, let us first import all the required libraries.

import numpy as np

import matplotlib.pyplot as plt

from skimage.io import imread, imshow

import matplotlib.pyplot as plt

from skimage.exposure import histogram, cumulative_distribution

from skimage import img_as_ubyte, img_as_float

from scipy.stats import norm

Great! Now let us load the image.

dark_image = imread('dark_books.png');

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

imshow(dark_image);

Dark Books (Image by Author)

To help us get a better idea of the RGB layers in this image, let us segregate each individual channel. Below is a useful code that will do that for any image.

def rgb_splitter(image):

rgb_list = ['Reds','Greens','Blues']

fig, ax = plt.subplots(1, 3, figsize=(17,7), sharey = True)

for i in range(3):

ax[i].imshow(image[:,:,i], cmap = rgb_list[i])

ax[i].set_title(rgb_list[i], fontsize = 22)

ax[i].axis('off')

fig.tight_layout() rgb_splitter(dark_image)

RGB Channels of the Image

Nice! We now have a general idea of what the individual color channels look like. Let us now check their CDFs, below is a useful function which will aid us.

def df_plotter(image):

freq_bins = [cumulative_distribution(image[:,:,i]) for i in

range(3)]

target_bins = np.arange(255)

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

names = ['Red', 'Green', 'Blue']

line_color = ['red','green','blue']

f_size = 20



fig, ax = plt.subplots(1, 3, figsize=(17,5))

for n, ax in enumerate(ax.flatten()):

ax.set_title(f'{names[n]}', fontsize = f_size)

ax.step(freq_bins[n][1], freq_bins[n][0], c=line_color[n],

label='Actual CDF')

ax.plot(target_bins,

target_freq,

c='gray',

label='Target CDF',

linestyle = '--') df_plotter(dark_image)

CDFs of the RGB Channels

As we can see, all three channels are quite far from the idealized straight line. To fix that let us simply interpolate their CDFs. Again, below is a useful function that does that for us.

def rgb_adjuster_lin(image):



target_bins = np.arange(255)

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

freq_bins = [cumulative_distribution(image[:,:,i]) for i in

range(3)]

names = ['Reds', 'Greens', 'Blues']

line_color = ['red','green','blue']

adjusted_figures = []

f_size = 20



fig, ax = plt.subplots(1,3, figsize=[15,5])

for n, ax in enumerate(ax.flatten()):

interpolation = np.interp(freq_bins[n][0], target_freq,

target_bins)

adjusted_image = img_as_ubyte(interpolation[image[:,:,

n]].astype(int))

ax.set_title(f'{names[n]}', fontsize = f_size)

ax.imshow(adjusted_image, cmap = names[n])

adjusted_figures.append([adjusted_image]) fig.tight_layout() fig, ax = plt.subplots(1,3, figsize=[15,5])

for n, ax in enumerate(ax.flatten()):

interpolation = np.interp(freq_bins[n][0], target_freq,

target_bins)

adjusted_image = img_as_ubyte(interpolation[image[:,:,

n]].astype(int))

freq_adj, bins_adj = cumulative_distribution(adjusted_image)



ax.set_title(f'{names[n]}', fontsize = f_size)

ax.step(bins_adj, freq_adj, c=line_color[n], label='Actual

CDF')

ax.plot(target_bins,

target_freq,

c='gray',

label='Target CDF',

linestyle = '--')

fig.tight_layout()

return adjusted_figures channel_figures = return adjusted_figures

Color Adjusted Images

We see significant improvement per color channel, with all of them almost resembling a straight line.

Additionally, note how this function returns all these values as a list of lists. This will serves us well for our final step, putting it all back together into a single picture. To give us a better idea of how this can be done, let us inspect our list of lists.

print(channel_figures)

print(f'Total Inner Lists : {len(channel_figures)}')

Contents of channel_figures

We see that within the variable channel_figures are three lists. These lists represent the values for the RGB channel. This being the case, it is possible for us to stitch all these values back together. The below code does just that.

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

imshow(np.dstack((channel_figures[0][0],

channel_figures[1][0],

channel_figures[2][0])));

Reconstructed Colored Bookstore Image

Note how significant the difference is. Not only does the image seem significantly brighter, the yellow overcast was also removed. Let us try this same technique on a different image. Fortunately we have already laid most of the groundwork by setting up function. The below is just plug and play.

dark_street = imread('dark_street.png');

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

imshow(dark_street);

Dark Street (Image by Author)

Now that we have loaded our new picture, let us simply run it by our functions.

channel_figures_street = adjusted_image_data = rgb_adjuster_lin(dark_street)

Color Adjusted Images

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

imshow(np.dstack((channel_figures_street [0][0],

channel_figures_street [1][0],

channel_figures_street [2][0])));

Reconstructed Colored Street Image

We can see an amazing improvement. Though admittedly the image is slightly overexposed. This may be due to the significantly bright neon lights in the back. In future articles, we shall learn how to fine tune our adjustment methodologies so that our functions can be more generalizable.

In Conclusion

In this article we learned how to adjust each RGB channel to preserve the color information of the image. Such a technique is a great improvement over the previous grayscale adjustment method. Going forward we will discuss the many different distributions one can snap the CDF into. Such fine tuning of the adjustment methodology should help us improve our functions so that they can work with any image.

I know that this particular article has been quite function heavy, and for many beginners this may be a challenge to understand. Hopefully, you can get the hang of using functions as they make working with Python a much more efficient (and dare I say fun) activity.