Back to articles list Articles
8 minutes read

Pillow: Introduction to Basic Picture Manipulation in Python

In this article, we provide an introduction to the Python Pillow module. It is useful to have some experience in image processing, as it is a foundation for various applications: post-processing photographs automatically, generating thumbnails in Python for online content, and pre-processing images for machine learning, among others.

The Python Pillow module is a fork of the Python Image Library (PIL). Pillow needs to be installed by the user. The easiest way to do this is with pip. For more background information, tutorials, or references for the functionality, see the official documentation.

For those of you who are new to programming, this track is a good place to get started. It assumes no prior knowledge in programming or IT. If you’re a regular reader of this blog, you may notice a few changes designed to make learning Python even easier. Check out this article for an overview of the changes.

Opening Images

There are several image formats you can work with using the Python Pillow module. You’re probably most familiar with raster image formats such as JPG, PNG, and GIF, among others.

Raster images have a fixed number of pixels depending on the image resolution, and each pixel has a defined color. If you zoom in on a raster image far enough, the pixels become more apparent. The vast majority of images are stored in this way.

Vector images, on the other hand, use curves defined by mathematical equations to create images. You can keep zooming into a vector image, and the curves remain smooth. Two examples of this file format are SVG and EPS.

However, working with vector-based images in Python can be a little tricky, as it involves using other specialized libraries. For simplicity, we limit our discussion to the familiar raster image format.

To open and display an image using the Python Pillow module, import the Image module and open the image as follows:

>>> from PIL import Image
>>> im = Image.open('image.png', mode='r')
>>> im.show()

The function returns an Image object, which you can start to analyze and modify. The optional keyword mode defines whether the image is opened in read or write mode. The second optional keyword formats defines a list or tuple of formats to try to load the file in.

The Python Pillow module supports over 30 different raster file types for reading. However, the support for writing files is less extensive. If you’re working with JPG, for example, the procedure to open a file is the same as the above. To display the image, you can use the show() method on the Image object. This displays the image in a separate window and is mostly useful for debugging purposes.

The object im has several methods that provide information about an image. The format, mode, and size methods provide some key information about your image. Try them out to see what information they return. These methods come in handy later. You can also find the resolution of an image using the info method, which returns a dictionary containing the key 'dpi'.

Modifying Images

The strength of the Python Pillow module is its usefulness in modifying images. Many image pre- and post-processing functions are included. Below, we take a look at some of the more useful ones.

A good starting point is to know the size of your image. To do this, simply call the size method on an Image object, which returns a tuple with the width and height of the image in pixels.

You can automatically generate thumbnails in Python by using the thumbnail() method, which is useful if you’re in the business of producing online content. It takes a required argument size – a tuple of (width, height) and an optional argument resample. To see a nice example, including how to do some error handling, check out the tutorials page in the documentation.

If you want to prepare a large number of photos for printing, it’s useful to convert them all to a standard aspect ratio. The aspect ratio changes the composition of a photo and how it’s perceived. An aspect ratio of 1:1 is good for profile pictures, and aspect ratios of 3:2 or 5:4 are common in photography and art prints.

By the way, if you need more background on the automatic handling of large numbers of files in Python, take a look at this article.

To change the aspect ratio of your pictures, you may try the resize method, which requires you to specify the new height and width dimensions in pixels. However, this distorts the image if a different aspect ratio is used.

Cropping images in Python is better. To demonstrate this, we first need a photo of a cute baby goat. We assume it is named 'goat.jpg' and saved to your working directory:

	>>> from PIL import Image
	>>> im = Image.open('goat.jgp')
	>>> im.show()

As stated before, this opens the following image in a new window.

Pillow

Using the size method on the Image object, we find the image has a size of (1124, 750), giving an aspect ratio of 3:2. We can change it to an aspect ratio of 1:1 as follows:

>>> height, width = 500, 500
>>> left, upper, right, lower = 60, 200, width+60, height+200
>>> cropped_image = im.crop((left, upper, right, lower))
>>> cropped_image.show()

The above code produces the following image, nicely framing this adorable little guy in the center.

Pillow

In the code above, we define the variables left, upper, right, and lower, which specify the pixel coordinates of the region to crop. Notice we had to manually define this so that the goat is framed nicely. That said, it’s possible to automate this by defining a point in the image and cropping around that.

The Python Pillow library comes with many pre-programmed functions that help you bring out the best in your images. These include functions to convert an image to grayscale and functions to adjust the brightness, contrast, and sharpness, among others. These are included in the ImageOps and ImageEnhance modules of the Python Pillow library.

Let’s apply a few of these functions to our image object cropped_image we have defined above. We convert the image to grayscale and enhance the sharpness by a factor of 1.2:

>>> from PIL import ImageOps, ImageEnhance
>>> im_gray = ImageOps.grayscale(cropped_image)
>>> im_sharp = ImageEnhance.Sharpness(im_gray).enhance(1.2)
>>> im_sharp.show()
Pillow

Another useful set of tools is contained in the ImageFilter module. Here, you find a few helpful image processing functions if you’re interested in machine learning with image data.

As we’ve said before, Python is great for machine learning projects. For example, if you’re programming an object detection application, using EDGE_ENHANCE or FIND_EDGES on your input images may help increase the accuracy of your application. Check out the documentation if you’re interested in getting more information on these filters.

More Advanced Python Image Processing

When we load our image using the Python Pillow module, the individual pixel values are stored in a data structure. This means we can manipulate our image pixel by pixel, which opens up a whole range of interesting possibilities such as creating custom filters.

We can access the pixel values for our cropped image as follows:

>>> pixels = list(cropped_image.getdata())

The get_data method returns a sequence object flattened to contain the pixel values one after the other. The variable pixels is a list of tuples, and each tuple contains the RGB values for each pixel. The method contains an optional argument, band, which allows you to return a single band of an RGB image by providing an index: 0 for the 'R' band, 1 for the 'G' band, and 2 for the 'B' band. The length of the pixels list is 250.000, which corresponds to its size of 500 x 500 (height x width).

Let’s say we want to create a custom filter by modifying the pixel values. To make what we’re doing here a little clearer, we separate out the channels using a list comprehension and recast them as arrays using NumPy:

>>> import numpy as np
>>> input_R = np.array([pix[0] for pix in pixels])
>>> input_G = np.array([pix[1] for pix in pixels])
>>> input_B = np.array([pix[2] for pix in pixels])

Now, to create a filter, we modify the channels as follows:

>>> output_R = (input_R*0.6358) + (input_G*0.4614) + (input_B*0.1134)
>>> output_G = (input_R*0.2093) + (input_G*0.8116) + (input_B*0.1008)
>>> output_B = (input_R*0.1324) + (input_G*0.3204) + (input_B*0.4786)

Let’s put the output arrays together and make sure the result has the correct shape (height, width, channels):

>>> new_pixels = np.array([output_R, output_G, output_B]).T.reshape(500, 500, 3)

In RGB mode, each color channel is represented by an integer between 0 and 255. We need to limit the pixel values to this range then convert the array elements to the correct data type:

>>> new_pixels[new_pixels>255]=255
>>> new_pixels = new_pixels.astype(np.uint8)

The last step is to convert our array of pixel values into an Image object and take a look at our hard work:

>>> new_image = Image.fromarray(np.array(new_pixels))
>>> new_image.show()
Pillow

Where Do I Go From Here?

There’s more to Pillow than we can cover in this article. We’d like to encourage you to take what you’ve learned here and start experimenting with your own images. Perhaps you can come up with your own image filter or automate the post-processing of your photographs.

As we mentioned, aspiring data scientists should use this as a foundation to start exploring image classification or object detection. Good luck!