## Image Processing Techniques

### Introduction

As you may have noticed, each of our pages in our various tutorials are introduced by eye candy pictures, which have created with great care to enrich the content. One of those images has been the raison d'être of this chapter. We want to demonstrate how we created the picture for our chapter on Decorators. The idea was to play with decorators in "real life", small icons with images of small workers painting a room and on the other hand blending this with the "at" sign, the Python symbol for decorator. It is also a good example of how to create a watermark.

We will demonstrate in this chapter the whole process chain of how we created this image. The picture on the right side of the current page has also been created the same way but uses a director's chair on a small painters background as a watermark instead of the at sign.

At first, we write a function "imag_tile" for tiling images both in horizontal and in vertical direction. We will use this to create the background of our image.

Then we show how to cut out with slicing a cutout or an excerpt of an image. We will use the shade function, which we introduced in our previous chapter on image processing, to shade our image.

Finally, we will use the original image, the shaded image, plus an image with a binary at sign with the conditional numpy where function to create the final image. The final image contains the at sign as a watermark, cut out from the shaded image.

### Tiling an Image

The function imag_tile, which we are going to design, can be best explained with the following diagram:

The function imag_tile

imag_tile(img, n, m)

creates a tiled image by appending an image "img" m times in horizontal direction. After this we append the strip image consisting of m img images n times in vertical direction.

In the following code, we use a picture of for painting decorators as the tile image:

%matplotlib inline

import matplotlib.pyplot as plt
import matplotlib.image as mpimg

import numpy as np

def imag_tile(img, n, m=1):
"""
The image "img" will be repeated n times in
vertical and m times in horizontal direction.
"""

if n == 1:
tiled_img = img
else:
lst_imgs = []
for i in range(n):
lst_imgs.append(img)
tiled_img = np.concatenate(lst_imgs, axis=1 )
if m > 1:
lst_imgs = []
for i in range(m):
lst_imgs.append(tiled_img)
tiled_img = np.concatenate(lst_imgs, axis=0 )

return tiled_img

decorators_img = imag_tile(basic_pattern, 3, 3)

plt.axis("off")
plt.imshow(decorators_img)

Output::
<matplotlib.image.AxesImage at 0x7fb4f423ccc0>

An image is a 3-dimensional numpy ndarray.

type(basic_pattern)

Output::
numpy.ndarray

The first three rows of our image basic_pattern look like this:

basic_pattern[:3]

Output::
array([[[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
...,
[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
[ 1.,  1.,  1.]],

[[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
...,
[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
[ 1.,  1.,  1.]],

[[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
...,
[ 1.,  1.,  1.],
[ 1.,  1.,  1.],
[ 1.,  1.,  1.]]], dtype=float32)

The innermost lists of our image contain the pixels. We have three values corresponding the the R, G, and B values, this means that we have a 24-bit RGB PNG image, eight bits for each of R, G, B.

PNG images might also consist of 32-bit images (RGBA). The fourth value "A" will be used for transparancy, single channel grayscale.

It's easy to access indivual pixels by indexing, e.g. the pixel in row 100 and column 20:

basic_pattern[100, 28]

Output::
array([ 0.90196079,  0.89019608,  0.86274511], dtype=float32)

As we have seen, the pixels are float (float32) values between 0 and 1. Matplotlib plotting can handle both float32 and uint8 for PNG images. For all other formats it will be only uint8.

### Crop Images

We can also crop subimages with the slicing function. We crop the image from (90, 50), i.e. row 90 and column 50, to (50, 120) in the following example:

cropped = basic_pattern[90:150, 50:120]
plt.axis("off")
plt.imshow(cropped)

Output::
<matplotlib.image.AxesImage at 0x7fb4f406a908>

We will need this technique in the following.

We can use the slicing function to crop parts of an image. We will use this to make sure that both images have the same size.

at_img=mpimg.imread('at_sign.png')

# at_img and decorators_img have to be of equal size:
d_shape = decorators_img.shape
at_shape = at_img.shape
height, width, colours = [min(x) for x in zip(*(d_shape, at_shape))]
at_img = at_img[0:height, 0:width]


We define a function "shade" in the following script. "shade" takes two parameters. The first one "imag" is the image, which will be shaded and the second one is the shading factor. It can be a value between 0 and 1. If the factor is set to 0, imag will remain unchanged. If set to one, the image will be completetely blackened.

def shade(imag, percent):
"""
imag: the image which will be shaded
percent: a value between 0 (image will remain unchanged
and 1 (image will be blackened)
"""
tinted_imag = imag * (1 - percent)
return tinted_imag

plt.imshow(tinted_decorator_img)
print(tinted_decorator_img[:3])

[[[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
...,
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]]

[[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
...,
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]]

[[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
...,
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]
[ 0.5  0.5  0.5]]]


### Blend Images

#### First example

We have everything together now to create the blended image. Our at sign picture consists of black and white pixels. The blended image is constructed like this: Let p=(n, m) be an arbitrary pixel in the n-th row and m-th column of the image at_image. If the value of this pixel is not black or dark gray, we will use the pixel at position (n, m) from the picture decorators_img, otherwise, we will use the corresponding pixel from tinted_decorator_img. The where function of numpy is ideal for this task:

print(at_img.shape,
decorators_img.shape,
tinted_decorator_img.shape)
img2 = np.where(at_img > [0.1, 0.1, 0.1],
decorators_img,
tinted_decorator_img)

plt.axis("off")
plt.imshow(img2)

(1077, 771, 3) (1077, 771, 3) (1077, 771, 3)

---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
<ipython-input-9-42d5229b30d8> in <module>()
2       decorators_img.shape,
3       tinted_decorator_img.shape)
5 img2 = np.where(at_img > [0.1, 0.1, 0.1],
6                 decorators_img,

1244             return handler(fd)
1245         else:
-> 1246             with open(fname, 'rb') as fd:
1247                 return handler(fd)
1248     else:

FileNotFoundError: [Errno 2] No such file or directory: 'decorators2.png'

All there is left to do is save the newly created image:

In [ ]:
mpimg.imsave('decorators_with_at.png', img2)


#### Second Example

We want to use now a different image as a "watermark". Instead of the at sign, we want to use now a director's chair. We will create the image from the top of this page.

In [ ]:
images = [mpimg.imread(fname) for fname in ["director_chair.png", "the_sea.png", "the_sky.png"]]
director_chair, sea, sky = images

plt.axis("off")
plt.imshow(sea)

In [ ]:
plt.axis("off")
plt.imshow(director_chair)


In the following, we blend together the images director_chair, decorators_img and sea by using where of numpy once more:

In [ ]:
#sea2 = mpimg.imread('the_sea2.png')
img = np.where(director_chair > [0.9, 0.9, 0.9],
decorators_img,
sea)
plt.axis("off")
plt.imshow(img)
mpimg.imsave('decorators_with_chair', img)


We could have used "Image.open" from PIL instead of mpimg.imread from matplotlib to read in the pictures. There is a crucial difference or a potential "problem" between these two ways: The image we get from imread has values between 0 and 1, whereas Image.open consists of values between 0 and 255. So we might have to divide all the pixels by 255, if we have to work with an image read in by mpimg.imread:

In [ ]:
from PIL import Image

img = Image.open("director_chair.jpg")
img = img.resize((at_img.shape[1], at_img.shape[0]))
img = np.asarray(img)

plt.axis("off")
plt.imshow(img)

print(img[100, 129])

In [ ]:
# PIL: Pixel are within range 0 and 255
# mpimg: range 0 bis 1

img = np.asarray(img, np.float)
img = img / 255

print(img[100, 129])