Python 101 - How to Work with Images

The Python Imaging Library (PIL) is a 3rd party Python package that adds image processing capabilities to your Python interpreter. It allows you to process photos and do many common image file manipulations. The current version of this software is in Pillow, which is a fork of the original PIL to support Python 3. Several other Python packages, such as wxPython and ReportLab, use Pillow to support loading many different image file types. You can use Pillow for several use cases including the following:

  • Image processing
  • Image archiving
  • Batch processing
  • Image display via Tkinter

In this articla, you will learn how to do the following with Pillow:

  • Opening Images
  • Cropping Images
  • Using Filters
  • Adding Borders
  • Resizing Images

As you can see, Pillow can be used for many types of image processing. The images used in this article are some that the author has taken himself. They are included with the code examples on Github. See the introduction for more details.

Now let's get started by installing Pillow!

Installing Pillow

Installing Pillow is easy to do with pip. Here is how you would do it after opening a terminal or console window:

python -m pip install pillow

Now that Pillow is installed, you are ready to start using it!

Opening Images

Pillow let's you open and view many different file types. For a full listing of the image file types that Pillow supports, see the following:

You can use Pillow to open and view any of the file types mentioned in the "fully supported formats" section at the link above. The viewer is made with Tkinter and works in much the same way as Matplotlib does when it shows a graph.

To see how this works, create a new file named open_image.py and enter the following code:

# open_image.py

from PIL import Image

image = Image.open('jellyfish.jpg')
image.show()

Here you import Image from the PIL package. Then you use Image.open() to open up an image. This will return an PIL.JpegImagePlugin.JpegImageFile object that you can use to learn more about your image. When you run this code, you will see a window similar to the following:

Loading an Image with Pillow

This is pretty handy because now you can view your images with Python without writing an entire graphical user interface. You can use Pillow to learn more about your images as well. Create a new file named get_image_info.py and add this code to it:

# get_image_info.py

from PIL import Image

def get_image_info(path):
    image = Image.open(path)
    print(f'This image is {image.width} x {image.height}')
    exif = image._getexif()
    print(exif)

if __name__ == '__main__':
    get_image_info('ducks.jpg')

Here you get the width and height of the image using the image object. Then you use the _getexif() method to get metadata about your image. EXIF stands for "Exchangeable image file format" and is a standard that specifies the formats for images, sound, and ancillary tags used by digital cameras. The output is pretty verbose, but you can learn from that data that this particular photo was taken with a Sony 6300 camera with the following settings: "E 18-200mm F3.5-6.3 OSS LE". The timestamp for the photo is also in the Exif information.

However, the Exif data can be altered if you use photo editing software to crop, apply filters or do other types of image manipulation. This can remove part or all of the Exif data. Try running this function on some of your own photos and see what kinds of information you can extract!

Another fun bit of information that you can extract from the image is its histogram data. The histogram of an image is a graphical representation of its tonal values. It shows you the brightness of the photo as a list of values that you could graph. Let's use this image as an example:

Butterfly

To get the histogram from this image you will use the image's histogram() method. Then you will use Matplotlib to graph it out. To see one way that you could do that, create a new file named get_histrogram.py and add this code to it:

# get_histrogram.py

import matplotlib.pyplot as plt

from PIL import Image

def get_image_histrogram(path):
    image = Image.open(path)
    histogram = image.histogram()
    plt.hist(histogram, bins=len(histogram))
    plt.xlabel('Histogram')
    plt.show()

if __name__ == '__main__':
    get_image_histrogram('butterfly.jpg')

When you run this code, you open the image as before. Then you extract the histogram from it and pass the list of values to your Matplotlib object where you call the hist() function. The hist() function takes in the list of values and the number of equal-width bins in the range of values.

When you run this code, you will see the following graph:

Butterfly histogram

This graph shows you the tonal values in the image that were mentioned earlier. You can try passing in some of the other images included on Github to see different graphs or swap in some of your own images to see their histograms.

Now let's discover how you can use Pillow to crop images!

Cropping Images

When you are taking photographs, all too often the subject of the photo will move or you didn't zoom in far enough. This results in a photo where the focus of the image isn't really front-and-center. To fix this issue, you can crop the image to that part of the image that you want to highlight.

Pillow has this functionality built-in. To see how it works, create a file named cropping.py and add the following code to it:

# cropping.py

from PIL import Image

def crop_image(path, cropped_path):
    image = Image.open(path)
    cropped = image.crop((40, 590, 979, 1500))
    cropped.save(cropped_path)

if __name__ == '__main__':
    crop_image('ducks.jpg', 'ducks_cropped.jpg')

The crop_image() function takes in the path of the file that you wish to crop as well as the path to the new cropped file. You then open() the file as before and call crop(). This method takes the beginning and ending x/y coordinates that you are using to crop with. You are creating a box that is used for cropping.

Let's take this fun photo of ducks and try cropping it with the code above:

Ducklings

Now when you run the code against this, you will end up with the following cropped image:

Cropped ducklings

The coordinates you use to crop with will vary with the photo. In fact, you should probably change this code so that it accepts the crop coordinates as arguments. You can do that yourself as a little homework. It takes some trial and error to figure out the crop bounding box to use. You can use a tool like Gimp to help you by drawing a bounding box with Gimp and noting the coordinates it gives you to try with Pillow.

Now let's move on and learn about applying filters to your images!

Using Filters

The Pillow package has several filters that you can apply to your images. These are the current filters that are supported:

  • BLUR
  • CONTOUR
  • DETAIL
  • EDGE_ENHANCE
  • EDGE_ENHANCE_MORE
  • EMBOSS
  • FIND_EDGES
  • SHARPEN
  • SMOOTH
  • SMOOTH_MORE

Let's use the butterfly image from earlier to test out a couple of these filters. Here is the image you will be using:

Butterfly

Now that you have an image to use, go ahead and create a new file named blur.py and add this code to it to try out Pillow's BLUR filter:

# blur.py

from PIL import Image
from PIL import ImageFilter

def blur(path, modified_photo):
    image = Image.open(path)
    blurred_image = image.filter(ImageFilter.BLUR)
    blurred_image.save(modified_photo)

if __name__ == '__main__':
    blur('butterfly.jpg', 'butterfly_blurred.jpg')

To actually use a filter in Pillow, you need to import ImageFilter. Then you pass in the specific filter that you want to use to the filter() method. When you call filter(), it will return a new image object. You then save that file to disk.

This is the image that you will get when you run the code:

Blurred butterfly

That looks kind of blurry, so you can count this as a success! If you want it to be even blurrier, you could run the blurry photo back through your script a second time.

Of course, sometimes you take photos that are slightly blurry and you want to sharpen them up a bit. Pillow includes that as a filter you can apply as well. Create a new file named sharpen.py and add this code:

# sharpen.py

from PIL import Image
from PIL import ImageFilter

def sharpen(path, modified_photo):
    image = Image.open(path)
    sharpened_image = image.filter(ImageFilter.SHARPEN)
    sharpened_image.save(modified_photo)

if __name__ == '__main__':
    sharpen('butterfly.jpg', 'butterfly_sharper.jpg')

Here you take the original butterfly photo and apply the SHARPEN filter to it before saving it off. When you run this code, your result will look like this:

Sharpened butterfly

Depending on your eyesight and your monitor's quality, you may or may not see much difference here. However, you can rest assured that it is slightly sharper.

Now let's find out how you can add borders to your images!

Adding Borders

One way to make your photos look more professional is to add borders to them. Pillow makes this pretty easy to do via their ImageOps module. But before you can do any borders, you need an image. Here is the one you'll be using:

Grey butterfly

Now that you have a nice image to play around with, go ahead and create a file named border.py and put this code into it:

# border.py

from PIL import Image, ImageOps


def add_border(input_image, output_image, border):
    img = Image.open(input_image)

    if isinstance(border, int) or isinstance(border, tuple):
        bimg = ImageOps.expand(img, border=border)
    else:
        raise RuntimeError('Border is not an integer or tuple!')

    bimg.save(output_image)

if __name__ == '__main__':
    in_img = 'butterfly_grey.jpg'

    add_border(in_img, output_image='butterfly_border.jpg',
             border=100)

The add_border() function takes in 3 arguments:

  • input_image - the image you want to add a border to
  • output_image - the image with the new border applied
  • border - the amount of border to apply in pixels

In this code, you tell Pillow that you want to add a 100 pixel border to the photo that you pass in. When you pass in an integer, that integer is used for the border on all four sides. The default color of the border is black. The key method here is expand(), which takes in the image object and the border amount.

When you run this code, you will end up with this lovely result:

Butterfly with black border

You can pass in a tuple of values to make the border different widths. For example, if you passed in (10, 50), that would add a 10-pixel border on the left and right sides of the images and a 50-pixel border to the top and bottom. Try doing that with the code above and re-running it. If you do, you'll get the following:

Butterfly with altered border

Isn't that nice? If you want to get really fancy, you can pass in different values for all four sides of the image. But there probably aren't very many use-cases where that makes sense.

Having a black border is nice and all, but sometimes you'll want to add a little pizazz to your picture. You can change that border color by passing in the fill argument to expand(). This argument takes in a named color or an RGB color.

Create a new file named colored_border.py and add this code to it:

# colored_border.py

from PIL import Image, ImageOps

def add_border(input_image, output_image, border, color=0):
    img = Image.open(input_image)

    if isinstance(border, int) or isinstance(
        border, tuple):
        bimg = ImageOps.expand(img,
                               border=border, 
                               fill=color)
    else:
        msg = 'Border is not an integer or tuple!'
        raise RuntimeError(msg)

    bimg.save(output_image)

if __name__ == '__main__':
    in_img = 'butterfly_grey.jpg'

    add_border(in_img,
               output_image='butterfly_border_red.jpg',
               border=100,
               color='indianred')

Now your add_border() function takes in a color argument, which you pass on to the expand() method. When you run this code, you'll see this for your result:

Butterfly with color border

That looks pretty nice. You can experiment around with different colors or apply your own favorite color as the border.

The next item on your Pillow tour is to learn how to resize images!

Resizing Images

Resizing images with Pillow is fairly simple. You will be using the resize() method which takes in a tuple of integers that are used to resize the image. To see how this works, you'll be using this lovely shot of a lizard:

Lizard

Now that you have a photo, go ahead and create a new file named resize_image.py and put this code in it:

# resize_image.py

from PIL import Image

def resize_image(input_image_path, output_image_path, size):
    original_image = Image.open(input_image_path)
    width, height = original_image.size
    print(f'The original image size is {width} wide x {height} '
          f'high')

    resized_image = original_image.resize(size)
    width, height = resized_image.size
    print(f'The resized image size is {width} wide x {height} '
          f'high')
    resized_image.show()
    resized_image.save(output_image_path)

if __name__ == '__main__':
    resize_image(
            input_image_path='lizard.jpg',
            output_image_path='lizard_small.jpg',
            size=(800, 400),
            )

Here you pass in the lizard photo and tell Pillow to resize it to 600 x 400. When you run this code, the output will tell you that the original photo was 1191 x 1141 pixels before it resizes it for you.

The result of running this code looks like this:

Resized lizard

Well, that looks a bit odd! Pillow doesn't actually do any scaling when it resizes the image. Instead, Pillow will stretch or contort your image to fit the values you tell it to use.

What you want to do is scale the image. To make that work, you need to create a new file named scale_image.py and add some new code to it. Here's the code you need:

# scale_image.py

from PIL import Image

def scale_image(
            input_image_path,
            output_image_path,
            width=None,
            height=None
    ):
    original_image = Image.open(input_image_path)
    w, h = original_image.size
    print(f'The original image size is {w} wide x {h} '
          'high')

    if width and height:
        max_size = (width, height)
    elif width:
        max_size = (width, h)
    elif height:
        max_size = (w, height)
    else:
        # No width or height specified
        raise ValueError('Width or height required!')
 
    original_image.thumbnail(max_size, Image.ANTIALIAS)
    original_image.save(output_image_path)

    scaled_image = Image.open(output_image_path)
    width, height = scaled_image.size
    print(f'The scaled image size is {width} wide x {height} '
          'high')


if __name__ == '__main__':
    scale_image(
            input_image_path='lizard.jpg',
            output_image_path='lizard_scaled.jpg',
            width=800,
            )

This time around, you let the user specify both the width and height. If the user specifies a width, a height, or both, then the conditional statement uses that information to create a max_size. Once it has the max_size value calculated, you pass that to thumbnail() and save the result. If the user specifies both values, thumbnail() will maintain the aspect ratio correctly when resizing.

When you run this code, you will find that the result is a smaller version of the original image and that it now maintains its aspect ratio.

Wrapping Up

Pillow is very useful for working with images using Python. In this article, you learned how to do the following:

  • Open Images
  • Crop Images
  • Use Filters
  • Add Borders
  • Resize Images

You can do much more with Pillow than what is shown here. For example, you can do various image enhancements, like changing the contrast or brightness of an image. Or you could composite multiple images together. There are many other applications that you can use Pillow for. You should definitely check the package out and read its documentation to learn more.

Related Reading

This article is based on a chapter from Python 101, 2nd Edition, which you can purchase on Leanpub or Amazon.

If you'd like to learn more Python, then check out these tutorials:

Copyright © 2021 Mouse Vs Python | Powered by Pythonlibrary