Image Processing with Python and Pillow

Image processing with Python and Pillow

Image processing and computer vision are things I always return to when I have some spare time. Even though I did some work with AI and image manipulation, I still do a lot of “manual” image processing to understand better the techniques we can use to make computers gather information from images.

After all, image processing is why I started blogging in the first place, back when one of my first articles was “Finding lanes without deep learning ”.

Those were fascinating topics (at least for me). Still, I never really wrote about other aspects of image manipulation, like touching, resizing, cropping, and different kinds of operations on an image. Even more so, generating images completely with Python, which is a fantastic idea with many practical applications, e.g., Generating thumbnails for blog posts or building a service like Canva .

Multiple libraries are available for image processing with Python, but today we will work with Pillow because of its simplicity and focus on the tasks we want to perform.

Installation and Project Setup

Installing Pillow as part of your project is as simple as installing a Python package. You need to run:

python3 -m pip install --upgrade Pillow

For our project, we will run all the commands in a Jupyter notebook on Google Colab . You can follow along in your own notebook, or I’m providing the complete code here .

Since we will be installing the dependency in Google Colab, we need to run:

!python3 -m pip install --upgrade Pillow

Last, before we move to code, we need a sample image to work. I selected this photo by Jordan Whitt on Unsplash , but you can use any other.

Image of a kid splashing in the water, just what my kid loves to do.

Image of a kid splashing in the water, just what my kid loves to do.

Since we will be working with this image throughout the tutorial, we will store a copy of it on Google Colab. Even if it is possible and easy to load an image from the network, we will load it from the system, in this case, from the Google Colab environment.

It is essential to consider that all uploaded files into Google Colab will be deleted when the environment is destroyed. For this reason, we will include the uploading of the image directly in the notebook. That way, wthen we run the code, we can provide the image we want to work with, and it will always be available when we need it.

To load an image into Google Colab, we will insert the following code:

from google.colab import files
uploaded = files.upload()

original_image_name = ''
if len(uploaded.keys()) > 0:
 original_image_name = next(iter(uploaded.keys()))
 print('We will be working with the image: {}'.format(original_image_name))
 raise ValueError('Please upload an image to continue')

The google.colab library abstracts all the logic to upload the file. We only have to consider if the user has effectively uploaded a file or not. If not, raise an error or alert the user, as it will not be possible to continue with the rest of the code.

The Image Object

At the center of the stage is the Image class, which represents a PIL image. All operations to an image will start from an Image object. There are multiple ways to load an image into an object instance. Some of them are: loading images from a file, creating new images as a result of an operation, etc.

To load an image from a file, we use the open() method in the Image module, passing it the image’s path as follows:

from PIL import Image

original_image =

To see the loaded image, we can display it in the cell with the following line of code:


Note that if you are not in a notebook environment, you may want to use instead:

But that’s not all the information the Image object contains. You can in addition get some information about the image like:

# Output the file format
# Output the pixel format, e.g. RGB
# Image size
# Output the color palette, if any
# A dictionary with data associated with the image

For our image, we get the following output:

(1920, 1282)
{'jfif': 257, 'jfif_version': (1, 1), 'dpi': (72, 72), 'jfif_unit': 1, 'jfif_density': (72, 72), 'progressive': 1, 'progression': 1, 'icc_profile': b'\x00\x00\x02\ ... \xff\xff'}

There’s a lot more you can do with the image object, and you can learn more about it on the official documentation .

Now that we loaded the image, we can start making changes with it.

Resizing Images

When talking about image manipulation, the most common of all is resizing images with Python. I’ve had that question many times on the blog, and it is also all over the place on Stack Overflow, yet it is a one liner with Pillow.

resized_image = original_image.resize((240, 160))

The resulting image of 240x160

The resulting image of 240x160

On the example above, we deliberately resized the image maintaining the aspect ratio, but that’s not necessarty required as we can see in the example below.

square_image = original_image.resize((240, 240))

The resulting image of 240x240

The resulting image of 240x240

In this example, the resulting image is a square of 240x240, and the aspect ratio of the image is altered, thus the image looks a bit strange.

But, what if we want to resize the image maintaining the aspect ratio without having to manually calculate the new width and height? The thumbnail() method has you covered.

thumbnail_image = original_image.copy()
thumbnail_image.thumbnail((240, 240))

The resulting image of 240x160

The resulting image of 240x160

The method thumbnail() works differently from the method resize as the first would modify the object inplace, instead of returning a new instance of the image. For this reason, we start the code sample by making a copy of the original image, and then calling thumbnail() method.

The method will generate a thumbnail of the original image, with the same aspect ratio, and with width and height no longer than the given size.


The method crop() returns a rectangular region from the source image. The rectangle, or box, is defined as a 4-touple for the coordinates left, upper, right and lower.

Let’s see an example:

box = (
   720, # left
   600, # top
   1300, # right
   980 # bottom
cropped_image = original_image.crop(box)

Cropped image showing the feet only

Cropped image showing the feet only

The resulting image contains a small section of the original image, showing only the shoes of the kid.

Stacking Images

By stacking images we mean to insert an image into another, or to paste() an image into another. Doing that with Pillow is very easy, but you will need at least two images to showcase it.

In the next example, we will take our original image and paste the cropped version (just the feet) into the bottom right corner.

stacked_image = original_image.copy()
position = (stacked_image.width - cropped_image.width, stacked_image.height - cropped_image.height)
stacked_image.paste(cropped_image, position)

The function paste() alters the image object in place instead of returning a new instance, so we start by making a copy of the original image, calculating the position for the cropped image and pasting it into the stacked image.

Here is what the end result looks like:

Original image, with the cropped image stacked on the bottom right

Original image, with the cropped image stacked on the bottom right

Cool, right?

Rotating Images

Pillow also offers the function rotate() to, well, rotate images. The method takes a numeric argument that represents the degrees of rotation. This method returns a new copy of the image and does not alter the original one.

rotated_image = original_image.rotate(90)

Here is the resulting image:

Image rotated by 90 degrees

Image rotated by 90 degrees

Note that by default the rotated image keeps the dimensions of the original image. Since the image rotates in the original frame, it could lead to situations where you see a black borders, and the image gets cropped.

If you would like to change this behavior, you can use the parameter expand set to True as follows:

rotated_expanded_image = original_image.rotate(90, expand=True)

The resulting image looks like:

image rotated and expanded to fit

image rotated and expanded to fit

Transposing Images, E.g. Flipping

Similar to rotation, Pillow allows you to transpose images using the transpose() method. The same takes one argument that represents the type of transpose you want to apply, here are the possible constant values:

  • PIL.Image.Transpose.FLIP_LEFT_RIGHT
  • PIL.Image.Transpose.FLIP_TOP_BOTTOM
  • PIL.Image.Transpose.ROTATE_90
  • PIL.Image.Transpose.ROTATE_180
  • PIL.Image.Transpose.ROTATE_270
  • PIL.Image.Transpose.TRANSPOSE
  • PIL.Image.Transpose.TRANSVERSE

With transpose you can either flip or rotate an image, though the rotation angles are already predefined, so if you want to use a different set of angles, it is recommended to use the rotate() function we covered above.

This method returns a new copy of the image and does not alter the original one.

Let’s see some examples:

flipped_lr_image = original_image.transpose(Image.FLIP_LEFT_RIGHT)

The resulting image is:

Image flipped on the Y axis

Image flipped on the Y axis

Another example:

flipped_lr_image = original_image.transpose(Image.FLIP_TOP_BOTTOM)

The resulting image is:

Image flipped on the X axis

Image flipped on the X axis

Drawing on Images

Pillow not only offers methods to transform images, but you can also draw on images using the ImageDraw module.

The ImageDraw modules allows you to draw lines, rectangles, arcs, any other type of irregular shapes, text, multiline text and much more.

Let’s see an example of loading an image, and drawing some random stuff on it:

from PIL import Image, ImageDraw, ImageFont
art_image = original_image.copy()
draw = ImageDraw.Draw(art_image)
draw.line((0, 0, art_image.width, art_image.height), fill=(0, 0, 255), width=20) # draw a blue diagonal
draw.rectangle((0, 0, 250, 250), fill=(255, 0, 0), outline='green') # draw a rectangle diagonal
font = ImageFont.truetype('/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf', size=70)
draw.text((512, 512), 'Hello world!', fill='white', font=font) # write text

The resulting image is:

Original image with a square, a line and some text that reads “Hello world!”

Original image with a square, a line and some text that reads “Hello world!”

Color Transformations

In other articles, like finding lanes without deep learning we covered the importance of working in different color spaces and applying color transformations on images to perform calculations and distinguish patterns and other things. To switch an image color mode we use the function convert(), which expectas a parameter with the color mode. For grayscale we use a rather specific parameter L, which stands for luminance. You can also apply RGB or CMYK modes.

grayscale_image = original_image.convert('L')

Here is the resulting image:

Grayscale version of the image

Grayscale version of the image

Blur an Image

Pillow has a powerful filtering system, among of which we have multiple options to blur images.

Let’s work an example using a Gaussian blur filter:

from PIL import ImageFilter
blurred_image = original_image.filter(filter=ImageFilter.GaussianBlur(20))

The resulting image:

Blurred image using the Gaussian filter

Blurred image using the Gaussian filter

Check the ImageFilter module to learn more about filters and different blur modes.

Saving Images

The last method we are covering today is the save() method, which allows you to save the image object back to a file.

It is simple, so let’s see it in an example:'blurred_image.png') # save as PNG'blurred_image.jpg') # save as JPG

The example above will save two images to disk, a PNG and a JPG copy. In case you are working with Google Colab, you will find the images stored in the content folder. If you are working with a local file, you will find them in the current directory.


In this article, we covered some common image processing operations using Python and the Pillow library.

Even though we performed multiple operations, and we had a lot of fun, we haven’t even scratched the surface of the tons of options Pillow offers.

If you are interested in image processing I highly recommend that you check Pillow’s official documentation .

Thanks for reading and happy coding!

If you liked what you saw, please support my work!

Juan Cruz Martinez - Author @ Live Code Stream

Juan Cruz Martinez

Juan has made it his mission to help aspiring developers unlock their full potential. With over two decades of hands-on programming experience, he understands the challenges and rewards of learning to code. By providing accessible and engaging educational content, Juan has cultivated a community of learners who share their passion for coding. Leveraging his expertise and empathetic teaching approach, Juan has successfully guided countless students on their journey to becoming skilled developers, transforming lives through the power of technology.