# Image processing with Python, NumPy (read, process, save)

Posted: 2019-05-14 / Tags: Python, NumPy, Image Processing

By storing the images read by Pillow(PIL) as a NumPy array `ndarray`, various image processing can be performed using NumPy functions.

By the operation of ndarray, acquisition and rewriting of pixel values, trimming by slice, concatenating can be done. Those who are used to NumPy can do a lot of things without using libraries such as OpenCV.

Even when using OpenCV, Python's OpenCV treats image data as `ndarray`, so it is useful to remember the processing in NumPy (`ndarray`).

This post describes the following contents.

• How to read image file as NumPy array `ndarray`
• How to save NumPy array `ndarray` as image file

Examples of image processing with NumPy (`ndarray`):

• Generation of single color image and concatenation
• Negative / positive inversion (inversion of pixel value)
• Color reduction
• Binarization
• Gamma correction
• Trimming with slice
• Paste with slice
• Rotate and flip

Here I will describe reading and saving of image files using Pillow. Refer to the following post about reading and saving image files with OpenCV.

## How to read an image file as ndarray

Pass the image data read by `PIL.Image.open()` to `np.array()` to obtain ndarray.

RGB (color) images become 3D ndarray (`row (height) x column (width) x color (3)`), black and white (grayscale) images become 2D ndarray (`row (height) x column (width)`).

``````from PIL import Image
import numpy as np

im = np.array(Image.open('data/src/lena_square.png'))

print(im.dtype)
# uint8

print(im.ndim)
# 3

print(im.shape)
# (512, 512, 3)
``````

When converting from `PIL.Image` to `ndarray`, the color order is RGB (red, green, blue). Note that this is different from when reading image files with OpenCV (BGR). If you want to convert the order, see the following post.

If you want to calculate with decimal point for image data, read in `float`.

``````im_f = np.array(Image.open('data/src/lena_square.png'), np.float)

print(im_f.dtype)
# float64
``````

Because it is `ndarray`, acquisition of pixel value is easy. The origin `(0, 0)` is the upper left of the image. Of course, methods such as `min()` and `max()` can be used as they are.

In the following example, the pixel value `[R, G, B]` at the position of `(x, y) = (256, 256)` and the minimum value of `R` are acquired.

``````print(im[256, 256])
# [180  65  72]

print(im[:, :, 0].min())
# 54
``````

Examples of other processes are described later.

## How to save ndarray as image file

Pass `ndarray` to `Image.fromarray()` to obtain `PIL.Image`.

`PIL.Image` can be saved as an image file with `save()`.

``````pil_img = Image.fromarray(im)
pil_img.save('data/temp/lena_square_save.png')
``````

You can write it in one line.

``````Image.fromarray(im).save('data/temp/lena_square_save.png')
``````

If the data type `dtype` of `ndarray` is `float` etc., convert it to `uint8` (unsigned 8-bit integer) to save it as JPG or PNG.

``````pil_img_f = Image.fromarray(im_f.astype(np.uint8))
pil_img_f.save('data/temp/lena_square_save.png')
``````

Note that if the pixel value is represented by `0.0` to `1.0`, it is necessary to multiply by `255` and convert to `uint8` and save.

## Generation of single color image and concatenation

Generate single-color images by setting other color values to `0`, and concatenate them horizontally with `np.concatenate()`. You can also concatenate images using `np.hstack()` or `np.c_[]`

``````from PIL import Image
import numpy as np

im = np.array(Image.open('data/src/lena_square.png'))

im_R = im.copy()
im_R[:, :, (1, 2)] = 0
im_G = im.copy()
im_G[:, :, (0, 2)] = 0
im_B = im.copy()
im_B[:, :, (0, 1)] = 0

im_RGB = np.concatenate((im_R, im_G, im_B), axis=1)
# im_RGB = np.hstack((im_R, im_G, im_B))
# im_RGB = np.c_['1', im_R, im_G, im_B]

pil_img = Image.fromarray(im_RGB)
pil_img.save('data/dst/lena_numpy_split_color.jpg')
`````` ## Negative / positive inversion (invert pixel value)

It is also easy to calculate and process pixel values.

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

im = np.array(Image.open('data/src/lena_square.png').resize((256, 256)))

im_i = 255 - im

Image.fromarray(im_i).save('data/dst/lena_numpy_inverse.jpg')
`````` ## Color reduction

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

``````import numpy as np
from PIL import Image

im = np.array(Image.open('data/src/lena_square.png').resize((256, 256)))

im_32 = im // 32 * 32
im_128 = im // 128 * 128

im_dec = np.concatenate((im, im_32, im_128), axis=1)

Image.fromarray(im_dec).save('data/dst/lena_numpy_dec_color.png')
`````` ## Binarization

It is also possible to assign to black and white according to the threshold.

See the following articles for details. ## Gamma correction

You can do multiplication, division, exponentiation, anything.

``````from PIL import Image
import numpy as np

im = np.array(Image.open('data/src/lena_square.png'), 'f')

im_1_22 = 255.0 * (im / 255.0)**(1 / 2.2)
im_22 = 255.0 * (im / 255.0)**2.2

im_gamma = np.concatenate((im_1_22, im, im_22), axis=1)

pil_img = Image.fromarray(np.uint8(im_gamma))
pil_img.save('data/dst/lena_numpy_gamma.jpg')
`````` ## Trimming with slice

By specifying an area with slice, you can trim it to a rectangle.

``````from PIL import Image
import numpy as np

im = np.array(Image.open('data/src/lena_square.png'))

print(im.shape)
# (512, 512, 3)

im_trim1 = im[128:384, 128:384]
print(im_trim1.shape)
# (256, 256, 3)

Image.fromarray(im_trim1).save('data/dst/lena_numpy_trim.jpg')
`````` See the following post for more information on slicing for `numpy.ndarray`.

It may be convenient to define a function that specifies the upper left coordinates and the width and height of the area to be trimmed.

``````def trim(array, x, y, width, height):
return array[y:y + height, x:x+width]

im_trim2 = trim(im, 128, 192, 256, 128)
print(im_trim2.shape)
# (128, 256, 3)

Image.fromarray(im_trim2).save('data/dst/lena_numpy_trim2.jpg')
`````` If an area outside the size of the image is specified, it is ignored.

``````im_trim3 = trim(im, 128, 192, 512, 128)
print(im_trim3.shape)
# (128, 384, 3)

Image.fromarray(im_trim3).save('data/dst/lena_numpy_trim3.jpg')
`````` ## Paste with slice

Using slices, one array rectangle can be replaced with another array rectangle.

By using this, a part of the image or the entire image can be pasted to another image.

``````import numpy as np
from PIL import Image

src = np.array(Image.open('data/src/lena_square.png').resize((128, 128)))
dst = np.array(Image.open('data/src/lena_square.png').resize((256, 256))) // 4

dst_copy = dst.copy()
dst_copy[64:128, 128:192] = src[32:96, 32:96]

Image.fromarray(dst_copy).save('data/dst/lena_numpy_paste.jpg')
`````` ``````dst_copy = dst.copy()
dst_copy[64:192, 64:192] = src

Image.fromarray(dst_copy).save('data/dst/lena_numpy_paste_all.jpg')
`````` Note that an error will occur if the size of the area specified on the left side differs from the size of the area specified on the right side.

By the operation for each element (= pixel) of the array, two images can be alpha-blended or composited based on a mask image. See the following articles for details.  ## Rotate and flip

There are also functions that rotate the array and flip it up, down, left and right.

Original image: Roteted image: Flipped image: 