Image Processing with NumPy Array

In Python we need to use the module pillow to process images. Just as usual, pillow module can be install using PIP command in your command prompt: pip install pillow

After installing you can check the pillow version using following code:

import PIL
print('Pillow Version:', PIL.version)

Pillow Version: 7.2.0

Loading Images with Module Pillow

Select a test image to load and work with Pillow (PIL) library. Images can be either PNG or JPEG. In this example, we’ll use an image named Snake-River_Wyoming.jpg. Either upload the image in the working directory or give your desired path. The Image class is the heart of PIL, and its properties help manipulate the pixels, format, and contrast of the image.

The Image class uses these functions:

  • open(): This can directly load the image. It has info properties like format, which gives information about the digital file format of an image, mode which gives a piece of information about pixel format (e.g., RGB or L), andsize, which displays the dimensions of the image in pixels (e.g., 1920×1440).
  • show(): This will display the image. Your default photo preview application will pop up.
  • pillow reference: https://pillow.readthedocs.io/en/stable/reference/Image.html

from PIL import Image
'Open the image form current directory'
img1 = Image.open('Snake-River_Wyoming.jpg')
width, height = img1.size
print(width, height)

'summarize some details about the image'
print(img1.format)
print(img1.mode)
'print(img1.histogram())'

'rotate image'
img2 = img1.rotate(180)
img2.save('jenny-01.jpg')

'crop image'
area = (0, 0, width/2, height/2)
img3 = img1.crop(area)
'img3.show()'

'resize image'
img4 = img1.resize((int(width/2), int(height/2)))
'img4.show()'

'paste one image to another'
img5 = Image.open("black-bear.png")
img1.paste(img5, (100, 100))
'img1.show()

'''
transposing image - Options are:
PIL.Image.FLIP_LEFT_RIGHT, PIL.Image.FLIP_TOP_BOTTOM,
PIL.Image.ROTATE_90, PIL.Image.ROTATE_180, PIL.Image.ROTATE_270,
PIL.Image.TRANSPOSE or PIL.Image.TRANSVERSE.
'''
img6 = img1.transpose(Image.TRANSVERSE)
img6.show()

splits = img1.split()
splits[2].show()
print(splits)

img1.thumbnail((100,100))
img1.show()

Three mode of image conversion – RBG, Palette, Luminance

RGB Mode

We can convert an image to RGB mode by calling method Image.convert(‘RGB’)

from PIL import Image
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
image = Image.open('session-13_red-blue-gradient.png')
print(type(image))
image = image.convert('RGB')
print(type(image))
red = np.array(image.getchannel(0))
print(red.shape)
green = np.array(image.getchannel(1))
print(green.shape)
blue = np.array(image.getchannel(2))
print(blue.shape)
print(f'RGB Red color is: {red}')
print(f'RGB Green color is: {green}')
print(f'RGB Blue color is: {blue}')
plt.figure(figsize=(10, 8))
plt.imshow(image)

<class 'PIL.PngImagePlugin.PngImageFile'>
<class 'PIL.Image.Image'>
(600, 800)
(600, 800)
(600, 800)
RGB Red color is: [[255 255 255 ... 255 255 255]
 [255 255 255 ... 255 255 255]
 [254 254 254 ... 254 254 254]
 ...
 [  1   1   1 ...   1   1   1]
 [  0   0   0 ...   0   0   0]
 [  0   0   0 ...   0   0   0]]
RGB Green color is: [[0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 ...
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]]
RGB Blue color is: [[  0   0   0 ...   0   0   0]
 [  0   0   0 ...   0   0   0]
 [  1   1   1 ...   1   1   1]
 ...
 [254 254 254 ... 254 254 254]
 [255 255 255 ... 255 255 255]
 [255 255 255 ... 255 255 255]]
<matplotlib.image.AxesImage at 0x172162afe20>

Palette Mode

“P” mode maps with a color palette. We can convert the same image to palette mode by calling Image.convert(‘P’).

'Convert to palette mode'
image = Image.open('session-13_red-blue-gradient.png').convert('P')
'Extract the palette and reshape as 256 entries of 3 RGB bytes each'
palette = np.array(image.getpalette()).reshape(256,3)
print(palette[15]) # red color for palette index 15 - [255 0 0]
print(palette[190]) # blue color for palette index 190 - [ 0 0 255]
red = np.array(image.getchannel(0))
print(red)
plt.figure(figsize=(10, 8))
plt.imshow(image)

[255   0   0]
[  0   0 255]
[[ 15  15  15 ...  15  15  15]
 [ 15  15  15 ...  15  15  15]
 [ 15  15  15 ...  15  15  15]
 ...
 [190 190 190 ... 190 190 190]
 [190 190 190 ... 190 190 190]
 [190 190 190 ... 190 190 190]]
<matplotlib.image.AxesImage at 0x17200bf26d0>

Luminance Mode

The same image could be converted to L mode, which means “Luminance” just as the HSL code mode. ‘L’ mode image is actual a grayscale one, being mapped to black and white pixels.

'Open into greyscale, or L mode'
image = Image.open('session-13_red-blue-gradient.png').convert('L')
c0 = np.array(image.getchannel(0))
print(c0)
plt.figure(figsize=(10, 8))
plt.imshow(image, cmap='gray')

[[76 76 76 ... 76 76 76]
 [76 76 76 ... 76 76 76]
 [76 76 76 ... 76 76 76]
 ...
 [29 29 29 ... 29 29 29]
 [29 29 29 ... 29 29 29]
 [29 29 29 ... 29 29 29]]
<matplotlib.image.AxesImage at 0x172162a7a90>

The top row of the image is 76 and the bottom row is 29. These numbers are coming from the following calculation:

  • The formula for converting RGB to L is: L = R * 299/1000 + G * 587/1000 + B * 114/1000
  • in the top row, R=255, G=0, B=0, so the Luminance has become: L = 255 * 299/1000 + 0 + 0 = 76
  • on the bottom row, R=0, G=0, B=255, so the Luminance has become: L = 0 + 0 + 255 * 114/1000 = 29

Demo for three modes

from PIL import Image
from matplotlib import pyplot as plt
%matplotlib inline
im = Image.open("sean-01.jpg")
im_l = im.convert('L')
im_p = im.convert('P')
im_rgb = im.convert('RGB')
splits = im_rgb.split()
'''
narr1 = np.asarray(im)
narr2 = np.asarray(im_l)
narr3 = np.asarray(im_p)
narr4 = np.asarray(im_rgb)
print(narr1.shape)
print(narr1.nbytes)
print(narr2.shape)
print(narr2.nbytes)
print(narr3.shape)
print(narr3.nbytes)
print(narr4.shape)
print(narr4.nbytes)
'''
im.show()
im_l.show()
im_p.show()
plt.figure(figsize = (10,8))
plt.imshow(im_l)
plt.imshow(im_p)

im_l.save('sean-01-gray.jpg')
fig, axs = plt.subplots(2,2, figsize=(12,10))
axs[0,0].set_title('Original')
axs[0,1].set_title('Luminance')
axs[1,0].set_title('Palette')
axs[1,1].set_title('RGB')
axs[0, 0].imshow(im)
axs[0, 1].imshow(im_l, cmap='gray')
axs[1, 0].imshow(im_p)
axs[1, 1].imshow(im_rgb)
fig.tight_layout()
plt.show()

Loading the Image with Matplotlib

We will use the Matplotlib library to load the same image and display it in the Matplotlib frame. Just like PIL, it has the image class that performs the same function. Functions used in this piece of code are imread(), which loads the image in the form of an array of the pixel and imshow(), which displays that image. Loading image data is supported by the Pillow library. Natively, Matplotlib only supports PNG images. The commands shown below fall back on Pillow if the native read fails.

We will use the pyplot class from the Matplotlib library to plot the image into the frame.

'load and display an image with Matplotlib'
from matplotlib import image
from matplotlib import pyplot as plt
%matplotlib inline
'load image as pixel array'
image = image.imread('aurora.jpg')
'summarize shape of the pixel array'
print(image.dtype)
print(image.shape)
'display the array of pixels as an image'
plt.figure(figsize = (15,10)) # width, height in inches, If not provided, defaults to [6.4, 4.8].
plt.imshow(image, aspect='auto')
plt.show()

uint8
(720, 1080, 3)

After the first step of loading the image using the dtype argument, we get a report on the data type of the array. In this case, it is 8-bit unsigned integers. The shape of the array is 1080 pixels wide by 720 pixels high and 3 denotes color channels for red, green, and blue.

Convert to NumPy Array and Back

In Python, Pillow is the most popular and standard library when it comes to working with image data.

NumPy uses the asarray() class to convert PIL images into NumPy arrays. The np.array function also produce the same result. The type function displays the class of an image.

The process can be reversed using the Image.fromarray() function. This function comes in handy when the manipulation is performed on numpy.ndarray image data, that we laterwant to save as a PNG or JPEG file.

from PIL import Image
from numpy import asarray
'load the image'
image = Image.open('sean-01.jpg')
print(type(image))
'convert image to numpy array'
data = asarray(image)
print(type(data))
'summarize shape'
print(data.shape)
'create red image'
data_red = data.copy()
print(data_red)
data_red[:, :, (1, 2)] = 0
print(data_red)
'create green image'
data_green = data.copy()
data_green[:, :, (0, 2)] = 0

'create blue image'
data_blue = data.copy()
data_blue[:, :, (0, 1)] = 0
'create Pillow image'
image2 = Image.fromarray(data)
print(type(image2))
'summarize image details'
print(image2.mode)
print(image2.size)

fig, axs = plt.subplots(1,4, figsize=(15, 10))
axs[0].imshow(data)
axs[1].imshow(data_red)
axs[2].imshow(data_green)
axs[3].imshow(data_blue)
fig.tight_layout()
plt.show()
data_RGB = np.concatenate((data_red, data_green, data_blue), axis=1)
plt.figure(figsize = (6,4))
plt.imshow(data_RGB)
plt.show()
image = Image.fromarray(data_green)
image.save('sean-01-RGB.jpg')

<class 'PIL.JpegImagePlugin.JpegImageFile'>
<class 'numpy.ndarray'>
(1080, 1920, 3)
[[[ 68  63  59]
  [ 72  67  63]
  [ 81  71  69]
  ...
  [202 187 192]
  [204 187 193]
  [205 189 192]]

 [[ 74  69  65]
  [ 76  71  67]
  [ 81  73  70]
  ...
  [202 187 194]
  [203 188 193]
  [204 189 192]]

 [[ 78  73  70]
  [ 80  75  72]
  [ 84  76  74]
  ...
  [202 189 196]
  [203 190 197]
  [204 192 196]]

 ...

 [[156 152 169]
  [150 146 163]
  [143 140 157]
  ...
  [144 143 141]
  [143 142 140]
  [142 141 139]]

 [[152 148 165]
  [148 144 161]
  [142 139 156]
  ...
  [146 145 143]
  [148 147 145]
  [148 147 145]]

 [[141 137 154]
  [139 135 152]
  [134 131 148]
  ...
  [149 148 146]
  [152 151 149]
  [154 153 151]]]
[[[ 68   0   0]
  [ 72   0   0]
  [ 81   0   0]
  ...
  [202   0   0]
  [204   0   0]
  [205   0   0]]

 [[ 74   0   0]
  [ 76   0   0]
  [ 81   0   0]
  ...
  [202   0   0]
  [203   0   0]
  [204   0   0]]

 [[ 78   0   0]
  [ 80   0   0]
  [ 84   0   0]
  ...
  [202   0   0]
  [203   0   0]
  [204   0   0]]

 ...

 [[156   0   0]
  [150   0   0]
  [143   0   0]
  ...
  [144   0   0]
  [143   0   0]
  [142   0   0]]

 [[152   0   0]
  [148   0   0]
  [142   0   0]
  ...
  [146   0   0]
  [148   0   0]
  [148   0   0]]

 [[141   0   0]
  [139   0   0]
  [134   0   0]
  ...
  [149   0   0]
  [152   0   0]
  [154   0   0]]]
<class 'PIL.Image.Image'>
RGB
(1920, 1080)

Manipulating and Saving the Image

Now that we have converted our image into a Numpy array, we might come across a case where we need to do some manipulations on an image before using it into the desired model. In this section, you will be able to build a grayscale converter. You can also resize the array of the pixel image and trim it.

After performing the manipulations, it is important to save the image before performing further steps. The format argument saves the file in different formats, such as PNG, GIF, or PEG.

For example, the code below loads the photograph in JPEG format and saves it in PNG format.

Trim and Resize Image

import numpy as np
from PIL import Image
img_orig = Image.open('jenny-01.jpg')
img_gray = np.array(Image.open('jenny-01.jpg').convert('L'))
print(type(img_gray))
gr_im= Image.fromarray(img_gray).save('jenny-01-L.png')
'resizing the image'
load_img_rz = np.array(Image.open('jenny-01.jpg').resize((200,200)))
Image.fromarray(load_img_rz).save('r_jenny-01.jpg')
print("After resizing:",load_img_rz.shape)
'Trim the image'
im = np.array(Image.open('jenny-01.jpg'))
print("Before trimming:",im.shape)
im_trim = im[300:900, 300:900]
print("After trimming:",im_trim.shape)
Image.fromarray(im_trim).save('trim_jenny-01.jpg')
fig, axs = plt.subplots(2, 2, figsize=(20,15))
axs[0, 0].imshow(img_orig)
axs[0,1].imshow(load_img_rz)
axs[1,0].imshow(img_gray, cmap='gray')
axs[1,1].imshow(im_trim)
fig.tight_layout()
plt.show()

<class 'numpy.ndarray'>
After resizing: (200, 200, 3)
Before trimming: (1080, 1440, 3)
After trimming: (600, 600, 3)

Negative / positive inversion

By inverting pixel values we could switch image between negative and positive mode. A negative-positive inverted image can be generated by subtracting the pixel value from the max value (255 for uint8).

import numpy as np
from PIL import Image
image = Image.open('sean-01.jpg')
im = np.array(image)
im_i = 255 - im
image2 = Image.fromarray(im_i)
image2.save('sean-01_inverse.jpg')
plt.figure(figsize=(12, 8))
plt.imshow(im_i)

Color Reduction

Cut off the remainder of the division using // and multiply again, the pixel values become discrete and the number of colors can be reduced.

import numpy as np
from PIL import Image
im = np.array(Image.open('jenny-01.jpg'))
im_32 = im // 32 * 32
im_128 = im // 128 * 128
im_dec = np.concatenate((im, im_32, im_128), axis=0)
Image.fromarray(im_dec).save('jenny-01_reduced.jpg')
plt.figure(figsize=(10,30))
plt.imshow(im_dec)

Image binarization with NumPy

If you just want to binarize to black and white with threshold values, you can do it with basic NumPy operations.

import numpy as np
from PIL import Image
im_orig = Image.open('sean-01.jpg')
im1 = np.array(im_orig)
im_gray = np.array(im_orig.convert('L'))
im_bin = (im_gray > 128) * 255
im_bin_keep = (im_gray > 128) * im_gray
im_combined = np.concatenate((im_gray, im_bin, im_bin_keep), axis=1)
Image.fromarray(np.uint8(im_bin)).save('sean-01-bin.jpg')
Image.fromarray(im_gray).save('sean-01-gray2.jpg')
Image.fromarray(im_bin_keep).save('sean-01-bin-keep.jpg')
Image.fromarray(im_combined).save('sean-01-combined.jpg')
fig, axs = plt.subplots(2,2, figsize=(15, 10))
axs[0,0].imshow(im_orig)
axs[0,1].imshow(im_gray)
axs[1,0].imshow(im_bin, cmap='gray')
axs[1,1].imshow(im_bin_keep, cmap='hot')
fig.tight_layout()
plt.show()

Since True is regarded as 1 and False is regarded as 0, when multiplied by 255 which is the Max value of uint8, True becomes 255 (white) and False becomes 0 (black).

If we multiply the boolean ndarray of the comparison result by the original ndarray, the pixel value of True remains original and the pixel value of False is 0 (black).

For color image

By applying different values to each RGB color, you can create a colorful image.

Generate a three-dimensional empty ndarray with np.empty() and store results of multiplying each color (each channel) by each values.

The size (height, width) obtained by shape is expanded by * and specified in np.empty().

im_orig = Image.open('sean-01.jpg')
im_gray = np.array(im_orig.convert('L'))
im_bool = im_gray > 128
im_dst = np.empty((*im_gray.shape, 3))
r, g, b = 255, 128, 32
im_dst[:, :, 0] = im_bool * r
im_dst[:, :, 1] = im_bool * g
im_dst[:, :, 2] = ~im_bool * b
Image.fromarray(np.uint8(im_dst)).save('sean-01-bin2.jpg')
Image.fromarray(im_gray).save('sean-01-gray3.jpg')
fig, axs = plt.subplots(1,3, figsize=(15, 10))
axs[0].imshow(im_orig)
axs[1].imshow(im_gray)
axs[2].imshow(im_dst)
plt.show()

im = np.array(Image.open('sean-01.jpg'))
im_th = np.zeros_like(im)
thresh = 128
maxval = 255
for i in range(3):
im_th[:, :, i] = (im[:, :, i] > thresh) * maxval
Image.fromarray(np.uint8(im_th)).save('sean-01_from_color.jpg')
plt.figure(figsize=(10,8))
plt.imshow(im_th)

More flexible processing is also possible, such as changing the threshold value or changing the replacement value for each color. You can write neatly by using a list (or tuple) and zip().

l_thresh = [64, 128, 192]
l_maxval = [192, 128, 64]
for i, thresh, maxval in zip(range(3), l_thresh, l_maxval):
im_th[:, :, i] = (im[:, :, i] > thresh) * maxval
Image.fromarray(np.uint8(im_th)).save('sean-01-_binarization_from_color2.jpg')
plt.figure(figsize=(10,8))
plt.imshow(im_th)

Alpha blending images with NumPy

Since NumPy can easily perform arithmetic operations for each pixels of the array, alpha blending can also be realized with a simple expression.

import numpy as np
from PIL import Image
im = Image.open('sean-03.jpg')
width, height = im.size
src1 = np.array(im)
src2 = np.array(Image.open('sean.jpg').resize(im.size, Image.BILINEAR))
dst = src1 * 0.7 + src2 * 0.3
Image.fromarray(dst.astype(np.uint8)).save('sean-mixed-03.jpg')
plt.figure(figsize=(12,10))
plt.imshow(dst.astype(np.uint8))