Image Processing with Python — Alternative Histogram Equalization Methods | by Tonichi Edeza | Towards Data Science

In a previous article, we discussed how to manually equalize each channel’s histogram. However, direct manipulation of the RGB channels is only one method to enhance and image. In this article we shall go over these methods and see their effects.
Let’s get started!
As always, let us first import the required Python Libraries.
import numpy as npimport matplotlib.pyplot as pltfrom skimage.io import imshow, imreadfrom skimage.color import rgb2hsv, rgb2gray, rgb2yuvfrom skimage import color, exposure, transformfrom skimage.exposure import histogram, cumulative_distribution, equalize_histfrom skimage import img_as_ubyte, img_as_uint
Great! Now let us load the image we wish to work with.
dark_image = imread('flea_market.png')plt.figure(num=None, figsize=(8, 6), dpi=80)imshow(dark_image);
The image clearly has lighting issues. Not only is it quite dark in some spots, it is blindingly bright in others. Let us first see what happens if we do a simple greyscale histogram adjustment.
dark_image_grey = img_as_ubyte(rgb2gray(dark_image))freq, bins = cumulative_distribution(dark_image_grey, nbins =256)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));axes[0].imshow(dark_image_grey, cmap = 'gray');axes[0].set_title(f'Original Greyscale', fontsize = 18)axes[1].imshow(dark_image_eq, cmap = 'gray');axes[1].set_title(f'Adjusted Greyscale', fontsize = 18)    axes[0].axis('off');axes[1].axis('off');
We can see that the image has improved significantly. However the bright spots are still quite evident. Let us see if we can remedy this. Below is a code for transforming the image into a Histogram Equalized Image by individually adjusting each color channel.
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        #Pad frequencies with min frequency    adj_freqs = []    for i in range(len(freq_bins)):        if len(freq_bins[i][0]) < 256:            frequencies = list(freq_bins[i][0])            min_pad = [min(frequencies)] * (256 - len(frequencies))            frequencies = min_pad + frequencies        else:            frequencies = freq_bins[i][0]        adj_freqs.append(np.array(frequencies))        #Plot RGB Images    fig, ax = plt.subplots(1,3, figsize=[15,5])    for n, ax in enumerate(ax.flatten()):        interpolation = np.interp(adj_freqs[n], 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()    #Plot Adjusted CDFs    fig, ax = plt.subplots(1,3, figsize=[15,3])    for n, ax in enumerate(ax.flatten()):        interpolation = np.interp(adj_freqs[n], 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()        adjusted_image = np.dstack((adjusted_figures[0][0],                                 adjusted_figures[1][0],                                 adjusted_figures[2][0]))        #Plot Original Image against Adjusted Image    fig, ax = plt.subplots(1,2, figsize=[17,7])        ax[0].imshow(image);    ax[0].set_title(f'Original Image', fontsize = f_size)        ax[1].imshow(adjusted_image);    ax[1].set_title(f'Adjusted Image', fontsize = f_size)            fig.tight_layout()        return adjusted_image
Excellent! We see that the image has improved greatly. However the glaring brightness of the bulbs are still very evident. To this end let us explore alternative ways to adjust the histogram of images.
Adjustment via the HSV Color Space
One alternative method we can explore is by first converting the RGB image into an HSV image. From there we can equalize the histogram for the V (Value) channel. The below code does just that (Note that I sliced the image so that it would only feed it the RGB channel, omitting the transparency channel). For the purposes of the article, we shall bypass our manually constructed code and opt for the equalize_hist function in Skimage Library.
hsv_image = color.rgb2hsv(dark_image[:,:,:-1])hsv_image[:, :, 2] = equalize_hist(hsv_image[:, :, 2])hsv_adjusted = color.hsv2rgb(hsv_image)    fig, ax = plt.subplots(1,2, figsize=[15,10])    ax[0].imshow(dark_image);ax[0].set_title(f'Original Image', fontsize = 18)    ax[1].imshow(hsv_adjusted);ax[1].set_title(f'HSV Adjusted Image', fontsize = 18)        fig.tight_layout()
It is clear that adjusting the image based solely on the V channel produces excellent results. Not only have the colors retained their vibrancy, the overall image also seems sharper.
Also, the overall warm tone of the image is maintained. In some cases this may be preferable. The glare from the bulbs are also not as blinding as those in the RGB adjusted images. The below code shows a side by side comparison of the RGB and HSV adjustment on another picture. For brevity and to ensure comparability, we will use the equalize_histogram function in Skimage to do the RGB adjustment.
rice_terraces = imread('rice_terraces.png')plt.figure(num=None, figsize=(8, 6), dpi=80)imshow(rice_terraces);
def rgb_vs_hsv(image):    f_size = 22        #Equalize Histogram    rgb_adjusted = image.copy()    rgb_adjusted = equalize_hist(rgb_adjusted)      #HSV Adjustment    new_image = color.rgb2hsv(image[:,:,:-1])    new_image[:, :, 2] = equalize_hist(new_image [:, :, 2])    hsv_adjusted = color.hsv2rgb(new_image)        images = [image, rgb_adjusted, hsv_adjusted]    names = ['Original', 'RGB Adjusted', 'HSV Adjusted']        fig, ax = plt.subplots(1,3, figsize=[19,14])    for n, ax in enumerate(ax.flatten()):        ax.imshow(images[n])        ax.set_title(f'{names[n]}', fontsize = f_size)        ax.set_axis_off()        fig.tight_layout()    rgb_vs_hsv(rice_terraces)
As we can see, there is a stark difference between adjusting the image directly via the color channels as compared to adjusting it via the V channel. The latter produces an image far crisper yet maintains its color overcast.
Adjustment via the YUV Color Space
Lastly, we should go over adjusting the image via the YUV color space. This is not too different from the previous method of adjusting the image by the HSV color space, in fact the codes are strikingly similar. We shall apply the method to the below image.
red_tree = imread('red_tree.png')plt.figure(num=None, figsize=(8, 6), dpi=80)imshow(red_tree);
#HSV Adjustment Manualhsv_image = color.rgb2hsv(red_tree[:,:,:-1])hsv_image[:, :, 2] = equalize_hist(hsv_image[:, :, 2])hsv_adjusted = color.hsv2rgb(hsv_image)#YUV Adjustment Manualyuv = color.rgb2yuv(red_tree[:,:,:-1])yuv[:, :, 0] = equalize_hist(yuv[:, :, 0])yuv_adjusted = color.yuv2rgb(yuv)fig, ax = plt.subplots(1,3, figsize=[15,10])    ax[0].imshow(red_tree);ax[0].set_title(f'Original Image', fontsize = 18)ax[1].imshow(hsv_adjusted);ax[1].set_title(f'HSV Adjusted', fontsize = 18)ax[2].imshow(yuv_adjusted);ax[2].set_title(f'YUV Adjusted', fontsize = 18)    fig.tight_layout()
We see that the YUV adjusted image stands out due to its overall brighter color. Unlike the deeper and crisper colors from the HSV adjusted image, the YUV offers overall increased lighting. This would make it ideal in situations where the goal would be to brighten the image’s color. Indeed, the YUV image almost looks like it was shot on a bright sunny day.
In Conclusion
We have only scratched the surface of alternative histogram adjustment techniques. I hope this has given you additional insight on the topic and opened your mind to trying out other methods for this staple process. In succeeding articles we shall discuss the myriad of alternative techniques we can use for this process as well as the many other color spaces available for us to use.
Remember, humans can see way more than RGB!