Programming with Python

Vedran Šego, vsego.org

Contents:

  1. PIL(low)

Uniman logo

PIL(low)

PIL stands for Python Imaging Library. It is an old library that was used for image manipulations in Python, but was discontinued in 2011. It's fork (a new product created on the foundations of the old one), named Pillow, added Python 3 support and it is a part of all major Python distributions.

Luckily, they are both used in a similar manner and even their import statements are the same. So, if you need to install it manually, be sure to install Pillow and not PIL, but when searching for the information, look for "PIL" or "PIL python3".

The next part is only for the easier use in IPython Notebooks:

In [1]:
import IPython.display
from io import BytesIO

from IPython.core import display
from PIL import Image

def display_pil_image(im):
    """Displayhook function for PIL Images, rendered as PNG."""
    # Taken from http://stackoverflow.com/a/26649884/1667018

    b = BytesIO()
    im.save(b, format='png')
    data = b.getvalue()

    ip_img = display.Image(data=data, format='png', embed=True)
    return ip_img._repr_png_()

# register display func with PNG formatter:
png_formatter = get_ipython().display_formatter.formatters['image/png']
dpi = png_formatter.for_type(Image.Image, display_pil_image)

Examples

Here, we open an image and save it under a different name:

In [2]:
from PIL import Image

im = Image.open("12a-uniman.png")
im.save("image.png")

In IPython Notebooks, we use the following two commands to display the images:

In [3]:
# Reading from a file
IPython.display.Image("image.png")
# Directly from a variable
im
Out[3]:

Note: In Python programs, instead of just im (which works only in IPython Notebooks) to display the image, we usually use im.show(), much like we did with Matplotlib in the previous lectures:

In [4]:
im.show()

The main purpose of PIL is easy editing of the existing images.

For example, we can load an image, rotate it by $17^\circ$, and then resize it to the half of its original width while keeping its height.

In [5]:
from PIL import Image

im = Image.open("12a-uniman.png")
print("Size: {}x{}".format(im.size[0], im.size[1]))
rot = im.rotate(17).resize((im.size[0]//2, im.size[1]), Image.ANTIALIAS)
rot
Size: 400x169
Out[5]:

To create a new image, we simply use the new function:

In [6]:
im = Image.new("RGBA", (400, 200))

The first argument is the image mode:

  • "RGB" stands for "Red, Green, Blue" and means that each pixel has a 3-component color value (defined as a tuple of three integers between 0 and 255). All the image editing programs support this and you can use any of them to define your own colors. There are even web pages that help you define colors, for example Html Color Codes. Here are some of the colors,

    • (255, 0, 0) is red,

    • (255, 0, 255) is purple (full red + full blue and no green),

    • (255, 255, 0) is yellow.

  • "RGBA" is the same as "RGB" with the addition of the alpha channel, which defines opacity from 0 (fully transparent, i.e., invisible) to 255 (fully visible). For example,

    • (255, 0, 0, 255) is fully visible red,

    • (255, 0, 255, 127) is half-transparent purple,

    • (255, 255, 0, 0) is invisible.

  • "1" is black and white,

  • "L" is grayscale.

There are more modes; you can find them all here.

The second argument is quite obvious: a two-element tuple defining the width and the height of the new image. Such an image is then defined by integer-coordinated pixels from the top left (0, 0) to the bottom right (width-1, height-1).

We can now manipulate other images and paste them on this one. For example:

In [7]:
import numpy as np
from math import cos, sin, pi

logo = Image.open("12a-uniman.png")
im = Image.new("RGB", logo.size)
im.paste(logo)

for w in range(logo.size[0], 0, -5):
    # Shrinkage factor
    fact = w / logo.size[0]
    # New height (to maintain the aspect ratio of the logo)
    h = int(round(fact * logo.size[1]))
    # Create a resized version of the logo
    resized = logo.resize((w, h), Image.ANTIALIAS)
    # Convert to NumPy's array, for fast pixel manipulation
    transparent = np.array(resized)
    # Multiply the last component (the alpha channel) by fact**7,
    # making it semi-transparent (smaller the image, stronger the transparency)
    transparent[..., -1] = 255*(0.025 + 0.975*fact**7)
    # Convert back to PIL's Image
    transparent = Image.fromarray(transparent)
    # Paste the resized logo on the image that we're creating
    im.paste(
        transparent,
        (
            int((logo.size[0] - transparent.size[0]) / 2 + 3 * cos(17*pi*fact)),
            int((logo.size[1] - transparent.size[1]) / 2 + 2 * sin(17*pi*fact))
        ), transparent)

# Remove the unneeded variables to release the memory
del resized, transparent

im
Out[7]:

It is, of course, possible to draw on the images. Let us draw something on the one that we have just created:

In [8]:
from PIL import ImageDraw

draw = ImageDraw.Draw(im, "RGBA")
r = 31
for x in (43, im.size[0]-43):
    for y in (43, im.size[1]-43):
        draw.ellipse((x-r+1, y-r+1, x+r-1, y+r-1), fill=(255, 255, 200, 127), outline=(0,0,0))
        draw.ellipse((x-r, y-r, x+r, y+r), outline=(0,0,0))
        draw.arc((x-r//2, y-r//2, x+r//2, y+r//2), 30, 150, fill=(255,0,0))
        draw.ellipse((x-r//3-3, y-r//3-5, x-r//3+3, y-r//3+5), fill=(0, 0, 180, 230))
        draw.ellipse((x+r//3-3, y-r//3-5, x+r//3+3, y-r//3+5), fill=(0, 0, 180, 230))

im
Out[8]:

We can also draw new images from scratch:

In [9]:
from itertools import takewhile, count

r = 300
n = int(input("Number of points (pick a prime number): ")) # Try 53
l = int(input("Number of layers: ")) # Try 11
col = 255//l
im = Image.new("RGBA", (2*r, 2*r))
draw = ImageDraw.Draw(im)

phi = 2*pi / n
for d in range(l+1):
    draw.polygon([
        (r * (1+cos(k*phi)), r * (1+sin(k*phi)))
            for k in takewhile(lambda t: t == 0 or t % n, (v*(n//2-d) for v in count()))
    ], outline=(3*col*d//4,0,255-col*d, 255-col*d//5))

im
Number of points (pick a prime number): 53
Number of layers: 11
Out[9]:

While the above code may seem overly complicated, the basic syntax of the polygon function is simple:

draw.polygon([ (x1, y1), (x2, y2),... ], options)

or

draw.polygon([ x1, y1, x2, y2,... ], options)

where options can be outline (the color of the polygon's outline) and fill (the color of the polygon's fill).

Such an image can be modified further, for example by using filters:

In [10]:
from PIL import ImageFilter

im_blurred = im.filter(ImageFilter.BLUR).resize((im.size[0]//2, im.size[1]//2), Image.ANTIALIAS)
im_blurred
Out[10]:

PIL also allows us to invert the colors:

In [11]:
from PIL import ImageOps

resized = im.resize((im.size[0]//2, im.size[1]//2), Image.ANTIALIAS)
inverted = ImageOps.invert(resized.convert("RGB"))
inverted
Out[11]: