Introduction to Image Processing with Python — Color Overcast Adjustment for Beginners
By: Tonichi Edeza
Tags: Image Processing, Coding, Data Science, Colors, Python

Overcast Adjusted Beach (Image by Author)

Image enhancement is a vital skill for any data scientist working with images. Most of the images we will be working with in our day to day work are most likely not shot in carefully controlled environments. Issues such as overexposure, underexposure, and color overcasts are common in real life data. As such, it would be useful to know how to deal with such issues.

In this article, we shall go over how to handle images with a color overcast.

Let’s get started!

#Import the required Python libraries

import numpy as np

import matplotlib.pyplot as plt

import pandas as pd

from skimage.io import imshow, imread

Sky with color overcast (Image by Author)

As we can see, the picture above has a color overcast. Though interesting from an artistic standpoint, such an issue may have consequences for the machine trying to process the data.

The first process we can do is examine the RGB spectrum of the image. To do that we must break the image into its RGB components.

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

fig, ax = plt.subplots(1, 3, figsize=(15,5), sharey = True)

for i in range(3):

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

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

RGB Split

From a purely visual perspective, we can see that the image has a pronounced deep red color.

But of course, this is just us eyeballing it. To get a better idea of the actual properties of the image we must examine it the way the machine sees it. The below function will produce the relevant statistics per color channel.

def channel_statistics(image):

df_color = []

for i in range(0, 3):

max_color =np.max(image[:,:,i])

mean_color = np.mean(image[:,:,i])

median_color = np.median(image[:,:,i])

perc_90 = np.percentile(image[:,:,i], 90, axis=(0,1))

perc_95 = np.percentile(image[:,:,i], 95, axis=(0,1))

perc_99 = np.percentile(image[:,:,i], 99, axis=(0,1))



row = (max_color, mean_color, median_color,

perc_90, perc_95, perc_99) df_color.append(row)



return pd.DataFrame(df_color,

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

columns = ['Max', 'Mean', 'Median',

'P_90',' P_95', 'P_99']) channel_statistics(image_overcast)

Channel Properties

In the table produced, we see that there is indeed a red overcast on the image. Now how do we balance it?

Though this will probably not work, for presentation reasons let us adjust the image via the mean, median, maximum values per channel.

def mean_and_median_adjusted(image):

fig, ax = plt.subplots(2, 2, figsize=(12,12), sharey = True)

f_size = 20

ax[0][0].imshow(image_overcast )

ax[0][0].set_title('Original', fontsize = f_size)

ax[0][1].imshow(img_as_ubyte((image / np.mean(image)).clip(0,

1)))

ax[0][1].set_title('Mean Adjusted', fontsize = f_size)



ax[1][0].imshow(img_as_ubyte((image/ np.median(image)).clip(0,

1)))

ax[1][0].set_title('Median Adjusted', fontsize = f_size) ax[1][1].imshow(img_as_ubyte((image/ np.max(image)).clip(0, 1)))

ax[1][1].set_title('Max Adjusted', fontsize = f_size);



fig.tight_layout() mean_and_median_adjusted(image)

Adjusting via the Mean, Median, and Max

We can see that the issue is not resolved via this method. Though one can say that the image is clearly much brighter, the red overcast is still very noticeable. One possible way to tackle this issue is by focusing on the red channel. The code below will adjust the red channel using its max value, while all the other channels are adjusted using their mean and median values.

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

f_size = 15

ax[0].imshow(image_overcast)

ax[0].set_title('Original', fontsize = f_size) ax[1].imshow(img_as_ubyte((image_overcast /

[np.max(image_overcast[:,:,0]),

np.mean(image_overcast[:,:,1]),

np.mean(image_overcast[:,:,2]),

np.mean(image_overcast[:,:,3])])

.clip(0, 1))) ax[1].set_title('Red : Max, Others : Mean', fontsize = f_size) ax[2].imshow(img_as_ubyte((image_overcast /

[np.max(image_overcast[:,:,0]),

np.median(image_overcast[:,:,1]),

np.median(image_overcast[:,:,2]),

np.median(image_overcast[:,:,3])])

.clip(0, 1)))

ax[2].set_title('Red : Max, Others : Median', fontsize = f_size);

Red at Max

It seems that we are on the right track. The image has clearly been rid of the red overcast and is much brighter overall. However, we now see a much more pronounced overcast of green and blue. This implies that we could have adjusted red far too much, or insufficiently adjusted green and blue. It seems that we need to fine tune our parameters.

The below code will filter the image based on specific percentile ranks per channel. For the purposes of this article, I have made it as a Python function. This allows us to feed any image into it.

def percentile_adjustment(image):

fig, ax = plt.subplots(2, 3, figsize=(15,10), sharey = True)

f_size = 15

red = 99.75

parameter_matrix = [[red] + [99]*3,

[red] + [95]*3,

[red] + [90]*3,

[red] + [85]*3,

[red] + [80]*3]



ax[0][0].imshow(image)

ax[0][0].set_title('Original', fontsize = f_size) ax[0][1].imshow(img_as_ubyte((image /

[np.percentile(image[:,:,i],

parameter_matrix[0][i], axis=(0, 1)) \

for i in range(0, 4)]).clip(0,1)))



ax[0][1].set_title(f'Red : {red}, Others : {parameter_matrix[0]

[1]}', fontsize = f_size) ax[0][2].imshow(img_as_ubyte((image /

[np.percentile(image[:,:,i],

parameter_matrix[1][i], axis=(0, 1)) \

for i in range(0, 4)]).clip(0,1)))



ax[0][2].set_title(f'Red : {red}, Others : {parameter_matrix[1]

[1]}', fontsize = f_size); ax[1][0].imshow(img_as_ubyte((image /

[np.percentile(image[:,:,i],

parameter_matrix[2][i], axis=(0, 1)) \

for i in range(0, 4)]).clip(0,1))) ax[1][0].set_title(f'Red : {red}, Others : {parameter_matrix[2]

[1]}', fontsize = f_size); ax[1][1].imshow(img_as_ubyte((image /

[np.percentile(image[:,:,i],

parameter_matrix[3][i], axis=(0, 1)) \

for i in range(0, 4)]).clip(0,1))) ax[1][1].set_title(f'Red : {red}, Others : {parameter_matrix[3]

[1]}', fontsize = f_size); ax[1][2].imshow(img_as_ubyte((image/

[np.percentile(image[:,:,i],

parameter_matrix[4][i], axis=(0, 1)) \

for i in range(0, 4)]).clip(0,1)))



ax[1][2].set_title(f'Red : {red}, Others : {parameter_matrix[4]

[1]}', fontsize = f_size);



percentile_adjustment(image_overcast)

Red at 99.75 Percentile

The improvement in image enhancement is quite noticeable. Not only is the red overcast greatly minimized, but the overexposure of the other channels are also kept to a minimum.

Visually it seems that the best results are when the other channels are adjusted between the 90–95th percentiles. Past that, the image seems to take on a blue tinge.

One could argue that setting the adjustment to 80 would be preferable as the sky itself is naturally blue and hence a blue overcast would be acceptable. However, for the purposes of this article we will stick to 95.

Overcast Adjusted Sky

Note that image enhancement can be very tricky at times. Techniques that work extraordinarily well for some images may not be as effective for others. An example is below.

image_overcast_beach = imread('tinted_sky.png')

imshow(image_overcast_beach);

Beach with color overcast (Image by Author)

The second image clearly exhibits issues similar to the first image. Let us try to apply the same process and see if the results are just as good. As we have already written a function, we should be able to call it and just feed it our second image.

percentile_adjustment(image_overcast_beach)

We see that the overcast was greatly diminished. However, we can see that there is a noticeable difference between the effectivity of our methodology in the top half and bottom half of the image. The sky portion of the image exhibits overexposure past the 95th percentile. Unfortunately, the water only loses its red overcast percentiles lower than 95. How can we address this issue?

Remember that the machine actually reads the image a matrix of numbers. As such as we can easily split it and apply different parameters to each area. In this case let us split the image between the sky and the water.

f_size = 15

parameter_matrix = [[99.9] + [97.75]*3,

[99.75] + [92]*3] fig, ax = plt.subplots(2, 2, figsize=(15,6), sharey = True)

ax[0][0].imshow(image_overcast_beach[0:320])

ax[0][0].set_title('Sky Original', fontsize = f_size) ax[1][0].imshow(image_overcast_beach[320:])

ax[1][0].set_title('Water Original', fontsize = f_size) ax[0][1].imshow(img_as_ubyte(((image_overcast_beach[0:320]/ [np.percentile(image_overcast_beach[:,:,i], parameter_matrix[0][i], axis=(0, 1)) for i in range(4)]).clip(0,1))))

ax[0][1].set_title('Sky Adjusted', fontsize = f_size) ax[1][1].imshow(img_as_ubyte(((image_overcast_beach[320:]/ [np.percentile(image_overcast_beach[:,:,i], parameter_matrix[1][i], axis=(0, 1)) for i in range(4)]).clip(0,1))))

ax[1][1].set_title('Water Adjusted', fontsize = f_size); fig.tight_layout()

Split and Adjusted

Lastly, we can combine the two parts using the concatenate function of NumPy.

imshow(np.concatenate((beach_correction_top, beach_correction_bottom), axis=0));

Concatenated Adjusted Images

As we saw, image enhancement can be quite tricky. Methods that will work well for one image may not work so well for another. But as long as we remember that images are nothing more than 3 dimensional matrices, we should be able to find a way to adequately manipulate it to suit our needs.

If you recall, we actually did most of our overcast corrections against a red overcast. To practice, I would suggest tackling photos with different overcasts. Being able to get a handle on how to manually manipulate images via Python will help you develop your algorithmic thinking, making it easier for you to generalize your processes. Eventually, you should be able to develop algorithms that will be able to do most of the color correcting for you.