Pillow
Pillow
Michael Driscoll
This book is for sale at http://leanpub.com/pillow
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Who is this book for? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
About the Author . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Requirements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Book Source Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Reader Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Errata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Cover Art . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Chapter 2 - Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Understanding Color . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Using Pillow to Get RGB Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Getting Colors from Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Changing Pixel Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Converting to Black and White . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Creating 4-Color Photos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Creating a Sepia Photo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Creating an Image Converter GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
CONTENTS
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Chapter 4 - Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
The BLUR Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
The CONTOUR Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
The DETAIL Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
The EDGE_ENHANCE Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
The EMBOSS Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
The FIND_EDGES Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
The SHARPEN Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
The SMOOTH Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
RankFilters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
MultiBand Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
The BoxBlur Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
The GaussianBlur Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
The Color3DLUT Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
The UnsharpMask Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Using Filters in a GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Afterword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
About the Technical Reviewers
Ethan Furman
Ethan, a largely self-taught programmer, discovered Python around the turn of the century, but
wasn’t able to explore it for nearly a decade. When he finally did, he fell in love with its simple
syntax, lack of boiler-plate, and the ease with which one can express one’s ideas in code. After
writing a dbf library to aid in switching his company’s code over to Python, he authored PEP 409,
wrote the Enum implementation for PEP 435, and authored PEP 461. He was invited to be a core
developer after PEP 435, which he happily accepted.
He thanks his mother for his love of language, stories, and the written word.
Alessia Marcolini
Alessia is a Data Science EIT Digital student at the Eindhoven University of Technology. She
is a Junior Data Scientist at HK3lab, working on machine learning/deep learning frameworks
to integrate multiple medical imaging modalities and different clinical data to get more precise
prognostic/diagnostic biomarkers for human and veterinary health.
She has been a volunteer of the Italian Python Community since 2017, helping with the organization
of PyCon Italy (the national Python Conference, hosting 700+ international delegates each year). In
2018, she also joined the organization committee of EuroSciPy, the European Conference for Python
in Science.
When not coding, she loves dancing and drinking black tea and good gin.
Acknowledgments
Writing a book takes a huge time commitment. Fortunately, I had a couple of very helpful people right
out the gate. Ethan Furman joined me for this book once more as a technical reviewer and editor. I
also had Alessia Marcolini helping out as a technical reviewer and giving me lots of feedback. Thank
you so much!
Mike Barnett (creator of PySimpleGUI) encouraged me to write a book on Pillow and has been
helpful in feedback on the GUI examples in the book. Steve Barnes has also helped out with finding
little bugs or making good suggestions that have helped make the chapters better.
I’d also like to thank my beta readers:
• Jase Lindgren, an early beta reader, who helped me find some typos and provided good feedback
too.
• Ruud van der Ham also helped with some good comments about PySimpleGUI and Pillow as
well.
• Roy Neilsen who reviewed many of the chapters in the book
• Mike
Introduction
The Python programming language has thousands of packages that you can install from the Python
Package Index that you can use to enhance your code. There are many popular packages for working
with images in Python, such as scikit-image, OpenCV and NumPy. However, for this book, you
will be learning how to use the Pillow package, which is the friendly fork of the Python Imaging
Library.
The Python Imaging Library adds image processing capabilities to your Python installation. Pillow
is the Python 3 version of the Python Imaging Library. According to Pillow’s documentation, it
provides you with “extensive file format support, an efficient internal representation, and fairly
powerful image processing capabilities.”
By the end of this book, you will be able to do all of the following tasks:
• Open images
• Extract histrogram data
• Extract image metadata
• Apply image transforms
• Add filters
• Crop images
• Enhance images
• Combine images
• Basic drawings
• and more!
Pillow is a powerful library that you can use for general purpose image processing. If you need to
do pixel-by-pixel operations where you need to examine or change pixels, you will find that Pillow
is relatively slow. You may want to look at a library like NumPy instead for that use case.
You will also be using the PySimpleGUI toolkit to build little applications in combination with Pillow.
The PySimpleGUI portions of the books are completely optional, but are helpful in demonstrating
the concepts you will be learning about. You can skip them entirely if you want to.
Conventions
There aren’t a lot of special conventions in this book. The main one you need to be aware of is that
code blocks will look like this:
1 import PIL
2
3 # do something fun!
Requirements
This book is written for Python 3. The examples may work in Python 2, but are not tested in Python
2.
You will need Pillow to be able to use the examples in this book. Installing Pillow can be done using
pip:
Linux users can also install Pillow by using their Linux package manager and installing python-imaging
if they prefer.
If you want to install Pillow from source, you should refer to the documentation³ for the latest
instructions.
If you want to be able to use the PySimpleGUI examples in this book, then you will need to install
it as well using the following command:
¹https://www.blog.pythonlibrary.org/
²https://www.blog.pythonlibrary.org/books/
³https://pillow.readthedocs.io/en/stable/installation.html
Introduction 5
Note: PySimpleGUI is licensed with the LGPLv3 license, which requires you to keep the library open
source. If you plan to use PySimpleGUI, make sure you understand the restrictions that LGPLv3 has
in regards to commercial use.
PySimpleGUI uses Tkinter by default. You can use PySimpleGUI with wxPython or PyQt simply by
changing the import. For this book, you will use the default PySimpleGUI.
On Ubuntu, you may need to install python3-tk as PySimpleGUI depends on Tkinter which isn’t
always included in the system Python installation.
Anaconda Users
If you are an Anaconda user that installed the full version of Anaconda, you may already
have Pillow installed as it comes with Anaconda. You can check to see if it is installed by
running the following command in your terminal or console:
python3 -c "import PIL;print('PIL:', PIL.__version__);"
• https://github.com/driscollis/image_processing_with_python
Reader Feedback
I welcome feedback about my writings. If you’d like to let me know what you thought of the book,
you can send comments to the following address:
• [email protected]
Introduction 6
Errata
I try my best not to publish errors in my writings, but it happens from time to time. If you happen
to see an error in this book, feel free to let me know by emailing me at the following:
Cover Art
The cover art was created by WolfOrDeer Collony.
Now let’s get started!
Chapter 1 - Pillow Basics
Pillow’s main module is called Image. This module is used to load photos into an object that Python
can use. It provides several factory functions that you can use to load images from files and create
new images.
You will be using the Image module in all of your Pillow code. It has many useful functions that you
can utilize for basic image manipulation.
In this chapter, you will learn how about the following topics:
• Opening Images
• Saving Images
• Changing Compression During Saving
• Reading Methods
• Creating Thumbnails
• Creating an Image Viewer
Opening Images
Pillow allows you to open many different image file types. Here are a few of the common ones that
Pillow supports:
• BMP
• GIF
• ICO
• JPEG
• PNG
• TIFF
You can see which formats your Pillow installation supports by running the following command in
your terminal:
Chapter 1 - Pillow Basics 8
1 python3 -m PIL
This will output all the image formats that Pillow supports on your operating system.
The maximum image size is limited by the file format, but is generally about 2 GB at the time of
writing. For the ICO file type, the maximum size is 256x256.
You can open and display an image to the user with three lines of code. To see how to do accomplish
this feat, open up a Python editor, create a new file named open_image.py, then add the following
code:
1 # open_image.py
2
3 from PIL import Image
4
5 image = Image.open("flowers.jpg")
6 image.show("flowers")
The first line of code is the name of the Python file as a comment. Then you have an import of the
Image module. Next, you call the open() function, which takes a file path. The open() function can
also take a mode, which must be set to “r”, and a formats argument, which is a list or tuple of file
formats to use to attempt to load the image.
Note: You can get the images used in this chapter by downloading the source code on GitHub⁴.
Most of the time, you can get away with using only the file path and letting Pillow intelligently
detect and load the correct image type.
The last line of code uses the image object’s show() method. This method is used primarily for
debugging. It will save the image off in a temporary file in a PNG format. Next, it will attempt to
display the image using the platform’s native image viewer for the PNG file type.
When you run this code, you will see something similar to the following:
⁴https://github.com/driscollis/image_processing_with_python
Chapter 1 - Pillow Basics 9
The show() function accepts a title parameter, which you can use to set the title in the image
window. Note that this only works for the viewers that support setting the title.
You can get a feel for what you can do with the image object by using Python’s dir() method to
introspect it. Load up a Python interpreter in your terminal (or cmd.exe on Windows) and run the
following commands:
You will be learning about many of these image methods and attributes throughout the rest of this
book. The items with the single or double beginning underscores are considered private in Python.
In most cases, you do not use them yourself.
You can use Pillow to learn more about the image object using a few of these attributes:
Using Pillow, you can get the size of the image as a tuple of integers. In this example, you save them
off as width and height. You can also get the filename from the image object, as well as the image’s
format and description.
Let’s move on and learn how to save an image!
Chapter 1 - Pillow Basics 11
Saving Images
Saving an image with Pillow is pretty straight-forward. If you go back to the previous section, you
will see that there is a save() method in that list of attributes and methods that you printed out.
To see how this works, you can take the flower photo from earlier and convert it from a JPG to a
PNG. Go ahead and create a new file named save_image.py and add this code to it:
1 # save_image.py
2
3 import pathlib
4
5 from PIL import Image
6
7
8 def image_converter(input_file_path, output_file_path):
9 image = Image.open(input_file_path)
10 image.save(output_file_path)
11 original_suffix = pathlib.Path(input_file_path).suffix
12 new_suffix = pathlib.Path(output_file_path).suffix
13 print(f"Converting {input_file_path} from {original_suffix} "
14 f"to {new_suffix}")
15
16 if __name__ == "__main__":
17 image_converter("flowers.jpg", "flowers.png")
Here you use Python’s built-in pathlib module to get the image suffixes from the image file names
that you pass into the image_converter() function. Inside of the function, you open up the input_-
file_path using Pillow. Then you save it using the output_file_path name.
The save() function also allows you to pass in a specific format parameter. If that is not used, then
Pillow will use the suffix of the image to programmatically determine what image format to save it
as. You will rarely use that parameter though, which is why it is not shown in this example.
After running this code, you should find a new image file with the PNG extension. When converting
from JPG to PNG, you may see the resulting image file size growing quite a bit. When I ran this
example using the provided image file, it started at 266 KB as a JPG, but the resulting PNG weighed
in at 2.6 MB, which is approximately 10 times larger!
The reason this happens is that JPG is a compressed format while PNG is a lossless format, although
PNG does support lossless compression. A PNG file has to save all the pixels, including the ones
that the JPG is compressing. What this means is that when you convert the JPG to PNG, you are
attempting to restore those missing pixels.
Chapter 1 - Pillow Basics 12
You can reduce the file size by setting the quality to something less than 100%. It also helps if you
tell Pillow to try to optimize while saving.
To see how this works, create a new file named save_image_with_compression.py and add the
following code:
1 # save_image_with_compression.py
2
3 import pathlib
4
5 from PIL import Image
6
7
8 def image_quality(input_file_path, output_file_path, quality):
9 image = Image.open(input_file_path)
Chapter 1 - Pillow Basics 13
Here you are telling Pillow to save the flowers photo using a quality of 95 and with optimize set to
True. If you run this code, the result will be 4 MB in size. Try experimenting with the quality amount
to see how small you can make the file without it effecting the quality of the photo visually.
If you want to compress a PNG file, you can pass the compress_level parameter when saving. It
supports 0-9 where 0 is no compression and 9 is best compression.
There is another way to reduce file size in high resolution photos. You’ll learn how in the next
section!
1 # save_image_with_new_dpi.py
2
3 import pathlib
4
5 from PIL import Image
6
7
8 def image_converter(input_file_path, output_file_path, dpi):
9 image = Image.open(input_file_path)
10 image.save(output_file_path, dpi=dpi)
11
12 if __name__ == "__main__":
13 image_converter("blue_flowers.jpg", "blue_flowers_dpi.jpg",
14 dpi=(72, 72))
When you want to change the DPI of a photo, the DPI setting is a tuple. In this case, you are changing
the flower photo from 240 DPI to 72 DPI. After running this code, the new file will be 1.2 MB.
Chapter 1 - Pillow Basics 14
You can combine the dpi and quality parameters to really change the file size. Another way to
change the file size would be to resize the photo itself. You will learn about that in chapter 5.
Now let’s learn more about Pillow’s reading modes!
Reading Methods
Pillow supports reading image data in several other ways. In this section, you will learn how to read
an image using the following three methods:
1 # open_image_context.py
2
3 from PIL import Image
4
5 with Image.open("flowers.jpg") as image:
6 image.show("flowers")
This code works in much the same way as the previous code with the exception that once you leave
the with statement block, the image object is cleaned up automatically.
Now let’s find out how to read an image from memory.
1 # open_image_from_memory.py
2
3 import io
4 import urllib.request
5
6 from PIL import Image
7
8 # Use a real URL to an image here:
9 url = "http://my_url/photo.jpg"
10 f = urllib.request.urlopen(url)
11 data = f.read()
12
13 with Image.open(io.BytesIO(data)) as image:
14 image.show("Downloaded Image")
In this code, you download an image from the Internet. You will need to find a real URL to use here.
You can use this one for example:
1 https://www.blog.pythonlibrary.org/wp-content/uploads/2020/02/py101_thumb.jpg
To do the downloading magic, you use Python’s urllib module. It returns a file-like object that
allows you to read the image into memory. Then you can use io.BytesIO to turn it into something
that Image.open() will understand. If everything works correctly, it will download the image and
display it to you!
Now you are ready to learn how to read an image from a tar file.
To see how this works, you should create a file named open_image_from_tar.py and put this code
in it:
Chapter 1 - Pillow Basics 16
1 # open_image_from_tar.py
2
3 from PIL import Image, TarIO
4
5 fobj = TarIO.TarIO("flowers.tar", "flowers.jpg")
6 image = Image.open(fobj)
7 image.show()
Here you need to use Pillow’s TarIO() class to load the image file into memory. As mentioned above,
you need to pass in the path to the tar file, followed by the image file that you wish to extract. This
will untar the file into memory and allow you to open it using Image.open(), which takes a file-like
object or a file path.
Now let’s do something practical before you finish this chapter. In the next section, you’ll learn how
to create thumbnails with Pillow!
Creating Thumbnails
One common task when working with photos is the generation of thumbnails. All modern file
browsers, like Windows Explorer or Mac’s Finder, can automatically show you thumbnails of
images.
With that in mind, why would you want to create thumbnails yourself? If you are creating a desktop
graphical user interface or a web application, you will almost certainly want thumbnails. They
are much smaller than the full-size images, which means they will load up much faster in your
applications.
In a real application, your program would create and probably cache the thumbnails for next time
before displaying them to the user. Pillow makes creating thumbnails easy.
To see how this works, create a file named thumbnail_maker.py and enter this code:
1 # thumbnail_maker.py
2
3 from PIL import Image
4
5
6 def create_thumbnail(input_file_path, thumbnail_path, thumbnail_size):
7 with Image.open(input_file_path) as image:
8 image.thumbnail(thumbnail_size)
9 image.save(thumbnail_path, format="JPEG")
10
11
12 if __name__ == "__main__":
13 create_thumbnail("flowers.jpg", "flowers.thumbnail", (128, 128))
Chapter 1 - Pillow Basics 17
In this code, you have create_thumbnail(), which takes in the following three arguments:
Pillow’s thumbnail() method will resize the photo using the size you specify, and it will do it while
also maintaining the aspect ratio of the image – this prevents the thumbnail from getting distorted.
Note that in this example you are using a non-traditional file extension in your call to save() – that
is why you tell Pillow that the file type should be JPEG. If you hadn’t included that bit of information,
Pillow would have thrown an exception because thumbnail is not a valid image file type.
When you are finished creating a thumbnail, check out the file size and compare it with the original
file’s size. You will quickly discover that thumbnails are much smaller, which is what makes them
so useful as a preview on websites and desktop GUIs.
Speaking of GUIs, you can learn how you might use one with Pillow in the following section.
1 # image_viewer.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 from PIL import Image
7
8
9 file_types = [("JPEG (*.jpg)", "*.jpg"),
10 ("All files (*.*)", "*.*")]
11
12 def main():
13 layout = [
14 [sg.Image(key="-IMAGE-", size=(400,400))],
15 [
16 sg.Text("Image File"),
17 sg.Input(size=(25, 1), key="-FILE-"),
18 sg.FileBrowse(file_types=file_types),
19 sg.Button("Load Image"),
20 ],
21 ]
22
23 window = sg.Window("Image Viewer", layout)
24
25 while True:
26 event, values = window.read()
27 if event == "Exit" or event == sg.WIN_CLOSED:
28 break
29 if event == "Load Image":
30 filename = values["-FILE-"]
31 if os.path.exists(filename):
32 image = Image.open(values["-FILE-"])
33 image.thumbnail((400, 400))
34 bio = io.BytesIO()
35 image.save(bio, format="PNG")
36 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
37
38 window.close()
39
40
41 if __name__ == "__main__":
42 main()
Chapter 1 - Pillow Basics 19
This is a decent chunk of code. Let’s break it down into a couple of smaller pieces:
1 # image_viewer.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 from PIL import Image
7
8
9 file_types = [("JPEG (*.jpg)", "*.jpg"),
10 ("All files (*.*)", "*.*")]
This is your initial setup code. You import PySimpleGUI and the modules you need from PIL, and
set file_types to the file selection choices for the Browse button in the form, which will default to
JPEG.
Now you’re ready to learn about the main() function:
1 def main():
2 elements = [
3 [sg.Image(key="-IMAGE-", size=(400,400))],
4 [
5 sg.Text("Image File"),
6 sg.Input(size=(25, 1), enable_events=True, key="-FILE-"),
7 sg.FileBrowse(file_types=file_types),
8 ],
9 ]
10
11 window = sg.Window("Image Viewer", elements)
This is your main() function. These 11 lines of code define how your Elements are laid out.
PySimpleGUI uses Python lists to lay out the user interface. In this case, you are telling it that
you want to create an Image widget at the top of your Window. Then you want to add three more
widgets underneath it. These three widgets are lined up horizontally in the form from left-to-right.
The reason they are lined up horizontally is because they are in a nested list.
These three widgets are as follows:
To enable events for an Element, you set the enable_events argument to True – this will submit
an Event whenever the Element changes. You disable the Input Element to make it read-only and
prevent typing into it – each keypress would be an individual Event, and your loop is not prepared for
that. Any Element you need to access later should also be given a name, which is the key argument.
These have to be unique.
The last piece of code to cover are these lines:
1 while True:
2 event, values = window.read()
3 if event == "Exit" or event == sg.WIN_CLOSED:
4 break
5 if event == "Load Image":
6 filename = values["-FILE-"]
7 if os.path.exists(filename):
8 image = Image.open(values["-FILE-"])
9 image.thumbnail((400, 400))
10 bio = io.BytesIO()
11 image.save(bio, format="PNG")
12 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
13
14 window.close()
15
16
17 if __name__ == "__main__":
18 main()
This is how you create the event loop in PySimpleGUI. You read() the window object for events and
values. You check for the Exit event, which occurs when you close the application. You also check
for the file event. This is the key name you defined earlier for the Input Element. When an event
occurs with that Element, it will be added to the window using that Element’s key or name.
This is where the meat of the program is. When the file event is fired, you will grab the image that
was chosen by doing a lookup using the key on the values dictionary. Now you have the path to
the image! You can open that image using Pillow, then resize it using thumbnail(). To display the
image, you convert the file into a byte stream using io.BytesIO, which lets you save the image in
memory. Then you pull the byte data from the in-memory file and pass that to the sg.Image object
in the window.update() method at the end.
Finally you show the image by calling update() on the Image widget and passing in the PhotoImage
object. You can do that by using the image key that is contained in the window object.
When you run this code, you will end up with something like this:
Chapter 1 - Pillow Basics 21
That is how the Image Viewer looks when an image is not loaded. If you load an image, it will look
like this:
Doesn’t that look nice? Give it a try by opening up a photo on your computer!
Wrapping Up
Pillow’s Image module can be used for many different image processing chores. This chapter barely
scratches the surface of the many things you can do with Pillow.
You learned about the following:
Chapter 1 - Pillow Basics 22
• Opening Images
• Saving Images
• Changing Compression During Saving
• Reading Methods
• Creating Thumbnails
• Creating an Image Viewer
These topics give you a basic understanding of how to get started using Pillow and create a little
GUI application. In the next chapter, you will learn about working with colors using Pillow.
Chapter 2 - Colors
Pillow also lets you work with the colors in images. For example, you could extract a list of all the
colors from an image, or you could change an image’s color pixel-by-pixel or in its entirety.
In this chapter, you will learn about the following topics:
• Understanding Color
• Using Pillow to Get RGB Values
• Getting Colors from Images
• Changing Pixel Colors
• Converting to Black and White
• Creating 4-Color Photos
• Creating a Sepia Photo
• Creating an Image Converter GUI
Let’s get started by learning how colors work when using a computer!
Understanding Color
There are several different ways to represent color using a computer. One of the most popular is
known as RGBA. These letters specify how much Red, Green, Blue, and Alpha (transparency) is in
a specific pixel in an image. Each of these is an integer that can be 0 (no color) up to 255 (maximum
color), as they are represented using 8-bit unsigned integers.
The pixel is a dot on a screen. A 1080p screen is made up of 1,920 pixels x 1,080 pixels or 2,073,600
individual pixels. As you can see, most computer monitors have several million pixels on their screen.
The RGB value tells your computer what shade of color it should display.
Pillow also supports RGBA. The alpha value in RGBA tells the computer how transparent the pixel
should be. This transparency can allow you to “see-through” the image to the background. To tell
Pillow about RGBA colors, you must pass in a tuple of four integer values. For example, Green is
represented by (0, 255, 0, 255).
Here are some common colors using RGBA values:
If the alpha value in the RGBA tuple is zero, then the pixel will be invisible. It doesn’t matter what
the values of red, green, and blue are at that point since you won’t be able to see them.
RGB is used to represent colors in computers because the screens emit light. When you use light,
the 3 primary colors are Red, Green, and Blue. This is known as the additive color model. Another
popular color model is CMYK or Cyan (blue), Magenta (red), Yellow, and blacK. This model is used
by printers to create the colors you would see on a page of paper.
Now that you have a basic understanding of how colors are represented, let’s find out how you can
apply this knowledge using Pillow!
The ImageColor can understand 140 standard HTML color names. The color names are not case
sensitive, that means that “red” and “Red” will both result in the same color being returned. You
can use hexadecimal color specifiers to get the RGB values from the ImageColor module. Pillow also
supports Hue-Saturation-Lightness (HSL) and Hue-Saturation-Value (HSV) functions.
Hue-Saturation-Lightness (HSL) and Hue-Saturation-Value (HSV) are alternative representations of
the RGB model. They are designed to more closely mimic how human vision perceives color.
For more information on how these alternate color name work, see the documentation⁵ for
ImageColor.
To see how this all works, open up a Python terminal and try running the following code:
⁵https://pillow.readthedocs.io/en/stable/reference/ImageColor.html
Chapter 2 - Colors 25
Each of these examples shows how to get the “red” RGB value. Note that when you use the HSV
value, Pillow returns the RGB value without the alpha channel. HSV is not completely compatible
with RGBA because HSV does not have an alpha value. Pillow converts it to RGB using getrgb().
You can get a full listing of the named colors that Pillow supports by accessing the colormap attribute
of the ImageColor module:
1 >>> ImageColor.colormap
2 {'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff',
3 'aquamarine': '#7fffd4', 'azure': '#f0ffff', 'beige': '#f5f5dc',
4 'bisque': '#ffe4c4', 'black': '#000000', 'blanchedalmond': '#ffebcd',
5 ...} # cropped for brevity
When you access colormap, it will return 140+ colors as a Python dictionary. The keys of
the dictionary are the HTML color names while the values are the corresponding hexadecimal
representations of the colors. The output above has been abbreviated. These colors are all in RGB,
not in RGBA. Try it out on your machine to see what color names are available to you.
You can use colormap in a loop to get the RBGA value for each of the colors. To see how that works,
create a file named color_test.py and add this code to it:
1 # color_test.py
2
3 from PIL import ImageColor
4
5
6 def get_rgb_value(color_name):
7 return ImageColor.getcolor(color_name, "RGBA")
8
9
10 if __name__ == "__main__":
11 for color in ImageColor.colormap:
12 print(f"{color} = {get_rgb_value(color)}")
Here you create get_rgb_value() which takes in a color name. It then returns ImageColor.getcolor()
using the color_name you specified. To make this more interesting, you add a for loop at the end
that loops over all the names in the colormap and prints them out.
Chapter 2 - Colors 26
Here are the first three lines of output that this code generates:
Go ahead and create a new file named image_colors.py and enter this code:
⁶https://github.com/driscollis/image_processing_with_python
Chapter 2 - Colors 27
1 # image_colors.py
2
3 from PIL import Image
4
5
6 def get_image_colors(image_path):
7 with Image.open(image_path) as image:
8 colors = image.getcolors()
9
10 return colors
11
12
13 if __name__ == "__main__":
14 print(get_image_colors("cape_thick_knee.jpg"))
When you run this code you will see output similar to the following:
1 [(28, (255, 255, 255)), (31, (253, 253, 253)), (89, (251, 251, 251)),
2 (262, (249, 249, 249)), (551, (247, 247, 247)), (511, (245, 245, 245)),
3 (583, (243, 243, 243)), (865, (241, 241, 241)), (1135, (239, 239, 239)),
4 ...]
This output tells you that Pillow found 28 pixels of the color (255, 255, 255), 31 pixels of the color
(253, 253, 253), etcetera (it has been truncated). The getcolors() function has a maxcolors argument
that defaults to 256 colors. If the image that you use happens to have more than 256 colors in it,
getcolors() will return None.
If you have that happen with your photos, you will need to adjust your maxcolors value higher. Try
loading this butterfly image with the code above:
Chapter 2 - Colors 28
If you re-run the code, it will return None instead of a list of colors. You will need to experiment to
figure out what the maxcolor value needs to be to get it to return some colors for you.
Now let’s apply the knowledge you learned about getting colors and learn how to change a pixel’s
color!
1 # create_image.py
2
3 from PIL import Image
4 from PIL import ImageColor
5
6
7 def create_image(path, size):
8 image = Image.new("RGBA", size)
9
10 red = ImageColor.getcolor("red", "RGBA")
11 green = ImageColor.getcolor("green", "RGBA")
12 color = red
13
14 count = 0
15 for y in range(size[1]):
16 for x in range(size[0]):
17 if count == 5:
18 # swap colors
19 color = red if red != color else green
20 count = 0
21 image.putpixel((x, y), color)
22 count += 1
23
24 image.save(path)
25
26
27 if __name__ == "__main__":
28 create_image("lines.png", (150, 150))
The first few lines are your imports. Since you need to change the color of the pixels, you will need
ImageColor to get the color RGB values you want. Next, you create a loop to iterate over all the
pixels in your code. The pixels start as transparent, so you will be changing them to either red or
green using the putpixel() method.
The putpixel() method takes in the specific pixel location and the color you want it to be as its
arguments. In this code, you change the color of 5 pixels at a time, then you swap colors. This will
create multiple vertical lines of color across your image.
When you run this code, you will end up with an image that looks like this:
Chapter 2 - Colors 30
You could take this code and modify it to look for specific colors in an image and swap them out for
other ones. However, since so many photos are now taken in very high resolutions, this code would
be very slow. Pillow is not made for fast pixel-by-pixel editing. If you want to try to do this on a
high-resolution image, you should use NumPy instead.
Now let’s move on and find out how to convert color images to black and white ones!
Now that you have an image, you are ready to write some code! Create a file and name it create_-
grayscale.py. Then enter this code in it:
1 # create_grayscale.py
2
3 from PIL import Image
4
5
6 def grayscale(input_image_path, output_image_path):
7 color_image = Image.open(input_image_path)
8 gray_scale = color_image.convert("L")
9 gray_scale.save(output_image_path)
10
11
Chapter 2 - Colors 32
12 if __name__ == "__main__":
13 grayscale("monarch_caterpillar.jpg", "gray_caterpillar.jpg")
Here you have a grayscale() function that accepts an input file path and an output file path. It opens
the file and then uses the convert() method to transform the image into grayscale. The convert
method’s signature looks like this:
In this example, you convert the image using L, which means you are converting the image to
grayscale.
You can apply dithering to the L mode if you want to. The term, dither, is used to describe the
intentional addition of noise to an image. One of the most common applications of dithering is
when converting a color image to black and white. The dithering changes the density of the black
dots in the image. You will see how dithering affects the image soon.
For now, go ahead and run the code. The output image will look like this:
Chapter 2 - Colors 33
That looks pretty good! Your image is nice and sharp and you can see the blacks in the caterpillar
quite well.
Let’s try doing another conversion! Create a new Python file and name it create_bw.py. Then enter
this code:
Chapter 2 - Colors 34
1 # create_bw.py
2
3 from PIL import Image
4
5
6 def black_and_white(input_image_path, output_image_path):
7 color_image = Image.open(input_image_path)
8 gray_scale = color_image.convert("1")
9 gray_scale.save(output_image_path)
10
11
12 if __name__ == "__main__":
13 black_and_white("monarch_caterpillar.jpg", "bw_caterpillar.jpg")
This code is almost the same as the previous example. However, in this example, you use the mode,
1 (one), instead of L. The output of this code shows a lot more white noise in the photo:
Chapter 2 - Colors 35
That’s not quite as nice as the last output is it? Let’s see if you can make it any better by applying
some dithering to the photo! Create a file named create_bw_dithering.py and enter this code:
1 # create_bw_dithering.py
2
3 from PIL import Image
4
5
6 def black_and_white(input_image_path, output_image_path):
7 color_image = Image.open(input_image_path)
8 gray_scale = color_image.convert("1", dither=0)
9 gray_scale.save(output_image_path)
10
11
Chapter 2 - Colors 36
12 if __name__ == "__main__":
13 black_and_white("monarch_caterpillar.jpg", "dither_caterpillar.jpg")
In this example, you apply a dither value of 0. When you run this code, you will get the following
output:
Now your image looks like it’s very over-exposed. It’s kind of reminiscent of old photos from the
early 1900s.
In the next section, you will learn how to use palettes to modify the colors in your photos!
Chapter 2 - Colors 37
1 # create_4_color.py
2
3 from PIL import Image
4
5
6 def four_color(input_image_path, output_image_path):
7 color_image = Image.open(input_image_path)
8 gray_scale = color_image.convert("P", palette=Image.ADAPTIVE, colors=4)
9 gray_scale.save(output_image_path)
10
11
12 if __name__ == "__main__":
13 four_color("monarch_caterpillar.jpg", "four_color_caterpillar.png")
This example sets the conversion mode to P, the palette to Image.ADAPTIVE and the color depth to 4.
Run your code and you will see the following image as your output:
Chapter 2 - Colors 38
Your photo now has a greenish tint. It looks a bit odd, frankly. But your code demonstrates what
happens when you change the color palette.
Let’s try doing something a bit more traditional with palettes and convert your image using a sepia
color.
To see how this works, you’ll need a new Python script. This time you will name it create_sepia.py.
Then add this code to your script:
1 # create_sepia.py
2
3 from PIL import Image
4
5
6 def make_sepia_palette(color):
7 palette = []
8 r, g, b = color
9 for i in range(255):
10 new_red = r * i // 255
11 new_green = g * i // 255
12 new_blue = b * i // 255
13 palette.extend((new_red, new_green, new_blue))
14
15 return palette
16
17
18 def create_sepia(input_image_path, output_image_path):
19 whitish = (255, 240, 192)
20 sepia = make_sepia_palette(whitish)
21
22 color_image = Image.open(input_image_path)
23
24 # convert our image to gray scale
25 gray = color_image.convert("L")
26
27 # add the sepia toning
28 gray.putpalette(sepia)
29
30 # convert to RGB for easier saving
31 sepia_image = gray.convert("RGB")
32
33 sepia_image.save(output_image_path)
34
35
36 if __name__ == "__main__":
37 create_sepia("monarch_caterpillar.jpg", "sepia_caterpillar.jpg")
The first step in create_sepia() is to create an off-white color. You will need the off-white color to
create a new sepia color. You pass that whitish color to make_sepia_palette(), which will take the
Chapter 2 - Colors 40
red, green, and blue components out of the color and then do some tinting on the entire range of
colors from 0-255.
This creates a Python list of integers that you can use as your palette for tinting your photo. You
cannot use append() here or you will get a list of tuples, which will cause an exception to be raised.
Next, you open the input image and convert it to grayscale. Once you have that grayscale version
of the image, you can use Pillow’s putpalette() method to apply your sepia palette to the image.
The last step is to convert the gray image back to RGB so that you can save the image. When you
run the code, you will end up with this nice sepia version of the caterpillar:
At this point, you know how to use palettes in a couple of different ways. But wouldn’t it be nice to
be able to see what your changes were doing in real-time rather than needing to open up the new
image all the time? You’ll find out how to do that in the next section!
Chapter 2 - Colors 41
You can get started by creating a new file named image_converter.py and adding this code to it:
Chapter 2 - Colors 43
1 # image_converter.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from create_bw import black_and_white
10 from create_grayscale import grayscale
11 from create_sepia import create_sepia as sepia
12 from PIL import Image
13
14 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
15
16 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
17
18 effects = {
19 "Normal": shutil.copy,
20 "Black and White": black_and_white,
21 "Grayscale": grayscale,
22 "Sepia": sepia,
23 }
24
25
26 def main():
27 effect_names = list(effects.keys())
28 layout = [
29 [sg.Image(key="-IMAGE-", size=(400,400))],
30 [
31 sg.Text("Image File"),
32 sg.Input(size=(25, 1), key="-FILENAME-"),
33 sg.FileBrowse(file_types=file_types),
34 sg.Button("Load Image")
35 ],
36 [
37 sg.Text("Effect"),
38 sg.Combo(
39 effect_names, default_value="Normal", key="-EFFECTS-",
40 enable_events=True, readonly=True
41 ),
42 ],
43 [sg.Button("Save")],
Chapter 2 - Colors 44
44 ]
45
46 window = sg.Window("Image Converter", layout, size=(450, 500))
47
48 while True:
49 event, values = window.read()
50 if event == "Exit" or event == sg.WIN_CLOSED:
51 break
52 if event in ["Load Image", "-EFFECTS-"]:
53 selected_effect = values["-EFFECTS-"]
54 image_file = values["-FILENAME-"]
55 if image_file:
56 effects[selected_effect](image_file, tmp_file)
57 image = Image.open(tmp_file)
58 image.thumbnail((400, 400))
59 bio = io.BytesIO()
60 image.save(bio, format="PNG")
61 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
62 if event == "Save" and values["-FILENAME-"]:
63 save_filename = sg.popup_get_file(
64 "File", file_types=file_types, save_as=True, no_window=True
65 )
66 if save_filename == values["-FILENAME-"]:
67 sg.popup_error(
68 "You are not allowed to overwrite the original image!")
69 else:
70 if save_filename:
71 shutil.copy(tmp_file, save_filename)
72 sg.popup(f"Saved: {save_filename}")
73
74 window.close()
75
76
77 if __name__ == "__main__":
78 main()
Since this code is pretty long, you will go over it one piece at a time. You can start by looking at the
imports:
Chapter 2 - Colors 45
1 # image_converter.py
2
3 import PySimpleGUI as sg
4 import shutil
5 import tempfile
6
7 from create_bw import black_and_white
8 from create_grayscale import grayscale
9 from create_sepia import create_sepia as sepia
10 from PIL import Image, ImageTk
In this code, you will be using shutil, which is useful for copying and moving files and folders. You
also use tempfile to create a temporary image file.
The next three lines import three of the functions you created earlier in the chapter:
• black_and_white() - for creating black and white images (with a lot of artifacts)
• grayscale() - for creating grayscale images
• sepia() - for creating sepia images (note how it renamed the actual function)
Knowing how to write reusable code is great. This means you can still use those Python modules as
standalone scripts, but they can also be imported by your GUI here.
The last line imports the two items you need from Pillow itself.
Now you’re ready to see the next few lines of code:
Here you setup some variables that are used by your GUI later on. The first is the list of file types
your application can open. Then you create the temporary image file where you will save your image
temporarily when you apply effects to it. Finally, you create a dictionary that contains all the effects
you will use and maps those effects to functions.
You are now ready to see how you layout your GUI:
Chapter 2 - Colors 46
1 def main():
2 effect_names = list(effects.keys())
3 layout = [
4 [sg.Image(key="-IMAGE-", size=(400,400))],
5 [
6 sg.Text("Image File"),
7 sg.Input(size=(25, 1), key="-FILENAME-"),
8 sg.FileBrowse(file_types=file_types),
9 sg.Button("Load Image")
10 ],
11 [
12 sg.Text("Effect"),
13 sg.Combo(
14 effect_names, default_value="Normal", key="-EFFECTS-",
15 enable_events=True, readonly=True
16 ),
17 ],
18 [sg.Button("Save")],
19 ]
20
21 window = sg.Window("Image Converter", layout, size=(450, 500))
This chunk of code is all about adding Elements to your application. You also create a list of effect_-
names. PySimpleGUI uses a Python list of lists to layout your Elements. This will create your entire
user interface. Nested lists of elements create a group of horizontal elements.
The main difference between this user interface and the one you created in the previous chapter is
that you now have a way to apply effects and save your changes. You load the effects list into the
Combo widget and set the default selection to “Normal”.
1 while True:
2 event, values = window.read()
3 if event == "Exit" or event == sg.WIN_CLOSED:
4 break
5 if event in ["Load Image", "-EFFECTS-"]:
6 selected_effect = values["-EFFECTS-"]
7 image_file = values["-FILENAME-"]
8 if image_file:
9 effects[selected_effect](image_file, tmp_file)
10 image = Image.open(tmp_file)
11 image.thumbnail((400, 400))
12 bio = io.BytesIO()
Chapter 2 - Colors 47
13 image.save(bio, format="PNG")
14 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
15 if event == "Save" and values["-FILENAME-"]:
16 save_filename = sg.popup_get_file(
17 "File", file_types=file_types, save_as=True, no_window=True
18 )
19 if save_filename == values["-FILENAME-"]:
20 sg.popup_error(
21 "You are not allowed to overwrite the original image!")
22 else:
23 if save_filename:
24 shutil.copy(tmp_file, save_filename)
25 sg.popup(f"Saved: {save_filename}")
26
27 window.close()
This is your event loop. If the user closes the window, then the event loop will end and the application
exits. If the user chooses a file to load or they pick an event from the combo box, then you will apply
an effect. After the effect is applied via the Pillow commands mentioned in that section, you call
window["image"].update() to update the image so that the user can see the effect.
The last conditional will execute if the user presses the “Save” button. It will ask the user where to
save the file. Then it will use shutil to copy the temporary file you created to the new location.
Finally, it lets the user know that the image is saved.
Now you’re done and you have a nice little application that you can use to apply the effects you
created in this chapter!
Wrapping Up
Colors are important in your photos. They can make your images look lush and vibrant. You can
use Pillow to access the colors in your photos and manipulate them.
In this chapter, you learned about the following topics:
• Understanding Color
• Using Pillow to Get RGBA Values
• Getting Colors from Images
• Changing Pixel Colors
• Converting to Black and White
• Creating 4-Color Photos
• Creating a Sepia Photo
• Creating an Image Converter GUI
Chapter 2 - Colors 48
Now you have a basic understanding of working with colors in Pillow. Go try some of these examples
with your images and see what you can create!
Chapter 3 - Getting Image Metadata
Digital photographs of the JPG filetype include a type of metadata that is known as Exchangeable
Image File Format or simply Exif. You can access these tags using the PIL.ExifTags module.
You can learn more about this module by checking out its documentation⁷.
Pillow also provides access to the standard Tag Image File Format (TIFF) metadata tag numbers,
names, and type information via its PIL.TiffTags module. The documentation⁸ for that module is
the best place to discover more.
Unfortunately, neither of these pages is very detailed. The best way to understand how they work
is to start using them. In this chapter, you will be learning how to do that in the following sections:
Let’s get started by learning how to load Exif data with Pillow!
Pillow can be used to retrieve the Exif metadata from the bridge photo above. To see how this works,
create a file named exif_getter.py and add this code to it:
1 # exif_getter.py
2
3 from PIL import Image
4 from PIL.ExifTags import TAGS
5
6
7 def get_exif(image_file_path):
8 exif_table = {}
9 image = Image.open(image_file_path)
10 info = image._getexif()
11 for tag, value in info.items():
12 decoded = TAGS.get(tag, tag)
13 exif_table[decoded] = value
14 return exif_table
15
16
Chapter 3 - Getting Image Metadata 51
17 if __name__ == "__main__":
18 exif = get_exif("bridge.JPG")
19 print(exif)
In this example, you create get_exif(), which takes in the path to the image that you want to extract
Exif tags from. Then you create an exif_table, which is a Python dictionary. Next, you open up
the image using Pillow and extract the Exif information using _getexif(). After that, you loop over
the items you extracted and use Pillow’s TAGS, which is a hard-coded Python dictionary that maps
a hexadecimal value to a human-readable name.
Once you have decoded the Exif data and populated the exif_table dictionary, you return the data.
When you run this code using the bridge image, you will get a lot of output.
The following is a sample of some of the data that is returned:
1 {'ExifVersion': b'0230',
2 'ComponentsConfiguration': b'\x01\x02\x03\x00',
3 'CompressedBitsPerPixel': 3.0,
4 'DateTimeOriginal': '2020:06:12 10:01:59',
5 'DateTimeDigitized': '2020:06:12 10:01:59',
6 'BrightnessValue': 10.56015625,
7 'ExposureBiasValue': 0.3,
8 'MaxApertureValue': 4.3359375,
9 'MeteringMode': 5,
10 'LightSource': 0,
11 'Flash': 16,
12 'FocalLength': 38.0,
13 'SceneCaptureType': 0,
14 'ExifImageHeight': 1080,
15 'Make': 'SONY',
16 'Model': 'ILCE-6300',
17 'Orientation': 1}
Some of the data that is returned is not human readable. One of the shortest examples is
ComponentsConfiguration mentioned above. But there are several others. The
ComponentsConfiguration property is a representation of the color planes in the image, starting at
the 1st plane and going through the 4th plane. Not all images have the same amount of data as others.
It depends on your camera and how much data it can record.
If you have edited the image yourself, you may have inadvertently deleted the Exif data. While it’s
nice to crop a photo or enhance one, you need to keep that in mind if you want to preserve the Exif
data. You can use it to help you take photos in the future since it has all your camera settings at the
time the shot was taken.
Now let’s find out how to get the Global Positioning System (GPS) Exif information from your
images!
Chapter 3 - Getting Image Metadata 52
Now that you have a photo to use, you need to write some code! Open up your editor and create a
new file named gps_exif_getter.py. Then add the following:
Chapter 3 - Getting Image Metadata 53
1 # gps_exif_getter.py
2
3 from PIL import Image
4 from PIL.ExifTags import TAGS, GPSTAGS
5
6
7 def get_exif(image_file_path):
8 exif_table = {}
9 image = Image.open(image_file_path)
10 info = image._getexif()
11 for tag, value in info.items():
12 decoded = TAGS.get(tag, tag)
13 exif_table[decoded] = value
14
15 gps_info = {}
16 for key in exif_table['GPSInfo']:
17 decode = GPSTAGS.get(key,key)
18 gps_info[decode] = exif_table['GPSInfo'][key]
19
20 if gps_info:
21 return gps_info
22 else:
23 return "No GPS data found!"
24
25
26 if __name__ == "__main__":
27 exif = get_exif("jester.jpg")
28 print(exif)
The first new item here is that you need to import GPSTAGS in addition to TAGS. The next thing
you need to do is add a second loop (lines 16-18) after decoding the regular Exif tags. Right before
that second loop, you create a new gps_info dictionary. Then you loop over the keys in the exif_-
table['GPSInfo'] dictionary and decode the GPS tags in the same way that you did with the regular
Exif tags.
When you run this code, you should get the following output:
1 {'GPSLatitudeRef': 'N',
2 'GPSLatitude': (41.0, 47.0, 2.17),
3 'GPSLongitudeRef': 'W',
4 'GPSLongitude': (93.0, 46.0, 42.09)}
You can use this information to look up the image’s location on a map. You could even write some
Chapter 3 - Getting Image Metadata 54
code for Google maps that you could use to programmatically load up a map of where you took
your photos, if you wanted to.
Now let’s move on and learn about how you can get TIFF tags with Pillow!
You can create your own TIFF metadata extractor utility by making a new file named tiff_-
metadata.py and adding this code to it:
Chapter 3 - Getting Image Metadata 55
1 # tiff_metadata.py
2
3 from PIL import Image
4 from PIL.TiffTags import TAGS
5
6
7 def get_metadata(image_file_path):
8 image = Image.open(image_file_path)
9 metadata = {}
10 for tag in image.tag.items():
11 metadata[TAGS.get(tag[0])] = tag[1]
12 return metadata
13
14
15 if __name__ == "__main__":
16 metadata = get_metadata("reportlab_cover.tiff")
17 print(metadata)
Here you import the TAGS dictionary from the PIL.TiffTags submodule. Then in get_metadata(),
you access the tag elements in the image by iterating over the contents of tag.items(). To make
that information more readable, you use the TAGS dictionary that you imported.
Here is a sample of the output you will get when you run this code:
1 {'ImageWidth': (400,),
2 'ImageLength': (562,),
3 'BitsPerSample': (8, 8, 8),
4 'Compression': (1,),
5 'PhotometricInterpretation': (2,),
6 'FillOrder': (1,),
7 'StripOffsets': (82, 130882, 261682, 392482, 523282, 654082),
8 'Orientation': (1,),
9 'SampleFormat': (1, 1, 1),
10 'SamplesPerPixel': (3,),
11 'RowsPerStrip': (109,),
12 'StripByteCounts': (130800, 130800, 130800, 130800, 130800, 20400),
13 'XResolution': ((300, 1),),
14 'YResolution': ((300, 1),),
15 'PlanarConfiguration': (1,),
16 'ResolutionUnit': (2,),
17 'ExifIFD': (8,),
18 'Software': ('Pixelmator 3.9',),
19 'DateTime': ('2020:10:27 12:10:37',),
20 }
Chapter 3 - Getting Image Metadata 56
You can see that the value entries above are all tuples. This is because of how the data is returned
from the tag data. If you would like a challenge, you can attempt to clean up this data a bit in your
version of the metadata extraction utility.
Let’s move on and learn how you could add a desktop user interface to view your Exif information!
The code for this viewer is about 80 lines or so. You can create a file called exif_viewer.py. Then
add the following code to it:
Chapter 3 - Getting Image Metadata 57
1 # exif_viewer.py
2
3 import PySimpleGUI as sg
4
5 from pathlib import Path
6 from PIL import Image
7 from PIL.ExifTags import TAGS
8
9 file_types = [
10 ("(JPEG (*.jpg)", "*.jpg"),
11 ("All files (*.*)", "*.*"),
12 ]
13
14 fields = {
15 "File name": "File name",
16 "File size": "File size",
17 "Model": "Camera Model",
18 "ExifImageWidth": "Width",
19 "ExifImageHeight": "Height",
20 "DateTime": "Creation Date",
21 "static_line": "*",
22 "MaxApertureValue": "Aperture",
23 "ExposureTime": "Exposure",
24 "FNumber": "F-Stop",
25 "Flash": "Flash",
26 "FocalLength": "Focal Length",
27 "ISOSpeedRatings": "ISO",
28 "ShutterSpeedValue": "Shutter Speed",
29 }
30
31
32 def get_exif_data(path):
33 """
34 Extracts the EXIF information from the provided photo
35 """
36 exif_data = {}
37 try:
38 image = Image.open(path)
39 info = image._getexif()
40 except OSError:
41 info = {}
42
43 if info is None:
Chapter 3 - Getting Image Metadata 58
44 info = {}
45
46 for tag, value in info.items():
47 decoded = TAGS.get(tag, tag)
48 exif_data[decoded] = value
49
50 return exif_data
51
52
53 def main():
54 layout = [[
55 sg.FileBrowse(
56 "Load image data", file_types=file_types, key="-LOAD-",
57 enable_events=True,
58 )
59 ]]
60 for field in fields:
61 layout += [[
62 sg.Text(fields[field], size=(15, 1)),
63 sg.Text("", size=(25, 1), key=field),
64 ]]
65 window = sg.Window("Image information", layout)
66
67 while True:
68 event, values = window.read()
69 if event == "Exit" or event == sg.WIN_CLOSED:
70 break
71 if event == "-LOAD-":
72 image_path = Path(values["-LOAD-"])
73 exif_data = get_exif_data(image_path.absolute())
74 for field in fields:
75 if field == "File name":
76 window[field].update(image_path.name)
77 elif field == "File size":
78 window[field].update(image_path.stat().st_size)
79 else:
80 window[field].update(exif_data.get(field, "No data"))
81
82
83 if __name__ == "__main__":
84 main()
That is a good-sized chunk of code! It would be confusing to try and explain all of it at once, so to
Chapter 3 - Getting Image Metadata 59
make things easier you will go over the code piece by piece.
Here are the first few lines of code:
1 # exif_viewer.py
2
3 import PySimpleGUI as sg
4
5 from pathlib import Path
6 from PIL import Image
7 from PIL.ExifTags import TAGS
8
9 file_types = [
10 ("(JPEG (*.jpg)", "*.jpg"),
11 ("All files (*.*)", "*.*"),
12 ]
13
14 fields = {
15 "File name": "File name",
16 "File size": "File size",
17 "Model": "Camera Model",
18 "ExifImageWidth": "Width",
19 "ExifImageHeight": "Height",
20 "DateTime": "Creation Date",
21 "static_line": "*",
22 "MaxApertureValue": "Aperture",
23 "ExposureTime": "Exposure",
24 "FNumber": "F-Stop",
25 "Flash": "Flash",
26 "FocalLength": "Focal Length",
27 "ISOSpeedRatings": "ISO",
28 "ShutterSpeedValue": "Shutter Speed",
29 }
The first half of this code is the imports you will need to make your application function. Next, you
create a file_types variable. This is used in a file dialog that you will create later on to allow the
user to select an image to load.
Then you create a Python dictionary that holds all the Exif fields that you want to display. This
dictionary maps the Exif name to a more readable name.
You are now ready to learn about the get_exif_data() function:
Chapter 3 - Getting Image Metadata 60
1 def get_exif_data(path):
2 """
3 Extracts the Exif information from the provided photo
4 """
5 exif_data = {}
6 try:
7 image = Image.open(path)
8 info = image._getexif()
9 except OSError:
10 info = {}
11
12 if info is None:
13 info = {}
14
15 for tag, value in info.items():
16 decoded = TAGS.get(tag, tag)
17 exif_data[decoded] = value
18
19 return exif_data
This function takes in the image path and attempts to extract the Exif data from it. If it fails, it sets
info to an empty dictionary. If image._getexif() returns None, then you also set info to an empty
dictionary. If info is populated, then you loop over it and decode the Exif data and populate your
exif_data dictionary before returning it.
1 def main():
2 layout = [[
3 sg.FileBrowse(
4 "Load image data", file_types=file_types, key="-LOAD-",
5 enable_events=True,
6 )
7 ]]
8 for field in fields:
9 layout += [[
10 sg.Text(fields[field], size=(15, 1)),
11 sg.Text("", size=(25, 1), key=field),
12 ]]
13 window = sg.Window("Image information", layout)
Here you create all the Elements you need to create your user interface. You loop over the fields
dictionary you defined at the beginning of your program and add a couple of text controls that will
display the Exif data you extract from your image.
Chapter 3 - Getting Image Metadata 61
PySimpleGUI makes this a snap since you can concatenate the new Elements to your elements list.
Once that’s all done, you add the elements to your Window.
The last piece of the puzzle is next:
1 while True:
2 event, values = window.read()
3 if event == "Exit" or event == sg.WIN_CLOSED:
4 break
5 if event == "-LOAD-":
6 image_path = Path(values["-LOAD-"])
7 exif_data = get_exif_data(image_path.absolute())
8 for field in fields:
9 if field == "File name":
10 window[field].update(image_path.name)
11 elif field == "File size":
12 window[field].update(image_path.stat().st_size)
13 else:
14 window[field].update(exif_data.get(field, "No data"))
15
16
17 if __name__ == "__main__":
18 main()
Here you have your event loop. When the user presses the “Load image data” button, the event is
set to “load”. Here you load up the selected image path into Python’s pathlib. This allows you to
extract the file name, absolute path, and file size using your Path object’s functions and attributes.
You use the dictionary’s get() method to get the field. If the field isn’t in the dictionary, then you
display “No data” for that field.
If you’d like a small challenge, try adding a sg.Image() Element to this GUI so you can view the
photo along with its metadata!
Wrapping Up
Image metadata is important. You can use it to learn how to take better photographs. If your
camera supports geographic tagging, you can use that data to figure out where a photo was taken
programmatically.
In this chapter, you learned about the following:
You can use the information from this chapter to build interesting applications. For example, you
could add a map to your GUI that shows where a photo was taken. It could provide links to the
camera that was used to take the photo. You can use the metadata to sort your library of photos into
different categories using the Exif data.
Use your imagination and give it a try!
Chapter 4 - Filters
Filters are effects that can be applied to your photographs. Instagram has made a lot of filters very
popular. A filter is a type of image transformation or simply image transform. An image transform
is a function that accepts an image as its input and which outputs a modified image. Filters can be
used to change the colors of an image, sharpen or blur an image and much more. You can think of
them as enhancements to your image.
You saw some examples of manual filters in chapter 2 when you changed a color image into a
grayscale or sepia image. However, Pillow includes several built-in filters that you can use via the
ImageFilter sub-module.
At the time of writing, ImageFilter supports the following predefined image enhancements / filters:
• BLUR
• CONTOUR
• DETAIL
• EDGE_ENHANCE
• EDGE_ENHANCE_MORE
• EMBOSS
• FIND_EDGES
• SHARPEN
• SMOOTH
• SMOOTH_MORE
This chapter will teach you how to use each of these enhancements. You will also learn about
RankFilters and MultibandFilters. There will be screenshots that demonstrate how an image
changes when a filter is applied so that you can visually see the difference.
When it comes to using filters with Pillow, you will find that the code structure is the same across
all of the predefined filters. The main thing to pay attention to is how the code changes the photos
that you supply.
With all of that out of the way, let’s get started by learning how to blur your photos!
1 # blur_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def blur(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.BLUR)
10 filtered_image.save(output_image)
11
12 if __name__ == "__main__":
13 blur("butterfly.jpg", "butterfly_blurred.jpg")
In this code, you are importing the Pillow module ImageFilter, which contains all the filters you
will be using in this chapter (and more) . To use this filter, you need to open an image and then call
the image object’s filter() method. This method takes in an ImageFilter class. For this example,
you use ImageFilter.BLUR.
You will be using this image to test your code:
Chapter 4 - Filters 65
Now run your code and you will get the following output:
Chapter 4 - Filters 66
You may need to open these files up using an image viewer on your machine to get a really good
comparison of these images.
Now let’s move on to the CONTOUR filter!
1 # contour_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def contour(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.CONTOUR)
10 filtered_image.save(output_image)
11
12 if __name__ == "__main__":
13 contour("flowers_dallas.jpg", "flowers_contour.jpg")
For this code, you will use this photo of some flowers from the Dallas Arboretum in Texas:
When you run this code, you will get the following image as your output:
Chapter 4 - Filters 68
Here you can see that the CONTOUR class grabs the “contours” of the image. It kind of makes a pencil-
like drawing of the image. Now you are ready to learn about adding details to your photos!
1 # detail_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def detail(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.DETAIL)
10 filtered_image.save(output_image)
11
12 if __name__ == "__main__":
13 detail("butterfly.jpg", "detailed_butterfly.jpg")
You will use the butterfly image from earlier with the DETAIL filter. Here is the butterfly before the
filter is applied:
After running your code, the new image will look like this:
Chapter 4 - Filters 70
The DETAIL filter is a bit hard to discern. You may need to open up the before and after photos and
compare them side-by-side to truly see the differences.
Now let’s learn about edge enhancement!
• EDGE_ENHANCE
• EDGE_ENHANCE_MORE
These filters will make the edges in your photos more prominent when applied. Once again, you
will need to create a new file. This time, you will name it edge_enhance_image.py and update the
code as follows:
Chapter 4 - Filters 71
1 # edge_enhance_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def edge_enhance(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.EDGE_ENHANCE)
10 filtered_image.save(output_image)
11
12 if __name__ == "__main__":
13 edge_enhance("cactus.jpg", "cactus_edge.jpg")
When you run this code, you can see that the cactus’s spines are more prominent after applying the
EDGE_ENHANCE filter to the image:
Chapter 4 - Filters 72
Now go back to the code and replace EDGE_ENHANCE with EDGE_ENHANCE_MORE. Then re-run the code.
When you do, you will end up with this image:
Chapter 4 - Filters 73
Be cautious when using EDGE_ENHANCE_MORE. It can make the image look grainy like it did in this
example. You will have to try each of these filters carefully to see which one works the best for you.
Now you are ready to see how the EMBOSS filter works!
1 # emboss_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def emboss(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.EMBOSS)
10 filtered_image.save(output_image)
11
12
13 if __name__ == "__main__":
14 emboss("hummingbird.jpg", "hummingbird_emboss.jpg")
For this example, you will be using this fun hummingbird photo:
When you run the code above, it will “emboss” the hummingbird and make the image look like this:
Chapter 4 - Filters 75
That looks kind of neat! You wouldn’t want to apply this all the time, but it does have a fun look.
Embossing will remove most of the details and colors in your photos.
Now you’re ready to learn how to find the edges of your images!
1 # find_edges_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def find_edges(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.FIND_EDGES)
10 filtered_image.save(output_image)
11
12
13 if __name__ == "__main__":
14 find_edges("buffalo.jpg", "buffalo_edges.jpg")
This code takes in a couple of paths as before, but this time it uses the FIND_EDGES filter. For this
piece of code, you will be using this photo of a buffalo:
When you run this code, it will transform the buffalo above into the new buffalo image below:
Chapter 4 - Filters 77
This looks almost like line art. You can still tell it’s a buffalo, but a lot of the details are removed
when you use this filter. Give it a try on your photos and see what you can come up with!
Now let’s move on and learn about sharpening your images.
1 # sharpen_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def sharpen(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.SHARPEN)
10 filtered_image.save(output_image)
11
12 if __name__ == "__main__":
13 sharpen("grasshopper.jpg", "grasshopper_sharpened.jpg")
For this code, you take a photo and apply the SHARPEN filter to it. You can use this photo of a fun
little grasshopper for this activity:
When you run the code above, the result will look like this:
Chapter 4 - Filters 79
This version of the grasshopper is sharpened. You can see the increase in the grasshopper itself
especially. However, you may need to open up both images side-by-side to truly see the difference.
Now let’s find out how to use smoothing filters in Pillow!
• SMOOTH
• SMOOTH_MORE
Chapter 4 - Filters 80
To see how you can use these filters, create a new file named smooth_image.py with the following
content:
1 # smooth_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def smooth(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.SMOOTH)
10 filtered_image.save(output_image)
11
12
13 if __name__ == "__main__":
14 smooth("spider.jpg", "spider_smooth.jpg")
You can run this code on a noisy photo or a regular photo to see what could change. For this example,
you will use this photo of a local spider:
Chapter 4 - Filters 81
It’s not particularly noisy, but it will give you a general idea of what the filter does. Run the code
above and you will end up with the following image:
Chapter 4 - Filters 82
The differences between these two are difficult to see. But you may notice that the image looks
slightly “flatter”. Try updating the code to use the SMOOTH_MORE filter. When you do, you will see
this:
Chapter 4 - Filters 83
Again, the differences are subtle at best. You would use smoothing to help remove noise from a
photo. This isn’t a filter for everyone, but it is a useful tool to have when you need it.
Now let’s move on and learn about the RankFilter!
RankFilters
Pillow supports other types of filters. One example is RankFilter. If you look at the docstring of
RankFilter, you will see the following definition:
Chapter 4 - Filters 84
1 class RankFilter(Filter):
2 """
3 Create a rank filter. The rank filter sorts all pixels in
4 a window of the given size, and returns the ``rank``'th value.
5
6 :param size: The kernel size, in pixels.
7 :param rank: What pixel value to pick. Use 0 for a min filter,
8 ``size * size / 2`` for a median filter, ``size * size - 1``
9 for a max filter, etc.
10 """
A kernel, or convolution matrix, is a type of image mask used for blurring, sharpening, embossing,
edge detection, and more. The term, convolution, refers to the process of adding each element to
the image to its neighbors while being weighted by the kernel. Pillow supports kernels that are 3x3
or 5x5 as well as floating-point kernels. If you are interested in learning more on this topic, see the
following links:
• MinFilter
• MedianFilter
• MaxFilter
Using these filters is slightly different than the filters you have tried in the previous sections. To see
how they differ, create a new file named minfilter_image.py and add this code to it:
1 # minfilter_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def minfilter(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.MinFilter(size=3))
10 filtered_image.save(output_image)
11
⁹https://en.wikipedia.org/wiki/Kernel_(image_processing)
¹⁰https://pillow.readthedocs.io/en/3.3.x/reference/ImageFilter.html
Chapter 4 - Filters 85
12
13 if __name__ == "__main__":
14 minfilter("giraffe.jpg", "giraffe_minfilter.jpg")
In this code, you are using MinFilter() with a kernel size of 3, which is the default value. Please
note that you can change the size of the kernel as you see fit.
You will be using this image of a giraffe for this code:
When you run this code on the giraffe photo, you will end up with this as your output:
Chapter 4 - Filters 86
MinFilter darkens the image slightly. Try changing the code to use MaxFilter instead and then
re-run the code.
Chapter 4 - Filters 87
Now the giraffe has been lightened up. You can try the MedianFilter yourself to see what happens
when you use it!
The next topic will cover MultiBand Filters.
MultiBand Filters
MultiBand filters are a copy of the Filter class. If you look in the source code for the MultibandFilter
class, you will see the following in lines 25-30 of the ImageFilter.py module:
1 class Filter:
2 pass
3
4
5 class MultibandFilter(Filter):
6 pass
There are four MultiBand filters included with Pillow at the time this book was written. They are as
follows:
Chapter 4 - Filters 88
• BoxBlur
• GaussianBlur
• Color3DLUT
• UnsharpMask
You will be using each of these filters on this photo of a Tyrannosaurus Rex:
1 class BoxBlur(MultibandFilter):
2 """Blurs the image by setting each pixel to the average value of the pixels
3 in a square box extending radius pixels in each direction.
4 Supports float radius of arbitrary size. Uses an optimized implementation
5 which runs in linear time relative to the size of the image
6 for any radius value.
7
8 :param radius: Size of the box in one direction. Radius 0 does not blur,
9 returns an identical image. Radius 1 takes 1 pixel
10 in each direction, i.e. 9 pixels in total.
11 """
12
13 name = "BoxBlur"
14
15 def __init__(self, radius):
16 self.radius = radius
17
18 def filter(self, image):
19 return image.box_blur(self.radius)
This tells you that you will need to pass in a radius to use this filter. The filter() method of the
BoxBlur() class should not be confused with the Image class’s filter() method. These methods are
distinct from each other.
You can try applying the BoxBlur filter by creating a new file named boxblur_image.py and adding
this code:
1 # boxblur_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def boxblur(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.BoxBlur(radius=2))
10 filtered_image.save(output_image)
11
12
13 if __name__ == "__main__":
14 boxblur("trex.jpg", "trex_boxblur.jpg")
Here you use the BoxBlur() filter with a radius of 2. When you run this code, you will end up with
this output:
Chapter 4 - Filters 90
It’s a very blurry photo now. You should try re-running the code with different values for the radius
to see how this filter behaves.
Now let’s move on to the Gaussian blue filter!
Pillow’s implementation of the Gaussian Blur is nice and succinct. Here it is in its entirety:
¹²https://en.wikipedia.org/wiki/Gaussian_blur
Chapter 4 - Filters 91
1 class GaussianBlur(MultibandFilter):
2 """Gaussian blur filter.
3
4 :param radius: Blur radius.
5 """
6
7 name = "GaussianBlur"
8
9 def __init__(self, radius=2):
10 self.radius = radius
11
12 def filter(self, image):
13 return image.gaussian_blur(self.radius)
This class also has a filter() method that the Image module will be using indirectly, just like the
previous MultibandFilter did. You can use that knowledge to write your code that utilizes this
blurring filter. Create a new file named gaussian_blur_image.py and enter this code:
1 # gaussian_blur_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def gaussian_blur(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.GaussianBlur)
10 filtered_image.save(output_image)
11
12
13 if __name__ == "__main__":
14 gaussian_blur("trex.jpg", "trex_gauss.jpg")
The GaussianBlur() can take in a radius amount, with 2 as its default value. That means you don’t
have to pass in a radius amount if you don’t want to. When you run this code against the dinosaur
image from earlier, you’ll end up with this:
Chapter 4 - Filters 92
Yes, it’s another blurry Tyrannosaurus Rex. While it’s not that great to look at, remember that the
Gaussian Blur isn’t necessarily for human consumption. It is used by computer vision applications
much more often.
Now let’s move on to the Color3DLUT filter!
Pillow’s implementation of the 3D lookup table is much more complex than any of the other filters
that Pillow supports. For brevity’s sake, only the definition of the __init__() method from the
Color3DLUT() class is included below:
1 class Color3DLUT(MultibandFilter):
2 """Three-dimensional color lookup table.
3
4 Transforms 3-channel pixels using the values of the channels as coordinates
5 in the 3D lookup table and interpolating the nearest elements.
6
7 This method allows you to apply almost any color transformation
8 in constant time by using pre-calculated decimated tables.
9 """
10
11 def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
This isn’t a filter that you would use on your images. This process is used as an intermediary
process. A good example is in the movie business where they use lookup tables during the Digital
Intermediate process. You can read more about it here:
Pillow includes a testing suite, which is one of the best places to go to find out how to use
Color3DLUT(). If you want to try it out anyway, you can create a new file named color_lut_-
image.py and add this code:
1 # color_lut_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def color_lut(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.Color3DLUT(2, [0, 1, 2] * 8))
10 filtered_image.save(output_image)
11
12
13 if __name__ == "__main__":
14 color_lut("trex.jpg", "trex_lut.jpg")
¹⁵https://en.wikipedia.org/wiki/Digital_intermediate
Chapter 4 - Filters 94
You are setting the size of your lookup table to 2 here. Then you create a flat lookup table, which is
a Python list of channels. This set of arguments is taken from Pillow’s own tests.
When you run this code, you will end up with this:
Most people won’t use this filter at all as it isn’t useful to them.
Let’s move on to the final filter of the day; the UnsharpMask filter!
1 class UnsharpMask(MultibandFilter):
2 """Unsharp mask filter.
3
4 See Wikipedia's entry on `digital unsharp masking`_ for an explanation of
5 the parameters.
6
7 :param radius: Blur Radius
8 :param percent: Unsharp strength, in percent
9 :param threshold: Threshold controls the minimum brightness change that
10 will be sharpened
11
12 .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digit\
13 al_unsharp_masking
14
15 """ # noqa: E501
16
17 name = "UnsharpMask"
18
19 def __init__(self, radius=2, percent=150, threshold=3):
20 self.radius = radius
21 self.percent = percent
22 self.threshold = threshold
23
24 def filter(self, image):
25 return image.unsharp_mask(self.radius, self.percent, self.threshold)
• radius
• percent
• threshold
These are nicely defaulted for you to 2, 150, and 3, respectively. You can try out the UnsharpMask()
on your image by creating a new file named unsharp_mask_image.py and adding this code:
Chapter 4 - Filters 96
1 # unsharp_mask_image.py
2
3 from PIL import Image
4 from PIL import ImageFilter
5
6
7 def unsharp_mask(input_image, output_image):
8 image = Image.open(input_image)
9 filtered_image = image.filter(ImageFilter.UnsharpMask)
10 filtered_image.save(output_image)
11
12
13 if __name__ == "__main__":
14 unsharp_mask("trex.jpg", "trex_unsharp.jpg")
When you run this code, it should sharpen the image. To make things easier to see, here is the original
image of the dinosaur:
The Tyrannosaurus Rex is much sharper after the filter was applied. Pretty neat, eh?
Now let’s take your knowledge about filters and use your code to create a GUI that can apply them.
1 # image_filter_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from blur_image import blur
10 from contour_image import contour
11 from detail_image import detail
12 from edge_enhance_image import edge_enhance
13 from emboss_image import emboss
14 from find_edges_image import find_edges
15 from PIL import Image
Note that you no longer import the conversion modules that you did back in chapter 2. Instead,
you import some of the new modules that you created in this chapter. Now you need to change the
effects dictionary next:
1 effects = {
2 "Normal": shutil.copy,
3 "Blur": blur,
4 "Contour": contour,
5 "Detail": detail,
6 "Edge Enhance": edge_enhance,
7 "Emboss": emboss,
8 "Find Edges": find_edges,
9 }
Now your effects dictionary matches the imports. You will do some refactoring at this point to
make the code you wrote originally better. This is normal in software development. You will find
that you are always improving your code when you come back to it. Or you will if you can do so
without introducing bugs.
For this example, you are going to move the effect application to its own function:
Chapter 4 - Filters 99
This code is what used to be in your main() function in the event loop portion of the code. By
moving these lines of code to their own function, you simplify the main() function. This is sometimes
called “separation of concerns”. It is usually recommended that a function should have only one
responsibility. So this will make the main() function better in the long run.
The other refactor you will do is to move the image saving code into its own function too:
1 def save_image(values):
2 save_filename = sg.popup_get_file(
3 "File", file_types=file_types, save_as=True, no_window=True
4 )
5 if save_filename == values["-FILENAME-"]:
6 sg.popup_error(
7 "You are not allowed to overwrite the original image!"
8 )
9 else:
10 if save_filename:
11 shutil.copy(tmp_file, save_filename)
12 sg.popup(f"Saved: {save_filename}")
This code is also the same as what you saw in chapter 2 except that now it is in its own function.
The last piece to update is the event loop portion of the main() function:
Chapter 4 - Filters 100
Now your conditionals are much less messy and even easier to read! Refactoring your code is a skill
you will develop and continue to improve for your entire career.
For completeness, here is the full code with all the changes in it:
1 # image_filter_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from blur_image import blur
10 from contour_image import contour
11 from detail_image import detail
12 from edge_enhance_image import edge_enhance
13 from emboss_image import emboss
14 from find_edges_image import find_edges
15 from PIL import Image
16
17 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
18
19 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
20
21 effects = {
22 "Normal": shutil.copy,
23 "Blur": blur,
24 "Contour": contour,
25 "Detail": detail,
Chapter 4 - Filters 101
69 sg.Combo(
70 effect_names, default_value="Normal", key="-EFFECTS-",
71 enable_events=True, readonly=True
72 ),
73 ],
74 [sg.Button("Save")],
75 ]
76
77 window = sg.Window("Image Filter App", layout)
78
79 while True:
80 event, values = window.read()
81 if event == "Exit" or event == sg.WIN_CLOSED:
82 break
83 if event in ["Load Image", "-EFFECTS-"]:
84 apply_effect(values, window)
85 if event == "Save" and values["-FILENAME-"]:
86 save_image(values)
87
88 window.close()
89
90
91 if __name__ == "__main__":
92 main()
Now you should be able to apply any of those filters that you have imported. When you run this
code and load an image into it, you will see something that looks like this:
Chapter 4 - Filters 103
Doesn’t that look nice? Give it a spin with your photos and see how it works. If you want a challenge,
try adding the other filters in this chapter to the GUI so you can apply them too.
Chapter 4 - Filters 104
Wrapping Up
Pillow has many filters that are included with the package. You learned how to use the ImageFilter
module as well as how to use each of these filters:
• BLUR
• CONTOUR
• DETAIL
• EDGE_ENHANCE
• EDGE_ENHANCE_MORE
• EMBOSS
• FIND_EDGES
• SHARPEN
• SMOOTH
• SMOOTH_MORE
Then you moved on to learn how to use more advanced filters that are based on RankFilter and
MultibandFilter. These filters let you do even more with your images in Pillow. Finally, you created
a simple graphical user interface with PySimpleGUI that lets you apply your filters to your images
in a user-friendly manner.
Now you can take this knowledge and use it for your photos!
Chapter 5 - Cropping, Rotating &
Resizing Images
When you’re working with photos, you may discover that you need to do some basic tweaks to them.
For example, you might take a photo that would have been better if you zoomed in a bit. Or you
might take a photo and find that its orientation is wrong. If you create websites or want to send a
photo to someone, you may want to resize the photo to a smaller size so that it can send or upload
faster.
All of these tasks can be accomplished programmatically with Pillow! In this chapter, you’ll learn
about the following topics:
To kick things off, you’ll learn about Pillow’s coordinate system in the first section!
the image. These coordinates are the starting x/y coordinates and the ending x/y coordinates of the
rectangle. Pillow will calculate the lines needed to join them together.
For example, you might have the following tuple: (0, 0, 50, 50).
This will create a rectangle that begins at your origin (the top-left) and end at 50 pixels to the right
and 50 pixels down. Sometimes it’s easier to think of the tuple as a tuple of named arguments: (Left,
Top, Right, Bottom). The Right value must be larger than the Left and the Bottom must be larger
than the Top so that you’ll create a rectangle.
Otherwise, you’ll end up with a horizontal or vertical line. This will all make sense in later chapters
when you’re using the box tuple more often.
Cropping Images
Cropping images is an art. What do you want to focus on? If you have a photo of three people, you
might want to crop one of them out. Another good example is if you take a photo of something,
but that something is not the focus of the photo. You can make it the focus by cropping the photo
appropriately.
Let’s use this photo of a praying mantis as an example:
It could be a better photo if you had zoomed in on the beetle some more. You can simulate zooming
in by cropping. Pillow supports this by passing a box tuple to the crop() method. You can see how
this works by creating a new file named crop_image.py and then entering this code:
Chapter 5 - Cropping, Rotating & Resizing Images 107
1 # crop_image.py
2
3 from PIL import Image
4
5
6 def crop_image(image_path, coords, output_image_path):
7 image = Image.open(image_path)
8 cropped_image = image.crop(coords)
9 cropped_image.save(output_image_path)
10
11
12 if __name__ == "__main__":
13 crop_image("green_mantis.jpeg",
14 (302, 101, 910, 574),
15 "cropped_mantis.jpg")
You open up the image and then you crop() it using the tuple of coordinates. You’ll need to
experiment a bit to figure out what the beginning and ending coordinates are to get a good crop
for your image. Here is the result that you’ll get when you run this code with the insect image:
Chapter 5 - Cropping, Rotating & Resizing Images 108
Now the beetle is having his or her closeup. You can make out the details of the insect better too.
The next topic to learn about is how to rotate images with Pillow!
Rotating Images
A camera doesn’t care if you’re holding it right-side-up or upside-down or even sideways. You can
make some really interesting photos by changing the orientation of the image. Of course, sometimes
you’ll take a photo in an orientation that doesn’t make sense.
When that happens, you need to rotate the photo! Pillow has this capability built-in! You’ll use this
image for your code:
Chapter 5 - Cropping, Rotating & Resizing Images 109
To see how rotation works, create a new file named rotate_image.py and enter the following:
1 # rotate_image.py
2
3 from PIL import Image
4
5
6 def rotate(image_path, degrees_to_rotate, output_image_path):
7 image_obj = Image.open(image_path)
8 rotated_image = image_obj.rotate(degrees_to_rotate)
9 rotated_image.save(output_image_path)
10
11
12 if __name__ == "__main__":
13 image = "dragonfly.jpg"
14 rotate(image, 90, "dragonfly_rotated.jpg")
In this example, you’re telling Pillow to rotate the image 90 degrees to the left. The rotate() method
can take up to four other arguments:
Chapter 5 - Cropping, Rotating & Resizing Images 110
• resample - The resampling filter to use. Defaults to PIL.Image.NEAREST. Note: Changing pixel
dimensions is known as resampling. It can degrade image quality.
• expand - If true, expands the output image to make it large enough to hold the entire rotated
image. If false or omitted, make the output image the same size as the input image. Note that
the expand flag assumes rotation around the center and no translation.
• center - The center of rotation
• translate - Post-rotate translation tuple
• fillcolor - Color for the area outside the rotated image
You can play around with these options to see how they affect the rotation of your images. When
you run the code above, you’ll end up with a dragonfly photo that looks like this:
Pillow makes rotating images quick. You could write a batch process with Pillow to turn images for
you based on the orientation found in the image’s EXIF metadata.
Let’s move on to the closely related topic of mirroring images!
Chapter 5 - Cropping, Rotating & Resizing Images 111
Mirroring Images
You can mirror an image using Pillow via its transpose() method. For this example, you’ll use this
photo of a brownish praying mantis:
Now that you have an image to use, create a new file named mirror_image.py and add this code to
it:
1 # mirror_image.py
2
3 from PIL import Image
4
5
6 def mirror(image_path, output_image_path):
7 image = Image.open(image_path)
8 mirror_image = image.transpose(Image.FLIP_LEFT_RIGHT)
9 mirror_image.save(output_image_path)
10
Chapter 5 - Cropping, Rotating & Resizing Images 112
11
12 if __name__ == "__main__":
13 image = "mantis.jpg"
14 mirror(image, "mantis_mirrored.jpg")
This code is similar to the previous example. The meat of this code is that you’re using the image
object’s transpose() method which takes one of the following constants:
• PIL.Image.FLIP_LEFT_RIGHT
• PIL.Image.FLIP_TOP_BOTTOM
• PIL.Image.TRANSPOSE
You can also use one of Pillow’s ROTATE constants here too, but you’re focusing on the mirroring
aspect of the transpose() method. Try swapping in one of these other constants into the code above
to see what happens.
If you run the code as-is, you’ll end up with the following image:
Now let’s find out how you can resize an image using Pillow!
Chapter 5 - Cropping, Rotating & Resizing Images 113
Resizing Images
There are many reasons to resize a photo. You might want to do so so that you can more easily email
the image. You might resize an image so that it loads faster on a website. Many people resize images
for a presentation too!
Some people need to resize lots of photos. Some real-world examples include processing after a major
photoshoot; preparing photos for machine learning; and post-processing photos for print.
The Pillow package can be used to resize images as well. To see how this works, create a new file
named resize_image.py and add this code to it:
1 # resize_image.py
2
3 from PIL import Image
4
5
6 def resize(input_image_path, output_image_path, size):
7 image = Image.open(input_image_path)
8 width, height = image.size
9 print(f"The original image size is {width} wide x {height} high")
10
11 resized_image = image.resize(size)
12 width, height = resized_image.size
13 print(f"The resized image size is {width} wide x {height} high")
14 resized_image.save(output_image_path)
15
16
17 if __name__ == "__main__":
18 resize(
19 input_image_path="pilot_knob.jpg",
20 output_image_path="pilot_knob_small.jpg",
21 size=(800, 400),
22 )
This code loads the passed-in image path using the Image.open() method. Then it extracts the
current size of the image and prints it out. Next, your code will call resize() and use the size
tuple that you passed in to resize the photo.
Pillow’s resize() method takes in a few other arguments, which you can see by looking at how
resize() is defined:
Chapter 5 - Cropping, Rotating & Resizing Images 114
For this example, you’ll be using this photo from the Pilot Knob State Park, which is in Iowa:
This is a nice park. If you happen to visit Iowa someday, you should go check it out. It’s mostly
woodland, but there are a couple of nice scenic lookouts. Anyway, when you run the code above on
this image, you’ll end up with the following image as your output:
Chapter 5 - Cropping, Rotating & Resizing Images 115
The problem with the resize() method is that it does not maintain the aspect ratio of the image.
What that means is that you’ll end up stretching the image, as you did in the example above.
Fortunately, you’ll learn about how to use Pillow to scale an image in the next section!
Scaling Images
Pillow provides another method you can use to resize your images. This method will maintain the
photo’s aspect ratio for you as well. The method is called thumbnail(). Here is how Pillow defines
it:
The size argument is a two-item tuple, just like the one that resize() takes. The resample argument
is also the same. There is no box argument, but there is a reducing_gap argument that defaults to
2.0 instead of None.
Chapter 5 - Cropping, Rotating & Resizing Images 116
Go ahead and create a new file called scale_image.py that you’ll use to learn how to scale images
using Pillow. Here is the code you should put in it:
1 # scale_image.py
2
3 import sys
4 from PIL import Image
5
6
7 def scale(input_image_path,
8 output_image_path,
9 width=None,
10 height=None):
11 image = Image.open(input_image_path)
12 w, h = image.size
13 print(f"The image size is {w} wide x {h} high")
14
15 if width and height:
16 max_size = (width, height)
17 elif width:
18 max_size = (width, h)
19 elif height:
20 max_size = (w, height)
21 else:
22 # No width or height specified
23 sys.exit("Width or height required!")
24
25 image.thumbnail(max_size, Image.ANTIALIAS)
26 image.save(output_image_path)
27
28 scaled_image = Image.open(output_image_path)
29 width, height = scaled_image.size
30 print(f"The scaled image size is {width} wide x {height} high")
31
32
33 if __name__ == "__main__":
34 scale(
35 input_image_path="pilot_knob.jpg",
36 output_image_path="pilot_knob_scaled.jpg",
37 width=800,
38 )
This code allows you to specify the input and output image paths. Also, you can specify the width
Chapter 5 - Cropping, Rotating & Resizing Images 117
or height that you want to scale the image down to. If you neither specify width nor height, you
exit the script with an error message using sys.exit(...).
You use the conditional statement to build your max_size tuple. This is the tuple that you’ll pass
to thumbnail(). You use the passed in width or height (or both) along with the image’s actual
width (w) and height (h) to build the max_size tuple. Once you have the tuple constructed, you call
thumbnail() with that tuple and also pass in the Image.ANTIALIAS flag which will apply a high-
quality downsampling filter which results in a better image.
Try running this code and then check the new file it creates. The image will now be scaled down
while maintaining its aspect ratio. You can try running this code using different values for width or
height and see how it changes the size of the image.
Now let’s discover how to create a GUI around some of the items that you learned!
Here is the full code that you’ll be using. You’ll go over what got changed after this code snippet:
Chapter 5 - Cropping, Rotating & Resizing Images 119
1 # image_rotator_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from mirror_image import mirror
10 from rotate_image import rotate
11 from PIL import Image
12
13 file_types = [("JPEG (*.jpg)", "*.jpg"),
14 ("All files (*.*)", "*.*")]
15
16 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
17
18 effects = {
19 "Normal": shutil.copy,
20 "Rotate 90": None,
21 "Rotate 180": None,
22 "Rotate 270": None,
23 "Mirror": mirror,
24 }
25
26 def apply_rotate(image_file, effect):
27 if effect == "Rotate 90":
28 rotate(image_file, 90, tmp_file)
29 elif effect == "Rotate 180":
30 rotate(image_file, 180, tmp_file)
31 elif effect == "Rotate 270":
32 rotate(image_file, 270, tmp_file)
33
34
35 def apply_effect(values, window):
36 selected_effect = values["-EFFECTS-"]
37 image_file = values["-FILENAME-"]
38 if os.path.exists(image_file):
39 if "Rotate" in selected_effect:
40 apply_rotate(image_file, selected_effect)
41 else:
42 effects[selected_effect](image_file, tmp_file)
43 image = Image.open(tmp_file)
Chapter 5 - Cropping, Rotating & Resizing Images 120
44 image.thumbnail((400, 400))
45 bio = io.BytesIO()
46 image.save(bio, format="PNG")
47 window["-IMAGE-"].update(data=bio.getvalue(), size=(400, 400))
48
49
50 def save_image(image_filename):
51 save_filename = sg.popup_get_file(
52 "File", file_types=file_types, save_as=True, no_window=True,
53 )
54 if save_filename == image_filename:
55 sg.popup_error(
56 "You are not allowed to overwrite the original image!",
57 )
58 else:
59 if save_filename:
60 shutil.copy(tmp_file, save_filename)
61 sg.popup(f"Saved: {save_filename}")
62
63
64 def main():
65 effect_names = list(effects.keys())
66 layout = [
67 [sg.Image(key="-IMAGE-", size=(400,400))],
68 [
69 sg.Text("Image File"),
70 sg.Input(size=(25, 1), key="-FILENAME-"),
71 sg.FileBrowse(file_types=file_types),
72 sg.Button("Load Image")
73 ],
74 [
75 sg.Text("Effect"),
76 sg.Combo(
77 effect_names, default_value="Normal", key="-EFFECTS-",
78 enable_events=True, readonly=True,
79 ),
80 ],
81 [sg.Button("Save")],
82 ]
83
84 window = sg.Window("Image Rotator App", layout, size=(450, 500))
85
86 while True:
Chapter 5 - Cropping, Rotating & Resizing Images 121
The first bit of code that you’ll want to edit in this application are the imports:
1 # image_rotator_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from mirror_image import mirror
10 from rotate_image import rotate
11 from PIL import Image
Here you drop the imports for the filters from the previous chapter and use the mirror_image and
rotate_image modules instead. You don’t need to modify the file_types or tmp_file variables.
However, you will need to update the effects dictionary to look like this:
1 effects = {
2 "Normal": shutil.copy,
3 "Rotate 90": None,
4 "Rotate 180": None,
5 "Rotate 270": None,
6 "Mirror": mirror,
7 }
This uses the mirror() function that you imported. It also maps the rotate items to the value of None.
Next, you will need to write a new function called apply_rotate(), which is below:
Chapter 5 - Cropping, Rotating & Resizing Images 122
You use this function by passing in the image file you want to modify and the effect that you want
to apply to the image.
The other change you need to add is in the apply_effect() function so that you can call apply_-
rotate():
This checks if the string, “Rotate”, is in the selected effect. If it is, then it calls apply_rotate().
Otherwise, it executes the function using the dictionary mapping directly.
Once you have finished those changes, you’re done! Give it a try on your images to see how it works!
Wrapping Up
Pillow continues to show how robust it is when it comes to working with images. In this chapter,
you learned about the following topics:
• Resizing Images
• Scaling Images
• Creating an Image Rotator GUI
Now that you know how coordinates work in Pillow, you’ll be using that knowledge in future
chapters as you learn even more about Pillow’s feature set. You’re also learning how to re-use your
code to build little GUIs. This knowledge will help you build bigger and better GUIs that you can
use for your photos. You can get started by editing some of the examples in this chapter to make
them do more!
Chapter 6 - Enhancing Images with
ImageEnhance
Pillow includes the ImageEnhance module which contains some classes you can use to enhance your
images. All of the included classes implement a common interface via the enhance() method. This
makes learning how to use ImageEnhance easier.
You can read more about these classes in the documentation¹⁷.
In this chapter, you will learn how to do the following:
You can do all of these adjustments using ImageEnhance. Let’s get started by learning how to change
the color balance of an image!
Go ahead and create a new file and name it enhance_color.py, then enter the following code in
your Python editor:
1 # enhance_color.py
2
3 from PIL import Image
4 from PIL import ImageEnhance
5
6
7 def enhance_color(image_path, enhance_factor, output_path):
8 image = Image.open(image_path)
9 enhancer = ImageEnhance.Color(image)
10 new_image = enhancer.enhance(enhance_factor)
11 new_image.save(output_path)
12
13
14 if __name__ == "__main__":
15 enhance_color("goldenrod_soldier_beetle.jpg", 0.0,
16 "beetle_color_enhanced.jpg")
Chapter 6 - Enhancing Images with ImageEnhance 126
When you run this code, you create an instance of ImageEnhance.Color(). Then you call its
enhance() method using the passed-in factor amount. In this example, you use 0.0, which will
convert the image to black and white.
Here is the result:
This conversion is a bit different than what you did when you converted from one mode to another
back in chapter 2. You should try converting this image using the convert() function from the Image
module to see how that differs. What you were doing in that chapter was converting to 1-bit pixels
(i.e. black-and-white) whereas here, you are using a blending technique.
Now go back to your code and change the factor to 0.5 instead of 0.0. Then re-run the code. Now
your image will look like this:
Chapter 6 - Enhancing Images with ImageEnhance 127
See how washed out the image is now? It’s no long black and white, but doesn’t have the vibrant
color of the original either.
Now try changing the factor to 2.5. This should push the color balance a bit out of whack. When
you run the code this time, you will end up with the following image:
Chapter 6 - Enhancing Images with ImageEnhance 128
The beetle is now mostly orange rather than yellow. The other colors in the photo have changed
quite a bit too. Here is the original image next to the enhanced image so you can see the differences
easier:
Chapter 6 - Enhancing Images with ImageEnhance 129
You should try using different factor amounts with this image or with one of your own. You can
really change the way your photo looks using this method.
Now let’s find out how to change an image’s contrast!
Now create a file named enhance_contrast.py and add this code to it:
1 # enhance_contrast.py
2
3 from PIL import Image
4 from PIL import ImageEnhance
5
6
7 def enhance_contrast(image_path, enhance_factor, output_path):
8 image = Image.open(image_path)
9 enhancer = ImageEnhance.Contrast(image)
10 new_image = enhancer.enhance(enhance_factor)
11 new_image.save(output_path)
12
13
14 if __name__ == "__main__":
15 enhance_contrast("madison_county_bridge.jpg", 2.5,
16 "madison_county_bridge_enhanced.jpg")
This code is nearly identical to the code from the last section. You change the name of the function
from enhance_color() to enhance_contrast(). You also use the Contrast() class here instead of
the Color() class from earlier.
Chapter 6 - Enhancing Images with ImageEnhance 131
When you run this example against the photo of the bridge, you will end up with the following:
This looks quite a bit different than the original. The colors are brighter and the contrast is way up.
You should try running this code with some different enhancement factor amounts. You will quickly
discover how to mute or enhance your photos’ contrast using this handy tool.
Now let’s find out how to change the brightness of your images!
To see how you can adjust the brightness of a photo with Python, open up your code editor and
create a new file named enhance_brightness.py. Now add this code to the file:
1 # enhance_brightness.py
2
3 from PIL import Image
4 from PIL import ImageEnhance
5
6
7 def enhance_brightness(image_path, enhance_factor, output_path):
8 image = Image.open(image_path)
9 enhancer = ImageEnhance.Brightness(image)
10 new_image = enhancer.enhance(enhance_factor)
11 new_image.save(output_path)
12
13
Chapter 6 - Enhancing Images with ImageEnhance 133
14 if __name__ == "__main__":
15 enhance_brightness("silver_falls.jpg", 1.5,
16 "silver_falls_enhanced.jpg")
In this code, you create an enhancer object using the Brightness() class. You use a factor of 1.5
for this example. When you run this code, you will see that the image has been brightened up
considerably:
If you had used an enhancement factor of 2.5 as you did in the previous section, the image would
have been very blown out. This one is brightened up more than necessary, but is still mostly
acceptable. You should try a few different values on this photo or on one of your own.
Now let’s find how to to adjust the sharpness of your photos using Pillow’s ImageEnhance module!
Chapter 6 - Enhancing Images with ImageEnhance 134
To see how to use a sharpness enhancement factor, create a new file named enhance_sharpness.py
and add this code:
Chapter 6 - Enhancing Images with ImageEnhance 135
1 # enhance_sharpness.py
2
3 from PIL import Image
4 from PIL import ImageEnhance
5
6
7 def enhance_sharpness(image_path, enhance_factor, output_path):
8 image = Image.open(image_path)
9 enhancer = ImageEnhance.Sharpness(image)
10 new_image = enhancer.enhance(enhance_factor)
11 new_image.save(output_path)
12
13
14 if __name__ == "__main__":
15 enhance_sharpness("hummingbird.jpg", 2.5,
16 "hummingbird_sharpened.jpg")
In this example, you use the Sharpness() class to create your enhancer object. Then you use an
enhancement factor of 2.5 to apply sharpness to the hummingbird image. The result of your work
looks like this:
Chapter 6 - Enhancing Images with ImageEnhance 136
Once again, when it comes to sharpening an image, it is very subtle. You have to look at both images
side-by-side to really see the difference. You could try using an enhancement factor of 0.0 to make
it blurry and then you would likely see what happened. You should give this one a try on a few
different images of your own and see what happens!
You can take the GUI code that you created in the previous chapter and modify it to work with your
new enhancer code. Go ahead and create a new file named image_enhancer_gui.py and copy and
paste that code into it.
Chapter 6 - Enhancing Images with ImageEnhance 138
Now let’s go over the bits you’ll need to change. The first step is to change your imports:
1 # image_enhancer_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from enhance_brightness import enhance_brightness
10 from enhance_color import enhance_color
11 from enhance_contrast import enhance_contrast
12 from enhance_sharpness import enhance_sharpness
13 from PIL import Image
Instead of importing the modules from the previous chapter, you will replace those imports with
your image enhancement modules.
The next step is to modify your effects dictionary:
1 effects = {
2 "Normal": shutil.copy,
3 "Brightness": enhance_brightness,
4 "Color": enhance_color,
5 "Contrast": enhance_contrast,
6 "Sharpness": enhance_sharpness
7 }
Here you update the dictionary to use your new enhancement functions that you created earlier in
the chapter.
To make your code more modular, you move the application of your effects to a new function named
apply_effect():
Chapter 6 - Enhancing Images with ImageEnhance 139
Here you pass in the values and window dictionaries that PySimpleGUI uses in its event loop. You
extract the currently selected effect, the loaded image file, and the current enhancement factor. Then
you apply the effect. This is similar to what you did in the last few chapters except that this time
the code is encapsulated in its own function.
The other refactor you’ll be doing is to create a save_image() function:
1 def save_image(image_filename):
2 save_filename = sg.popup_get_file(
3 "File", file_types=file_types, save_as=True, no_window=True,
4 )
5 if save_filename == image_filename:
6 sg.popup_error(
7 "You are not allowed to overwrite the original image!"
8 )
9 else:
10 if save_filename:
11 shutil.copy(tmp_file, save_filename)
12 sg.popup(f"Saved: {save_filename}")
This moves the image saving code to its own function. Otherwise, it is the same as it was the last
time you saw it.
The last item to change is in your main() function. Here is the first portion:
Chapter 6 - Enhancing Images with ImageEnhance 140
1 def main():
2 effect_names = list(effects.keys())
3 layout = [
4 [sg.Image(key="-IMAGE-", size=(400, 400))],
5 [
6 sg.Text("Image File"),
7 sg.Input(size=(25, 1), key="-FILENAME-"),
8 sg.FileBrowse(file_types=file_types),
9 sg.Button("Load Image")
10 ],
11 [
12 sg.Text("Effect"),
13 sg.Combo(
14 effect_names, default_value="Normal", key="-EFFECTS-",
15 enable_events=True, readonly=True,
16 ),
17 sg.Slider(range=(0, 5), default_value=2, resolution=0.1,
18 orientation="h", enable_events=True, key="-FACTOR-"),
19 ],
20 [sg.Button("Save")],
21 ]
22
23 window = sg.Window("Image Enhancer", layout, size=(500, 550))
This first part of the main() function sets up your user interface. The only difference here is that you
have added a new Slider Element, which you will use to set your enhancement factor.
The last piece of code to look at is your modified event handler:
1 while True:
2 event, values = window.read()
3 if event == "Exit" or event == sg.WIN_CLOSED:
4 break
5 if event in ["Load Image", "-EFFECTS-", "-FACTOR-"]:
6 apply_effect(values, window)
7 image_filename = values["-FILENAME-"]
8 if event == "Save" and image_filename:
9 save_image(image_filename)
10
11 window.close()
12
13 if __name__ == "__main__":
14 main()
Chapter 6 - Enhancing Images with ImageEnhance 141
The event handler has been refactored completely. It now either calls apply_effect(), save_image()
or exits the event loop.
At this point, you are done with all the changes. Here is the full code:
1 # image_enhancer_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from enhance_brightness import enhance_brightness
10 from enhance_color import enhance_color
11 from enhance_contrast import enhance_contrast
12 from enhance_sharpness import enhance_sharpness
13 from PIL import Image
14
15 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
16
17 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
18
19 effects = {
20 "Normal": shutil.copy,
21 "Brightness": enhance_brightness,
22 "Color": enhance_color,
23 "Contrast": enhance_contrast,
24 "Sharpness": enhance_sharpness
25 }
26
27 def apply_effect(values, window):
28 selected_effect = values["-EFFECTS-"]
29 image_file = values["-FILENAME-"]
30 factor = values["-FACTOR-"]
31 if os.path.exists(image_file):
32 if selected_effect == "Normal":
33 effects[selected_effect](image_file, tmp_file)
34 else:
35 effects[selected_effect](image_file, factor, tmp_file)
36
37 image = Image.open(tmp_file)
38 image.thumbnail((400, 400))
Chapter 6 - Enhancing Images with ImageEnhance 142
39 bio = io.BytesIO()
40 image.save(bio, format="PNG")
41 window["-IMAGE-"].update(data=bio.getvalue(), size=(400, 400))
42
43
44 def save_image(image_filename):
45 save_filename = sg.popup_get_file(
46 "File", file_types=file_types, save_as=True, no_window=True,
47 )
48 if save_filename == image_filename:
49 sg.popup_error(
50 "You are not allowed to overwrite the original image!"
51 )
52 else:
53 if save_filename:
54 shutil.copy(tmp_file, save_filename)
55 sg.popup(f"Saved: {save_filename}")
56
57
58 def main():
59 effect_names = list(effects.keys())
60 layout = [
61 [sg.Image(key="-IMAGE-", size=(400, 400))],
62 [
63 sg.Text("Image File"),
64 sg.Input(size=(25, 1), key="-FILENAME-"),
65 sg.FileBrowse(file_types=file_types),
66 sg.Button("Load Image")
67 ],
68 [
69 sg.Text("Effect"),
70 sg.Combo(
71 effect_names, default_value="Normal", key="-EFFECTS-",
72 enable_events=True, readonly=True,
73 ),
74 sg.Slider(range=(0, 5), default_value=2, resolution=0.1,
75 orientation="h", enable_events=True, key="-FACTOR-"),
76 ],
77 [sg.Button("Save")],
78 ]
79
80 window = sg.Window("Image Enhancer", layout, size=(500, 550))
81
Chapter 6 - Enhancing Images with ImageEnhance 143
82 while True:
83 event, values = window.read()
84 if event == "Exit" or event == sg.WIN_CLOSED:
85 break
86 if event in ["Load Image", "-EFFECTS-", "-FACTOR-"]:
87 apply_effect(values, window)
88 image_filename = values["-FILENAME-"]
89 if event == "Save" and image_filename:
90 save_image(image_filename)
91
92 window.close()
93
94
95 if __name__ == "__main__":
96 main()
Go ahead and give the application a try. You will find that it works well, although there is a slight
delay as it processes the enhancements. You will learn how to fix that issue using threads in a future
chapter. For now though, you can use this application to try out different enhancement factors.
Note that the enhancement factors are cumulative. So if you change the enhancement factor and the
selection, it will be applied to the image that is currently shown.
If you want to apply it on the original image, you need to choose “Normal” before changing the
combo box selection.
Wrapping Up
Pillow comes with some nice and basic image enhancement tools in its ImageEnhance module. You
learned how to use this module to do the following:
You can take the knowledge you learned in this chapter and create your own tools for enhancing
your photos. Try out the classes from ImageEnhance and see if you can improve some of your photos!
Chapter 7 - Combining Images
There are times when you will want to combine two images. For example, you might want to put
before and after photos together for comparison. Or you may want to watermark an image with
your company’s logo. This is sometimes called compositing two images together.
The Pillow package lets you combine images in a couple of different ways. You will learn how to
use the Image module’s paste(), blend() and composite() methods in the following sections:
• Pasting an Image
• Tiling Images
• Concatenating Images
• Watermarking an Image
• Blending Images
• Compositing Images
• Creating a Watermark GUI
These sections will teach you how to use Pillow to combine images effectively. Let’s get started by
learning how to use the versatile paste() method!
Pasting an Image
The Pillow package provides methods called copy() and paste(). These methods copy and paste
Image objects. They do not use your computer’s clipboard. What that means is that if you were
to call Pillow’s copy() method, you couldn’t paste it into another program, like Microsoft Word,
because Pillow doesn’t copy the object into your computer’s clipboard.
The copy() method is used when you wish to paste things into an image but still retain the original.
The paste() method will paste another image into the Image object that is calling paste(). The
paste() method takes in three arguments:
Now you’re ready to learn how to use paste(). For this example, you will be re-using this
hummingbird photo from the previous chapter:
Open up your code editor and create a new file named image_paste.py and add this code to it:
1 # image_paste.py
2
3 from PIL import Image
4
5
6 def paste_demo(image_path, output_path, crop_coords):
7 image = Image.open(image_path)
8 cropped_image = image.crop(crop_coords)
9 image.paste(cropped_image, (0, 0))
10 image.save(output_path)
11
Chapter 7 - Combining Images 146
12
13 if __name__ == "__main__":
14 coords = (125, 712, 642, 963)
15 paste_demo("hummingbird.jpg", "hummingbird_pasted.jpg", coords)
In this code, you take in an image_path, an output_path, and a box tuple called coords. That last
argument tells Pillow what part of the image to crop out, which in this case is just the hummingbird
itself. Once you have opened the image and cropped out a portion of it, you use the paste() method
to paste the cropped part back onto the original image at position (0, 0), which is at the top left.
Note that the crop() method returns the cropped version as a new image, and paste affects only
the image in memory, not on disk – which is why you have to explicity save the image.
When you run this code, you will get this as your result:
That’s a neat trick, but you can do more with paste(). Let’s see how you can use it to create a tiled
image!
Tiling Images
You can use Image module’s paste() method to create a “tiled” version of an image. Tiling is
where you take an image and repeat it multiple times across an image. You will continue to use
the hummingbird image from the previous section for this example too.
Chapter 7 - Combining Images 147
To see how this works, create a new file named image_tiling.py and add the following code:
1 # image_tiling.py
2
3 from PIL import Image
4
5
6 def image_tiling(image_path, output_path, crop_coords):
7 image = Image.open(image_path)
8 width, height = image.size
9 new_image = Image.new('RGB', (width, height))
10
11 cropped_image = image.crop(crop_coords)
12 cropped_width, cropped_height = cropped_image.size
13
14 for left_pos in range(0, width, cropped_width):
15 for top_pos in range(0, height, cropped_height):
16 new_image.paste(cropped_image, (left_pos, top_pos))
17 new_image.save(output_path)
18
19
20 if __name__ == "__main__":
21 coords = (125, 712, 642, 963)
22 image_tiling("hummingbird.jpg", "hummingbird_tiled.jpg", coords)
Here you crop the image as you did in the previous example. But you also get the size of the original
image using the size attribute. Then you loop over the image’s width and height using a nested
loop. When you loop, you use the cropped version’s size information to step left-to-right and top-
to-bottom to prevent overlapping the tiles. What this code should do is copy and paste the cropped
image as tiles across the entire image of the original and then save it with a different name so you
don’t overwrite the original.
When you run this code, you will have this image as your result:
Chapter 7 - Combining Images 148
Now you have a tiled hummingbird image! With a little practice, you can use this knowledge to
created tiled images of your own. Let’s move on and learn about a similar topic: concatenating
images!
Concatenating Images
Pillow also lets you take two images and combine or concatenate them into one. You can do this
by concatenating them vertically or horizontally. This works best if both images are the same size.
When they aren’t the same size, you would need to do one of the following:
The simplest way to concatenate two images is if they are both the same size. You will be using two
different images taken of Silver Falls in Oregon for this example.
You will discover how concatenating works by creating a new file named concatenating_images.py
and adding this code to it:
Chapter 7 - Combining Images 149
1 # concatenating_images.py
2
3 from PIL import Image
4
5
6 def concatenate_vertically(
7 first_image_path, second_image_path, output_image_path,
8 ):
9 image_one = Image.open(first_image_path)
10 image_two = Image.open(second_image_path)
11 height = image_one.height + image_two.height
12 new_image = Image.new("RGB", (image_one.width, height))
13
14 new_image.paste(image_one, (0, 0))
15 new_image.paste(image_two, (0, image_one.height))
16
17 new_image.save(output_image_path)
18
19
20 def concatenate_horizontally(
21 first_image_path, second_image_path, output_image_path,
22 ):
23 image_one = Image.open(first_image_path)
24 image_two = Image.open(second_image_path)
25 width = image_one.width + image_two.width
26 new_image = Image.new("RGB", (width, image_one.height))
27
28 new_image.paste(image_one, (0, 0))
29 new_image.paste(image_two, (image_one.width, 0))
30
31 new_image.save(output_image_path)
32
33
34 if __name__ == "__main__":
35 concatenate_horizontally("silver_falls.jpg",
36 "silver_falls2.jpg",
37 "silver_h_combined.jpg")
The code in each of these two functions is quite similar but worth going over separately as the
differences are kind of subtle. Here is the first function you will learn about:
1 # concatenating_images.py
2
3 from PIL import Image
4
5
6 def concatenate_vertically(
7 first_image_path, second_image_path, output_image_path,
8 ):
9 image_one = Image.open(first_image_path)
10 image_two = Image.open(second_image_path)
11 height = image_one.height + image_two.height
12 new_image = Image.new("RGB", (image_one.width, height))
13
14 new_image.paste(image_one, (0, 0))
15 new_image.paste(image_two, (0, image_one.height))
16
17 new_image.save(output_image_path)
Here you open up the two images. Because you are stacking the images vertically here, you need
to calculate the total height. Then you can use the Image module’s new() function to create a new
image with that combined height, but the regular width. Next, you paste the two images onto your
new image object. The first one is pasted at position 0, 0. The second one is pasted at 0 with an offset
based on the height of the first image.
When you run this function using the provided images you will get an image like this:
Chapter 7 - Combining Images 151
That’s a nice tall image of waterfalls! Now you can turn your attention to the other function:
1 def concatenate_horizontally(
2 first_image_path, second_image_path, output_image_path,
3 ):
4 image_one = Image.open(first_image_path)
5 image_two = Image.open(second_image_path)
6 width = image_one.width + image_two.width
7 new_image = Image.new("RGB", (width, image_one.height))
8
9 new_image.paste(image_one, (0, 0))
10 new_image.paste(image_two, (image_one.width, 0))
11
12 new_image.save(output_image_path)
This function works in much the same way as the first one. The biggest difference here is that instead
of calculating the total height, you are calculating the total width. The height should be the same
across the images since they are the same size.
Then when you paste the two images in, you need to make sure that the second image you paste in
starts where the first image stops. You do this by using the width of the first image as the first item
in the position tuple. When you run this code, you will have the following output:
Chapter 7 - Combining Images 152
These functions could be made better by adding a quick check to verify that the images have the
same size. If they don’t, the functions could raise an exception or return early.
Now you may be wondering how you would concatenate images that aren’t the same size. You can
use the hummingbird photo for this example along with one of the waterfall photos as these two
are not the same size. Go ahead and create a new file named concatenating_mismatched_images.py
and add this code to see one way to do it:
1 # concatenating_mismatched_images.py
2
3 from PIL import Image
4
5
6 def concatenate_vertically(
7 first_image_path, second_image_path, output_image_path,
8 ):
9 image_one = Image.open(first_image_path)
10 image_two = Image.open(second_image_path)
11 height = image_one.height + image_two.height
12 width = min(image_one.width, image_two.width)
13 new_image = Image.new("RGB", (width, height))
14
Chapter 7 - Combining Images 153
In this code, you add a new line to each function where you choose the minimum width or height.
Then you use it when you create the new image. What this does is it effectively cuts the larger image
to fit the smaller one and then aligns to the top or left edge depending on which function you run.
Here is the result of running the horizontal concatenation code:
Chapter 7 - Combining Images 154
You can see that the waterfall was cropped to fit the hummingbird photo. Try running the vertical
concatenation code and see what you end up with on your own!
Now let’s find out how you can watermark a photo using Pillow!
Watermarking an Image
A watermark is a marking of ownership on your intellectual property. You will usually see
watermarks on photos and videos. It’s a type of digital rights management (DRM) that isn’t very
intrusive to the user. Pillow provides a couple of different ways to add a watermark to an image.
You can draw text on an image or you can use the paste() method. You will learn how to draw text
in chapter 8. Here you will focus on the paste() method.
You will be using the following logo to watermark the hummingbird image:
Now that you have a logo to use, go create a new file and name it watermark.py. Then add this code
to it
1 # watermark.py
2
3 from PIL import Image
4
5
6 def watermark(
7 input_image_path, output_image_path,
8 watermark_image_path, position,
9 ):
10 base_image = Image.open(input_image_path)
11 watermark_image = Image.open(watermark_image_path)
12 # add watermark to your image
13 base_image.paste(watermark_image, position)
14 base_image.save(output_image_path)
15
16
17 if __name__ == "__main__":
18 watermark("hummingbird.jpg", "hummingbird_watermarked.jpg",
19 "logo.png", position=(0, 0))
This code has a watermark() function that takes in the following arguments:
Your code here is pretty similar to the other paste() code you have been using. This time though, you
aren’t creating a new image. You are opening the input image file and then pasting the watermark
image file into it. Then you save the image to a new location.
The result looks like this:
Chapter 7 - Combining Images 156
Oops! That doesn’t look right! The pasted-in image doesn’t account for transparency. So instead
of getting just the watermark, you get the watermark and its background too. Fortunately, Pillow
supports the concept of masks. The mask is a way to control the transparency of the image that you
are pasting.
Create a new file named watermark_transparent.py and copy your code into it. Then update the
code as follows:
1 # watermark_transparent.py
2
3 from PIL import Image
4
5
6 def watermark_with_transparency(
7 input_image_path, output_image_path,
8 watermark_image_path, position,
9 ):
10 base_image = Image.open(input_image_path)
11 watermark = Image.open(watermark_image_path)
12 width, height = base_image.size
13 transparent = Image.new("RGB", (width, height), (0, 0, 0, 0))
Chapter 7 - Combining Images 157
The key update here is that you extract the size of the image that you are adding the watermark to.
Then you use that information to create a new image of the same size. Next, you paste the input
image onto it. Then you paste the watermark image on and set the mask argument to also be the
watermark image object. Finally, you save the result to disk.
The watermark image needs to be in a file format that supports transparency, such as PNG. If you
don’t use one, you may receive an error from Pillow.
When you run this version of the code, your output image will look like this:
That’s much better! The background of the watermark has been masked and the watermark is applied
correctly. Now let’s discover how Pillow’s composite() works!
Compositing Images
Compositing an image, according to Pillow, is “blending images using a transparency mask”. You
can check out the docstring for composite() below:
That might sound familiar. Didn’t you just do that in the watermarking section? If you look at
the code above, you can see that composite() is almost exactly what you did in the watermarking
section with one important difference: composite() requires that the images are the same mode and
size and you also have to use a mask. If you’d like to learn about the modes that Pillow supports, see
the Pillow handbook¹⁸.
In other words, using paste() is more flexible than using composite(). To get a proper understand-
ing of how this works, you need a couple of images. You will be re-using the photo of Pilot Knob
from the previous chapter:
¹⁸https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes
Chapter 7 - Combining Images 159
Now you should create a file named composite_images.py and enter the following:
1 # composite_images.py
2
3 from PIL import Image
4
5
6 def composite_image(input_image_path, input_image_path_2, output_path):
7 image1 = Image.open(input_image_path)
8 image2 = Image.open(input_image_path_2).resize(image1.size)
9 mask = Image.new("L", image1.size, 120)
10 composited_images = Image.composite(image1, image2, mask)
11 composited_images.save(output_path)
12
13
14 if __name__ == "__main__":
15 composite_image("pilot_knob.jpg", "grasshopper.jpg", "composited.jpg")
To make sure the images are the exact same size, you use the resize() method to resize the second
image to match the first one. Next, you create a mask. You could use the second image as your mask
Chapter 7 - Combining Images 161
as you did in the watermarking section, but this is more about mixing the two photos together rather
than watermarking. Instead, you can create a new image in L mode, which is 8-bit pixels. You set
the color to 120.
Finally, you create the composite image. When you run this code, you’ll end up with the following:
You can set the color anywhere from 0-255. Try using different values in this code and then re-run
it to see how it changes the composited result.
Now it’s time to move on and learn about the art of blending images together.
Blending Images
Pillow supports blending two photos together. According to the docstring for blend(), that means
the following:
Chapter 7 - Combining Images 162
There are a couple of key takeaways here. First, the blend() method works on two images at a time.
Second, the two images must be in the same mode and size as each other.
The first step in blending some images is finding two images that are the same size. For this example,
the first image you will be using is a photo of the sky:
Chapter 7 - Combining Images 163
The next step is to write some code. Create a new file named blend_images.py and add this code to
it:
1 # blend_images.py
2
3 from PIL import Image
4
5
6 def blend(input_image_path, input_image_path_2, output_path, alpha):
7 image1 = Image.open(input_image_path).convert("RGBA")
8 image2 = Image.open(input_image_path_2).convert("RGBA")
9 if image1.size != image2.size:
10 print("ERROR: Images are not the same size!")
11 return
12 blended_image = Image.blend(image1, image2, alpha)
13 blended_image.save(output_path)
14
15 if __name__ == "__main__":
16 blend("skyline.png", "shell.png", "blended.png", alpha=0.2)
The first item to take note of is that you are checking to verify that both of the image’s sizes are the
Chapter 7 - Combining Images 165
same. This could be improved to also check if the mode is the same. Then you call blend() with the
images and the alpha amount that you want to apply.
When you run this code, your output image will look like this:
Try changing the alpha amount to 0.4 and re-run the code. If you do, you’ll see that the shell become
less transparent:
Chapter 7 - Combining Images 166
That’s pretty neat! Go ahead and try out some other alpha amounts and see how they affect this
example or try out this code with some of your own images! Now let’s find out how to create a GUI
that lets your users apply watermarks to their images!
To get started, you’ll need to create a file named watermark_gui.py. Next, you’ll have to add the
following code:
Chapter 7 - Combining Images 168
1 # watermark_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from PIL import Image
10 from watermark_transparent import watermark_with_transparency
11
12 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
13 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
14
15 def convert_image(image_path):
16 image = Image.open(image_path)
17 image.thumbnail((400, 400))
18 bio = io.BytesIO()
19 image.save(bio, format="PNG")
20 return bio.getvalue()
21
22
23 def create_row(label, key, file_types, save=False):
24 if save:
25 return [
26 sg.Text(label),
27 sg.Input(size=(25, 1), key=key),
28 sg.FileSaveAs(file_types=file_types),
29 ]
30 else:
31 return [
32 sg.Text(label),
33 sg.Input(size=(25, 1), key=key),
34 sg.FileBrowse(file_types=file_types),
35 ]
36
37
38 def apply_watermark(original_image, values, position, window):
39 watermark_with_transparency(
40 original_image, tmp_file, values["-WATERMARK-"], position,
41 )
42 photo_img = convert_image(tmp_file)
43 window["-IMAGE-"].update(data=photo_img, size=(400,400))
Chapter 7 - Combining Images 169
44
45
46 def check_for_errors(values):
47 if not values["-FILENAME-"]:
48 sg.Popup("Error", "Image file not loaded!")
49 return True
50 if not values["-WATERMARK-"]:
51 sg.Popup("Error", "Watermark file not loaded!")
52 return True
53 if not values["-WATERMARK-X-"] or not values["-WATERMARK-Y-"]:
54 sg.Popup("Error", "Watermark position not set completely")
55 return True
56 return False
57
58
59 def save_image(values):
60 save_filename = sg.popup_get_file(
61 "File", file_types=file_types, save_as=True, no_window=True,
62 )
63 if save_filename == values["-FILENAME-"]:
64 sg.popup_error(
65 "You are not allowed to overwrite the original image!",
66 )
67 else:
68 if save_filename:
69 shutil.copy(tmp_file, save_filename)
70 sg.popup(f"Saved: {save_filename}")
71
72
73 def main():
74 original_image = None
75 layout = [
76 [sg.Image(key="-IMAGE-", size=(400,400))],
77 create_row("Image File:", "-FILENAME-", file_types),
78 create_row("Watermark File:", "-WATERMARK-",
79 [("PNG (*.png)", "*.png")]),
80 [sg.Button("Load Image")],
81 [
82 sg.Text("Watermark Position"),
83 sg.Text("X:"),
84 sg.Input("0", size=(5, 1), enable_events=True,
85 key="-WATERMARK-X-"),
86 sg.Text("Y:"),
Chapter 7 - Combining Images 170
130
131
132 if __name__ == "__main__":
133 main()
That’s a big chunk of code! To help you sort it out, you will go over this code in smaller pieces.
Here are the first few lines:
1 # watermark_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from PIL import Image, ImageTk
10 from watermark_transparent import watermark_with_transparency
11
12 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
13 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
Here you import the various items that you need. Make sure you import the watermark_transparent
module instead of the watermark module. You want your logo to be pasted with transparency.
Otherwise, you’ll end up with a black box around your logo, as you saw in the watermark section.
The next line will be used by the FileBrowse Element, which is used to open a File Dialog so that
the user can pick a file. You are defaulting the file type to JPG, but allowing the user to switch to
All files if they want to. You could update that so that it can only load the image types that Pillow
supports, but for brevity, this example only shows those two options.
Finally, you create a tmp_file to save the interim image to after applying the watermark.
The first function that you’ll want to look at is convert_image():
1 def convert_image(image_path):
2 image = Image.open(image_path)
3 image.thumbnail((400, 400))
4 bio = io.BytesIO()
5 image.save(bio, format="PNG")
6 return bio.getvalue()
This function takes in a path to an image. It resizes the image down to a maximum of 400 x 400
pixels, but it does this while maintaining the image’s aspect ratio. That last line converts the Pillow
Image object into something that can be displayed by PySimpleGUI.
Chapter 7 - Combining Images 172
Here you are creating a Python list that contains three PySimpleGUI elements. These make up a row.
If you go back to the beginning of this section and look at the screenshot of the GUI you are making,
you will see that you need three rows that are very similar. They have a label (sg.Text), a text entry
box (sg.Input), and a browse button (sg.FileBrowse). Your create_row() function is what creates
each of those rows!
The last row has a “Save As” button. This function also takes care of that for you if you set save to
True.
Now it’s time to find out how you apply the watermark in your GUI:
This is where you call the watermark_with_transparency() function that you imported from the
watermark code that you wrote earlier in this chapter. The values argument is a dictionary that
contains the file paths that you need to pass to the watermark code. The window argument is a
dictionary that contains all the Elements in your GUI. You use it here to update the Image Element
so that you can see the watermark after it’s been applied.
The next function to go over is check_for_errors():
Chapter 7 - Combining Images 173
1 def check_for_errors(values):
2 if not values["-FILENAME-"]:
3 sg.Popup("Error", "Image file not loaded!")
4 return True
5 if not values["-WATERMARK-"]:
6 sg.Popup("Error", "Watermark file not loaded!")
7 return True
8 if not values["-WATERMARK-X-"] or not values["-WATERMARK-Y-"]:
9 sg.Popup("Error", "Watermark position not set completely")
10 return True
11 return False
The purpose of this function is to look for some error conditions and display an error to the user if
one of those conditions occurs. This function verifies that all the Elements in your GUI have data in
them as they are all required to apply a watermark successfully. If any of those items is blank, then
it will display a message to the user.
The next function you need to look over is the save() function:
1 def save_image(values):
2 save_filename = sg.popup_get_file(
3 "File", file_types=file_types, save_as=True, no_window=True,
4 )
5 if save_filename == values["-FILENAME-"]:
6 sg.popup_error(
7 "You are not allowed to overwrite the original image!",
8 )
9 else:
10 if save_filename:
11 shutil.copy(tmp_file, save_filename)
12 sg.popup(f"Saved: {save_filename}")
This function lets you ask the user where to save the watermarked image to. It also prevents the user
from trying to overwrite the original image.
The last function to learn about is your main() function:
Chapter 7 - Combining Images 174
1 def main():
2 original_image = None
3 layout = [
4 [sg.Image(key="-IMAGE-", size=(400,400))],
5 create_row("Image File:", "-FILENAME-", file_types),
6 create_row("Watermark File:", "-WATERMARK-",
7 [("PNG (*.png)", "*.png")]),
8 [sg.Button("Load Image")],
9 [
10 sg.Text("Watermark Position"),
11 sg.Text("X:"),
12 sg.Input("0", size=(5, 1), enable_events=True,
13 key="-WATERMARK-X-"),
14 sg.Text("Y:"),
15 sg.Input("0", size=(5, 1), enable_events=True,
16 key="-WATERMARK-Y-"),
17 ],
18 [
19 sg.Button("Apply Watermark", enable_events=True),
20 sg.Button("Save Image", enable_events=True),
21 ],
22 ]
23
24 window = sg.Window("Watermark GUI", layout)
This is the meat of your program. The above code is the first half of your main() function. This code
creates your user interface. It adds your Image() control, which is used to display the image to the
user. It also creates the three rows of widgets using the create_row() function. It also adds a fourth
row that you use to position the watermark. At the bottom of your user interface, you add an “Apply
Watermark” button.
Lastly, you add all your Elements to the Window(), which will layout your user interface and display
it to the user.
The last piece of code is the event loop:
Chapter 7 - Combining Images 175
1 while True:
2 event, values = window.read()
3
4 if event == "Exit" or event == sg.WIN_CLOSED:
5 break
6
7 watermark_x = values["-WATERMARK-X-"]
8 watermark_y = values["-WATERMARK-Y-"]
9
10 if event == "Load Image":
11 filename = values["-FILENAME-"]
12 if os.path.exists(filename):
13 photo_img = convert_image(filename)
14 window["-IMAGE-"].update(data=photo_img, size=(400,400))
15 original_image = filename
16 shutil.copy(original_image, tmp_file)
17 if event in ["-WATERMARK-X-", "-WATERMARK-Y-"]:
18 # filter watermark position to integers
19 if watermark_x and watermark_y:
20 if not watermark_x[-1].isdigit():
21 window["-WATERMARK-X-"].update(watermark_x[:-1])
22 if not watermark_y[-1].isdigit():
23 window["-WATERMARK-Y-"].update(watermark_y[:-1])
24 if event == "Apply Watermark":
25 if check_for_errors(values):
26 continue
27 position = (int(watermark_x), int(watermark_y))
28 apply_watermark(original_image, values, position, window)
29 if event == "Save Image" and values["-FILENAME-"]:
30 save_image(values)
31
32 window.close()
33
34
35 if __name__ == "__main__":
36 main()
Here you run an infinite loop that checks for user events. If the user clicks a button, you will check
to see which button was clicked on and act accordingly. If the user opens the image that you want to
apply a watermark to, then your event will equal “file” and you’ll run the convert_image() function
to convert the image into something that PySimpleGUI can display.
If the event is “watermark-x” or “watermark-y”, then you run a filter to make sure the user only
enters integers.
Chapter 7 - Combining Images 176
Next you check if the event equals “apply”. If so, then you check for errors. If there are no errors,
then you apply the watermark to the image and update the display so the user can see where the
watermark has been applied.
The last conditional is used for saving the watermark and is only triggered if the user presses the
“Save” button AND an image has been loaded.
Now you have a fully functional application that you can use for adding watermarks to your images!
Wrapping Up
In this chapter, you learned how to combine photos in several different ways. Specifically, you
learned about the following topics:
• Pasting an Image
• Tiling Images
• Concatenating Images
• Watermarking an Image
• Blending Images
• Compositing Images
• Creating a Watermark GUI
Learning how to use paste(), blend() and composite() effectively can allow you to create really
interesting combinations of images. You should try using what you have learned in this chapter on
your own photos and see what you can come up with. You can also try your hand at creating a GUI
that lets you blend or composite a couple of images together rather than only watermarking images.
Chapter 8 - Drawing Shapes
Pillow provides a drawing module called ImageDraw that you can use to create simple 2D graphics
on your Image objects. According to Pillow’s documentation, “you can use this module to create new
images, annotate or retouch existing images, and to generate graphics on the fly for web use.”
If you need more advanced drawing capabilities than what is included in Pillow, you can get a
separate package called aggdraw¹⁹.
You will focus on what comes with Pillow in this chapter. Specifically, you will learn about the
following:
• Common Parameters
• Drawing Lines
• Drawing Arcs
• Drawing Chords
• Drawing Ellipses
• Drawing Pie Slices
• Drawing Polygons
• Drawing Rectangles
• Creating a Drawing GUI
When drawing with Pillow, it uses the same coordinate system that you have been using with the
rest of Pillow. The upper left corner is still (0,0), for example. If you draw outside of the image bounds,
those pixels will be discarded.
If you want to specify a color, you can use a series of numbers or tuples as you would when
using PIL.Image.new(). For “1”, “L”, and “I” images, use integers. For “RGB” images, use a 3-tuple
containing integer values. You may also use the color names that are supported by Pillow that you
learned about in chapter 2.
Common Parameters
When you go to use the various drawing methods, you will discover that they have a lot of common
parameters that they share. Rather than explain the same parameters in every section, you will learn
about them up-front!
¹⁹https://github.com/pytroll/aggdraw
Chapter 8 - Drawing Shapes 178
xy
Most of the drawing methods have an xy parameter that sets a rectangular area in which to draw a
figure. This can be defined in the following two ways:
• ((upper left x, upper left y), (lower right x, lower right y)) or simply ((x1, y1), (x2, y2))
• A box tuple of (x1, y1, x2, y2)
When it comes to drawing a line, polygon, or point, multiple coordinates are specified in either of
these ways:
The line() method will draw a straight line, connecting each point. The polygon() will draw a
polygon where each point is connected. Finally, the point() will draw a point of 1-pixel at each
point.
fill
The parameter, fill, is used to set the color that will fill the shape. The way you set the fill is
determined by the mode of the image:
• RGB: Set each color value (0-255) using (R, G, B) or a color name
• L (grayscale): Set a value (0-255) as an integer
outline
The outline sets the border color of your drawing. Its specification is the same as the one you use
for fill.
The default is None, which means no border.
Now that you know about the common parameters, you can move on and learn how to start drawing!
Drawing Lines
The first type of drawing you will learn about is how to draw lines in Pillow. All shapes are made
up of lines. In Pillow’s case, a line is drawn by telling Pillow the beginning and ending coordinates
to draw the line between. Alternatively, you can pass in a series of XY coordinates and Pillow will
draw lines to connect the points.
Following is the line() method definition:
Chapter 8 - Drawing Shapes 179
You can see that it accepts several different parameters. You learned what some of these parameters
mean in the previous section. The width parameter is used to control the width of the lines.
Before you learn how to use joint, you should learn how to draw lines without it. But first, you
will need an image to draw on. You will use this image of one of the Madison County bridges:
Now go open up your Python editor and create a new file named draw_line.py and add this code
to it:
1 # draw_line.py
2
3 import random
4 from PIL import Image, ImageDraw
5
6
7 def line(image_path, output_path):
8 image = Image.open(image_path)
9 draw = ImageDraw.Draw(image)
Chapter 8 - Drawing Shapes 180
Here you open up the image in Pillow and then pass the Image object to ImageDraw.Draw(), which
returns an ImageDraw object. Now you can draw lines on your image. In this case, you use a for loop
to draw five lines on the image. The first line starts at (0,0) in the first loop. Then the X position
changes in each iteration. The endpoint is the size of the image, or the lower-right corner.
You use the random module to choose a random color from a list of colors. When you run this code,
the output will look something like this:
Note: You may see some artifacts around your drawings. You may be able to resolve this by
saving in a different image format, such as PNG.
Chapter 8 - Drawing Shapes 181
Now you can try creating a series of points and drawing lines that way. Create a new file named
draw_jointed_line.py and put this code in your file:
1 # draw_jointed_line.py
2
3 from PIL import Image, ImageDraw
4
5
6 def line(output_path):
7 image = Image.new("RGB", (400, 400), "red")
8 points = [(100, 100), (150, 200), (200, 50), (400, 400)]
9 draw = ImageDraw.Draw(image)
10 draw.line(points, width=15, fill="green", joint="curve")
11 image.save(output_path)
12
13 if __name__ == "__main__":
14 line("jointed_lines.jpg")
This time you create an image using Pillow rather than drawing on one of your own. Then you
create a list of points. To make the line connections look nicer, you can set the joint parameter to
“curve”. If you look at the source code for the line() method, you will find that “curve” is the only
valid value to give it other than None. This may change in a future version of Pillow.
When you run this code, your image will look like this:
Chapter 8 - Drawing Shapes 182
Now try removing the joint parameter from your code and re-run the example. Your output will
now look like this:
Chapter 8 - Drawing Shapes 183
By setting joint to “curve”, the output will be slightly more pleasing to the eye.
Now you’re ready to learn about drawing arcs with Pillow!
Drawing Arcs
An arc is a curved line. You can draw arcs with Pillow too. Here is the arc() method specification:
As a reminder, xy describes the box the arc will be drawn in, and the arc itself will be some portion
of the circle/ellipse that exactly fits inside that box. The start parameter defines the starting angle,
in degrees, from the center of the circle, with 0 being horizontal to the right and 180 being horizontal
to the left. The end parameter, also in degrees, tells Pillow what the ending angle is. The other two
parameters are ones that have already been introduced.
To see how you might draw an arc, create a new file named draw_arc.py and add this code to it:
Chapter 8 - Drawing Shapes 184
1 # draw_arc.py
2
3 from PIL import Image, ImageDraw
4
5
6 def arc(output_path):
7 image = Image.new("RGB", (400, 400), "white")
8 draw = ImageDraw.Draw(image)
9 draw.arc((25, 50, 175, 200), start=30, end=250, fill="green")
10
11 draw.arc((100, 150, 275, 300), start=20, end=100, width=5,
12 fill="yellow")
13
14 image.save(output_path)
15
16 if __name__ == "__main__":
17 arc("arc.jpg")
In this code, you create a new image with a white background. Then you create your Draw object.
Next, you create two different arcs. The first arc will be filled with green. The second arc will be
filled in yellow, but its line width will be 5. When you draw an arc, the fill is referring to the arc’s
line color. You aren’t filling the arc itself.
When you run this code, your output image will look like this:
Chapter 8 - Drawing Shapes 185
Try changing some of the parameters and re-running the code to see how you can change the arcs
yourself.
Now let’s move on and learn about drawing chords!
Drawing Chords
Pillow also supports the concept of chords. A chord is the same as an arc except that the endpoints
are connected with a straight line.
Here is the method definition of chord():
The only difference here is that you can also add an outline color. This color can be specified in
any of the ways that you can specify a fill color.
Create a new file and name it draw_chord.py. Then add this code so you can see how you make
chords yourself:
Chapter 8 - Drawing Shapes 186
1 # draw_chord.py
2
3 from PIL import Image, ImageDraw
4
5
6 def chord(output_path):
7 image = Image.new("RGB", (400, 400), "green")
8 draw = ImageDraw.Draw(image)
9 draw.chord((25, 50, 175, 200), start=30, end=250, fill="red")
10
11 draw.chord((100, 150, 275, 300), start=20, end=100, width=5,
12 fill="yellow", outline="blue")
13
14 image.save(output_path)
15
16 if __name__ == "__main__":
17 chord("chord.jpg")
This example will draw two chords on a green image. The first chord is filled in with a red color.
The second chord is filled in with yellow but is outlined in blue. The blue outline has a width of 5.
When you run this code, you will create the following image:
Chapter 8 - Drawing Shapes 187
That looks pretty good. Go ahead and play around with this example too. You’ll soon master chord
making with Pillow with a little practice.
Now let’s continue and learn about drawing ellipses!
Drawing Ellipses
An ellipse, or oval, is drawn in Pillow by giving it a bounding box (xy). You have seen this several
other times in previous sections.
Here is the ellipse() method definition:
The ellipse() lets you fill it with a color, add a colored border (outline) and change the width of
that outline.
To see how you can create an ellipse(), make a new file named draw_ellipse.py and add this code
to it:
Chapter 8 - Drawing Shapes 188
1 # draw_ellipse.py
2
3 from PIL import Image, ImageDraw
4
5
6 def ellipse(output_path):
7 image = Image.new("RGB", (400, 400), "white")
8 draw = ImageDraw.Draw(image)
9 draw.ellipse((25, 50, 175, 200), fill="red")
10
11 draw.ellipse((100, 150, 275, 300), outline="black", width=5,
12 fill="yellow")
13
14 image.save(output_path)
15
16 if __name__ == "__main__":
17 ellipse("ellipse.jpg")
In this code, you create a nice white image via the new() method. Then you draw a red ellipse on
top of it. Finally, you draw a second ellipse that is filled with yellow and outlined in black where
the outline width is set to 5.
When you run this code, the image it creates will look like this:
Chapter 8 - Drawing Shapes 189
You can create ovals and circles using ellipse(). Give it a try and see what you can do with it.
Now let’s find out how to create pie slices!
You have used all of these parameters in other drawings. To review, fill adds color to the inside of
the pieslice() while outline adds a colored border to the figure.
To start practicing this shape, create a new file named draw_pieslice.py and add this code to your
file:
Chapter 8 - Drawing Shapes 190
1 # draw_pieslice.py
2
3 from PIL import Image, ImageDraw
4
5
6 def pieslice(output_path):
7 image = Image.new("RGB", (400, 400), "grey")
8 draw = ImageDraw.Draw(image)
9 draw.pieslice((25, 50, 175, 200), start=30, end=250, fill="green")
10
11 draw.pieslice((100, 150, 275, 300), start=20, end=100, width=5,
12 outline="yellow")
13
14 image.save(output_path)
15
16 if __name__ == "__main__":
17 pieslice("pieslice.jpg")
In this code, you generate a grey image to draw on. Then you create two pie slices. The first
pieslice() is filled in with green. The second one is not filled in, but it does have a yellow outline.
Note that each pieslice() has a different starting and ending degree.
When you run this code, you will get the following image:
Chapter 8 - Drawing Shapes 191
With a little work, you could create a pie graph using Pillow! You should play around with your
code a bit and change some values. You will quickly learn how to make some nice pie slices of your
own.
Now let’s find out how to draw polygons with Pillow!
Drawing Polygons
A polygon is a geometric shape that has a number of points (vertices) and an equal number of
line segments or sides. A square, triangle, and hexagon are all types of polygons. Pillow lets you
create your own polygons. Pillow’s documentation defines a polygon like this: The polygon outline
consists of straight lines between the given coordinates, plus a straight line between the last and the
first coordinate.
Here is the code definition of the polygon() method:
All of these parameters should be familiar to you now. Go ahead and create a new Python file and
name it draw_polygon.py. Then add this code:
Chapter 8 - Drawing Shapes 192
1 # draw_polygon.py
2
3 from PIL import Image, ImageDraw
4
5
6 def polygon(output_path):
7 image = Image.new("RGB", (400, 400), "grey")
8 draw = ImageDraw.Draw(image)
9 draw.polygon(((100, 100), (200, 50), (125, 25)), fill="green")
10
11 draw.polygon(((175, 100), (225, 50), (200, 25)),
12 outline="yellow")
13
14 image.save(output_path)
15
16 if __name__ == "__main__":
17 polygon("polygons.jpg")
This code will create a grey image like the last example in the previous section. It will then create
a polygon that is filled with the color green. Then it will create a second polygon and outline it in
yellow without filling it.
In both of the drawings, you are supplying three points. That will create two triangles.
When you run this code, you will get this output:
Chapter 8 - Drawing Shapes 193
Try changing the code by adding additional points to one or more of the polygons in the code above.
With a little practice, you’ll be able to create complex polygons quickly with Pillow.
Drawing Rectangles
The rectangle() method allows you to draw a rectangle or square using Pillow. Here is how
rectangle() is defined:
You can pass in two tuples that define the beginning and ending coordinates to draw the rectangle.
Or you can supply the four coordinates as a box tuple (4-item tuple). Then you can add an outline,
fill it with a color, and change the outline’s width.
Create a new file and name it draw_rectangle.py. Then fill it in with this code so you can start
drawing rectangles:
Chapter 8 - Drawing Shapes 194
1 # draw_rectangle.py
2
3 from PIL import Image, ImageDraw
4
5
6 def rectangle(output_path):
7 image = Image.new("RGB", (400, 400), "blue")
8 draw = ImageDraw.Draw(image)
9 draw.rectangle((200, 100, 300, 200), fill="red")
10 draw.rectangle((50, 50, 150, 150), fill="green", outline="yellow",
11 width=3)
12 image.save(output_path)
13
14 if __name__ == "__main__":
15 rectangle("rectangle.jpg")
This code will create a blue image that is 400x400 pixels. Then it will draw two rectangles. The first
rectangle will be filled with red. The second will be filled with green and outlined with yellow.
When you run this code, you will get this image as output:
Chapter 8 - Drawing Shapes 195
Aren’t those lovely rectangles? You can modify the rectangle’s points to create thinner or wider
rectangles. You could also modify the outline width that you add to the rectangles.
Now let’s move on and create a user interface that applies some of the new skills you’ve learned!
• ellipse
• rectangle
The reason for this is that these two shapes take the same arguments. You can take on the challenge
yourself to add the other shapes to the GUI!
When your GUI is finished, it will look like this:
Chapter 8 - Drawing Shapes 196
Now open up your Python editor and create a new file named drawing_gui.py. Then add this code
to your new file:
Chapter 8 - Drawing Shapes 197
1 # drawing_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from PIL import Image, ImageColor, ImageDraw
10
11 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
12 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
13
14
15 def get_value(key, values):
16 value = values[key]
17 if value.isdigit():
18 return int(value)
19 return 0
20
21
22 def apply_drawing(values, window):
23 image_file = values["-FILENAME-"]
24 shape = values["-SHAPES-"]
25 begin_x = get_value("-BEGIN_X-", values)
26 begin_y = get_value("-BEGIN_Y-", values)
27 end_x = get_value("-END_X-", values)
28 end_y = get_value("-END_Y-", values)
29 width = get_value("-WIDTH-", values)
30 fill_color = values["-FILL_COLOR-"]
31 outline_color = values["-OUTLINE_COLOR-"]
32
33 if os.path.exists(image_file):
34 shutil.copy(image_file, tmp_file)
35 image = Image.open(tmp_file)
36 image.thumbnail((400, 400))
37 draw = ImageDraw.Draw(image)
38 if shape == "Ellipse":
39 draw.ellipse(
40 (begin_x, begin_y, end_x, end_y),
41 fill=fill_color,
42 width=width,
43 outline=outline_color,
Chapter 8 - Drawing Shapes 198
44 )
45 elif shape == "Rectangle":
46 draw.rectangle(
47 (begin_x, begin_y, end_x, end_y),
48 fill=fill_color,
49 width=width,
50 outline=outline_color,
51 )
52 image.save(tmp_file)
53
54 bio = io.BytesIO()
55 image.save(bio, format="PNG")
56 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
57
58
59 def create_coords_elements(label, begin_x, begin_y, key1, key2):
60 return [
61 sg.Text(label),
62 sg.Input(begin_x, size=(5, 1), key=key1, enable_events=True),
63 sg.Input(begin_y, size=(5, 1), key=key2, enable_events=True),
64 ]
65
66
67 def save_image(values):
68 save_filename = sg.popup_get_file(
69 "File", file_types=file_types, save_as=True, no_window=True,
70 )
71 if save_filename == values["-FILENAME-"]:
72 sg.popup_error(
73 "You are not allowed to overwrite the original image!",
74 )
75 else:
76 if save_filename:
77 shutil.copy(tmp_file, save_filename)
78 sg.popup(f"Saved: {save_filename}")
79
80
81 def main():
82 colors = list(ImageColor.colormap.keys())
83 layout = [
84 [sg.Image(key="-IMAGE-", size=(400,400))],
85 [
86 sg.Text("Image File"),
Chapter 8 - Drawing Shapes 199
87 sg.Input(
88 size=(25, 1), key="-FILENAME-",
89 ),
90 sg.FileBrowse(file_types=file_types),
91 sg.Button("Load Image"),
92 ],
93 [
94 sg.Text("Shapes"),
95 sg.Combo(
96 ["Ellipse", "Rectangle"],
97 default_value="Ellipse",
98 key="-SHAPES-",
99 enable_events=True,
100 readonly=True,
101 ),
102 ],
103 [
104 *create_coords_elements(
105 "Begin Coords", "10", "10", "-BEGIN_X-", "-BEGIN_Y-",
106 ),
107 *create_coords_elements(
108 "End Coords", "100", "100", "-END_X-", "-END_Y-",
109 ),
110 ],
111 [
112 sg.Text("Fill"),
113 sg.Combo(
114 colors,
115 default_value=colors[0],
116 key="-FILL_COLOR-",
117 enable_events=True,
118 readonly=True,
119 ),
120 sg.Text("Outline"),
121 sg.Combo(
122 colors,
123 default_value=colors[0],
124 key="-OUTLINE_COLOR-",
125 enable_events=True,
126 readonly=True,
127 ),
128 sg.Text("Width"),
129 sg.Input("3", size=(5, 1), key="-WIDTH-", enable_events=True),
Chapter 8 - Drawing Shapes 200
130 ],
131 [sg.Button("Save")],
132 ]
133
134 window = sg.Window("Drawing GUI", layout)
135
136 events = [
137 "Load Image",
138 "-BEGIN_X-",
139 "-BEGIN_Y-",
140 "-END_X-",
141 "-END_Y-",
142 "-FILL_COLOR-",
143 "-OUTLINE_COLOR-",
144 "-WIDTH-",
145 "-SHAPES-",
146 ]
147 while True:
148 event, values = window.read()
149 if event == "Exit" or event == sg.WIN_CLOSED:
150 break
151 if event in events:
152 apply_drawing(values, window)
153 if event == "Save" and values["-FILENAME-"]:
154 save_image(values)
155 window.close()
156
157
158 if __name__ == "__main__":
159 main()
That’s a bunch of code! To make things easier, you will go over this code in smaller chunks.
The first chunk is the code at the top of the file:
Chapter 8 - Drawing Shapes 201
1 # drawing_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from PIL import Image, ImageColor, ImageDraw
10
11 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
12 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
These lines of code define the imports of the packages and modules that you need. It also sets up
two variables:
• file_types - which you will use for browsing and saving your images
• tmp_file - a temporary file that is created to save your intermediate image file
This function is used to convert strings to integers. If you enter an alphabetical character or a special
character, it will cause this code to throw an error. Feel free to catch those kinds of things here or
implement a filter to prevent users from entering anything other than integers.
If the user empties the Element of its contents, you force it to return a zero. This allows the user
interface to continue to function. Another improvement that you could add here is to take the image
bounds into account. You could make it so that the user cannot enter negative numbers or numbers
that are larger than the image.
The next function is a meaty one:
Chapter 8 - Drawing Shapes 202
This code is used for creating the two shapes you want to draw on your image. It gets all the various
settings that you need to create an ellipse or a rectangle. The settings that you can change are:
If the user has opened up an image, then your code will automatically draw a shape using the default
settings. When you edit any of those settings, this function will get called and the image will update.
Note: The drawing is applied to a thumbnail version of the image rather than a copy of
the image. This is done to keep the image visible in your GUI. Many photos have a higher
resolution than can be shown on a typical monitor.
The next function to look at is called create_coords_elements():
This function returns a Python list that contains three Elements in it. One label (sg.Text) and two
text boxes (sg.Input). Because these elements are in a single list, they will be added as a horizontal
row to your user interface.
Now you’re ready to go on to the save_image() function:
1 def save_image(values):
2 save_filename = sg.popup_get_file(
3 "File", file_types=file_types, save_as=True, no_window=True,
4 )
5 if save_filename == values["-FILENAME-"]:
6 sg.popup_error(
7 "You are not allowed to overwrite the original image!",
8 )
9 else:
10 if save_filename:
11 shutil.copy(tmp_file, save_filename)
12 sg.popup(f"Saved: {save_filename}")
This function is taken from a previous user interface that you wrote. It asks the user where to save
your file. This function also prevents the user from trying to overwrite the original image.
The last function is your main() one:
Chapter 8 - Drawing Shapes 204
1 def main():
2 colors = list(ImageColor.colormap.keys())
3 layout = [
4 [sg.Image(key="-IMAGE-", size=(400,400))],
5 [
6 sg.Text("Image File"),
7 sg.Input(
8 size=(25, 1), key="-FILENAME-",
9 ),
10 sg.FileBrowse(file_types=file_types),
11 sg.Button("Load Image"),
12 ],
13 [
14 sg.Text("Shapes"),
15 sg.Combo(
16 ["Ellipse", "Rectangle"],
17 default_value="Ellipse",
18 key="-SHAPES-",
19 enable_events=True,
20 readonly=True,
21 ),
22 ],
23 [
24 *create_coords_elements(
25 "Begin Coords", "10", "10", "-BEGIN_X-", "-BEGIN_Y-",
26 ),
27 *create_coords_elements(
28 "End Coords", "100", "100", "-END_X-", "-END_Y-",
29 ),
30 ],
31 [
32 sg.Text("Fill"),
33 sg.Combo(
34 colors,
35 default_value=colors[0],
36 key="-FILL_COLOR-",
37 enable_events=True,
38 readonly=True,
39 ),
40 sg.Text("Outline"),
41 sg.Combo(
42 colors,
43 default_value=colors[0],
Chapter 8 - Drawing Shapes 205
44 key="-OUTLINE_COLOR-",
45 enable_events=True,
46 readonly=True,
47 ),
48 sg.Text("Width"),
49 sg.Input("3", size=(5, 1), key="-WIDTH-", enable_events=True),
50 ],
51 [sg.Button("Save")],
52 ]
53
54 window = sg.Window("Drawing GUI", layout)
This is one of the most complex user interfaces you have written. It adds lots of Elements in rows.
You can also see that you are using a special syntax to extract items from a list:
• *create_coords_elements()
When you call create_coords_elements(), it returns a list. But you want both the beginning and
ending coordinates on the same line. So you extract the elements from the list.
This little code example illustrates what is happening:
If you don’t use the asterisk, you will end up with a nested list instead of a list that has three elements
in it.
Here are the last few lines of code from the main() function:
1 events = [
2 "Load Image",
3 "-BEGIN_X-",
4 "-BEGIN_Y-",
5 "-END_X-",
6 "-END_Y-",
7 "-FILL_COLOR-",
8 "-OUTLINE_COLOR-",
9 "-WIDTH-",
10 "-SHAPES-",
Chapter 8 - Drawing Shapes 206
11 ]
12 while True:
13 event, values = window.read()
14 if event == "Exit" or event == sg.WIN_CLOSED:
15 break
16 if event in events:
17 apply_drawing(values, window)
18 if event == "Save" and values["-FILENAME-"]:
19 save_image(values)
20 window.close()
21
22 if __name__ == "__main__":
23 main()
The first line defines the events that you use to know when to update the drawing. The last
conditional will save the image where the user chooses. Give your new GUI a try. Then you can
start planning how you will improve it!
Wrapping Up
You can use Pillow to add shapes to your images. This can be helpful for adding outlines to your
images, highlighting one or more portions of your image, and more.
In this chapter, you learned about the following topics:
• Common Parameters
• Drawing Lines
• Drawing Arcs
• Drawing Chords
• Drawing Ellipses
• Drawing Pie Slices
• Drawing Polygons
• Drawing Rectangles
• Creating a Drawing GUI
You can do a lot with the shapes that are provided by Pillow. You should take these examples and
modify them to test them out with your own photos. Give it a try and see what you can come up
with!
Chapter 9 - Drawing Text
Pillow supports drawing text on your images in addition to shapes. Pillow uses its font file format
to store bitmap fonts, limited to 256 characters. Pillow also supports TrueType and OpenType fonts
as well as other font formats supported by the FreeType library.
In this chapter, you will learn about the following:
• Drawing Text
• Loading TrueType Fonts
• Changing Text Color
• Drawing Multiple Lines of Text
• Aligning Text
• Changing Text Opacity
• Learning About Text Anchors
• Creating a Text Drawing GUI
While this chapter is not completely exhaustive in its coverage of drawing text with Pillow, when
you have finished reading it, you will have a good understanding of how text drawing works and
be able to draw text on your own.
Let’s get started by learning how to draw text.
Drawing Text
Drawing text with Pillow is similar to drawing shapes. However, drawing text has the added
complexity of needing to be able to handle fonts, spacing, alignment, and more. You can get an
idea of the complexity of drawing text by taking a look at the text() function’s signature:
This function takes in a lot more parameters than any of the shapes you can draw with Pillow! Let’s
go over each of these parameters in turn:
• xy - The anchor coordinates for the text (i.e. where to start drawing the text).
Chapter 9 - Drawing Text 208
You probably won’t use most of these parameters regularly unless your job requires you to work
with foreign languages or arcane font features.
When it comes to learning something new, it’s always good to start with a nice example. Open up
your Python editor and create a new file named draw_text.py. Then add this code to it:
1 # draw_text.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def text(output_path):
7 image = Image.new("RGB", (200, 200), "green")
8 draw = ImageDraw.Draw(image)
9 draw.text((10, 10), "Hello from")
10 draw.text((10, 25), "Pillow",)
11 image.save(output_path)
12
13 if __name__ == "__main__":
14 text("text.jpg")
Chapter 9 - Drawing Text 209
Here you create a small image using Pillow’s Image.new() method. It has a nice green background.
Then you create a drawing object. Next, you tell Pillow where to draw the text. In this case, you
draw two lines of text.
When you run this code, you will get the following image:
That looks pretty good. Normally, when you are drawing text on an image, you would specify a
font. If you don’t have a font handy, you can leave the font parameter unspecified (as above) to get
Pillow’s default font, or you can explicitly use Pillow’s default font.
Here is an example that updates the previous example to explicitly use Pillow’s default font:
1 # draw_text_default_font.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def text(output_path):
7 image = Image.new("RGB", (200, 200), "green")
8 draw = ImageDraw.Draw(image)
9 font = ImageFont.load_default()
10 draw.text((10, 10), "Hello from", font=font)
11 draw.text((10, 25), "Pillow", font=font)
12 image.save(output_path)
13
14 if __name__ == "__main__":
15 text("text.jpg")
In this version of the code, you use ImageFont.load_default() to load up Pillow’s default font. Then
Chapter 9 - Drawing Text 210
you apply the font to the text, you pass it in with the font parameter.
The output of this code will be the same as the first example.
Now let’s discover how to use a TrueType font with Pillow!
1 # draw_truetype.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def text(input_image_path, output_path):
7 image = Image.open(input_image_path)
8 draw = ImageDraw.Draw(image)
9 y = 10
10 for font_size in range(12, 75, 10):
11 font = ImageFont.truetype("Gidole-Regular.ttf", size=font_size)
12 draw.text((10, y), f"Chihuly Exhibit ({font_size=}", font=font)
13 y += 35
14 image.save(output_path)
15
16 if __name__ == "__main__":
17 text("chihuly_exhibit.jpg", "truetype.jpg")
For this example, you use the Gidole font and load an image taken at the Dallas Arboretum in Texas:
²⁰https://github.com/larsenwork/Gidole
²¹https://github.com/python-pillow/Pillow
²²https://github.com/driscollis/image_processing_with_python
Chapter 9 - Drawing Text 211
Then you loop over several different font sizes and write out a string at different positions on the
image. When you run this code, you will create an image that looks like this:
Chapter 9 - Drawing Text 212
That code demonstrated how to change font sizes using a TrueType font. Now you’re ready to learn
how to switch between different TrueType fonts.
Note: You may notice some artifacts around your characters when drawing text. This can be
reduced by saving the image as a PNG instead of a JPG. You may be able to mitigate it by
changing fonts as well.
Create another new file and name this one draw_multiple_truetype.py. Then put this code into it:
1 # draw_multiple_truetype.py
2
3 import glob
4 from PIL import Image, ImageDraw, ImageFont
5
6
7 def truetype(input_image_path, output_path):
8 image = Image.open(input_image_path)
9 draw = ImageDraw.Draw(image)
10 y = 10
11 ttf_files = glob.glob("*.ttf")
12 for ttf_file in ttf_files:
Chapter 9 - Drawing Text 213
Here you use Python’s glob module to search for files with the extension .ttf. Then you loop over
those files and write out the font name on the image using each of the fonts that glob found.
When you run this code, your new image will look like this:
This demonstrates writing text with multiple formats in a single code example. You always need to
provide a relative or absolute path to the TrueType or OpenType font file that you want to load. If
you don’t provide a valid path, a FileNotFoundError exception will be raised.
Now let’s move on and learn how to change the color of your text!
Chapter 9 - Drawing Text 214
1 # text_colors.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def text_color(output_path):
7 image = Image.new("RGB", (200, 200), "white")
8 draw = ImageDraw.Draw(image)
9 colors = ["green", "blue", "red", "yellow", "purple"]
10 font = ImageFont.truetype("Gidole-Regular.ttf", size=12)
11 y = 10
12 for color in colors:
13 draw.text((10, y), f"Hello from Pillow", font=font, fill=color)
14 y += 35
15 image.save(output_path)
16
17 if __name__ == "__main__":
18 text_color("colored_text.jpg")
In this example, you create a new, white image. Then you create a list of colors. Next, you loop over
each color in the list and apply the color to your string using the fill parameter.
When you run this code, you will end up with this nice output:
Chapter 9 - Drawing Text 215
This output demonstrates how you can change the color of your text.
Now let’s learn how to draw multiple lines of text at once!
1 # draw_multiline_text.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def text(input_image_path, output_path):
7 image = Image.open(input_image_path)
8 draw = ImageDraw.Draw(image)
9 font = ImageFont.truetype("Gidole-Regular.ttf", size=42)
10 text = "Chihuly Exhibit\nDallas, Texas"
11 draw.text((10, 25), text, font=font)
12 image.save(output_path)
13
14 if __name__ == "__main__":
15 text("chihuly_exhibit.jpg", "multiline_text.jpg")
Chapter 9 - Drawing Text 216
For this example, you create a string with the newline character inserted in the middle. When you
run this example, your result should look like this:
Pillow has a built-in method for drawing multiple lines of text too. Take the code you wrote in
the example above and copy and paste it into a new file. Save your new file and name it draw_-
multiline_text_2.py.
Now modify the code so that it uses the multiline_text() function:
1 # draw_multiline_text_2.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def text(input_image_path, output_path):
7 image = Image.open(input_image_path)
8 draw = ImageDraw.Draw(image)
9 font = ImageFont.truetype("Gidole-Regular.ttf", size=42)
10 text = """
11 Chihuly Exhibit
12 Dallas, Texas"""
Chapter 9 - Drawing Text 217
In this example, you create a multiline string using Python’s triple quotes. Then you draw that string
onto your image by calling multiline_text().
When you run this code, your image will be slightly different:
The text is positioned down and to the right of the previous example. The reason is that you used
Python’s triple quotes to create the string. It retains the newline and indentation that you gave it.
If you put this string into the previous example, it should look the same – the multiline_text()
doesn’t affect the result unless you use the multiline_text()-specific parameters such as spacing
and align.
Now let’s learn how you can align text when you draw it.
Chapter 9 - Drawing Text 218
Aligning Text
Pillow lets you align text. However, the alignment is relative to the anchor and only applies to text
with multiple lines. You will look at an alternative method for aligning text without using the align
parameter in this section as well.
First, let’s get started by using align. Create a new file and name it text_alignment.py. Then add
the following code:
1 # text_alignment.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def alignment(output_path):
7 image = Image.new("RGB", (200, 200), "white")
8 draw = ImageDraw.Draw(image)
9 alignments = ["left", "center", "right"]
10 y = 10
11 font = ImageFont.truetype("Gidole-Regular.ttf", size=12)
12 for alignment in alignments:
13 draw.text((10, y), f"Hello from\nPillow", font=font,
14 align=alignment, fill="black")
15 y += 35
16 image.save(output_path)
17
18 if __name__ == "__main__":
19 alignment("aligned_text.jpg")
Here you create a small, white image. Then you create a list of all of the valid alignment options:
“left”, “center”, and “right”. Next, you loop over these alignment values and apply them to the same
multiline string.
After running this code, you will have the following result:
Chapter 9 - Drawing Text 219
Looking at the output, you can kind of get a feel for how alignment works in Pillow. Whether or not
that works for your use-case is up for you to decide. You will probably need to adjust the location
of where you start drawing in addition to setting the align parameter to get what you want.
To skip using the align parameter you can use Pillow to get the size of your string – using the
Drawing object’s textsize() method or the font object’s getsize() method – and then do some
simple math to find the appropriate starting coordinates.
To see how that works, you can create a new file named center_text.py and put this code into it:
1 # center_text.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def center(output_path):
7 width, height = (400, 400)
8 image = Image.new("RGB", (width, height), "grey")
9 draw = ImageDraw.Draw(image)
10 font = ImageFont.truetype("Gidole-Regular.ttf", size=12)
11 text = "Pillow Rocks!"
12 font_width, font_height = font.getsize(text)
13
14 new_width = (width - font_width) / 2
15 new_height = (height - font_height) / 2
16 draw.text((new_width, new_height), text, font=font, fill="black")
17 image.save(output_path)
18
Chapter 9 - Drawing Text 220
19 if __name__ == "__main__":
20 center("centered_text.jpg")
When manually aligning you must keep track of the image’s size as well as the string’s size. For
this example, you used the font’s getsize() method which automatically takes the font’s width and
height into account.
Then you took the image width and height and subtracted the width and height of the string and
divided them by two. This should get you the coordinates you need to write the text in the center
of the image.
When you run this code, you can see that the text is centered!
Now create a new file and name it text_opacity.py. Then add the following code to your new file:
1 # text_opacity.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def change_opacity(input_path, output_path):
7 base_image = Image.open(input_path).convert("RGBA")
8
9 txt_img = Image.new("RGBA", base_image.size, (255,255,255,0))
10 font = ImageFont.truetype("Gidole-Regular.ttf", 40)
11 draw = ImageDraw.Draw(txt_img)
12
13 # draw text at half opacity
14 draw.text((10,10), "Pillow", font=font, fill=(255,255,255,128))
15
16 # draw text at full opacity
Chapter 9 - Drawing Text 222
In this example, you open the flower image and convert it to RGBA. Then you create a new image
that is the same size as the flower image. Next, you load the Gidole font and create a drawing context
object using the custom image you just created.
Now comes the fun part! You draw one string and set the alpha value to 128, which equates to about
half opacity. Then you draw a second string on the following line and tell Pillow to use full opacity.
Note that in both of these instances, you are using RGBA values rather than color names, as you did
in your previous code examples. This gives you more versatility in setting the alpha amount.
The last step is to call alpha_composite() and composite the txt_img onto the base_image.
When you run this code, your output will look like this:
This demonstrates how you can change the opacity of your text with Pillow. You should try a few
different values for your txt_img to see how it changes the text’s opacity.
Now let’s learn what text anchors are and how they affect text placement.
• l (left) - Anchor is to the left of the text. This is the origin of the first glyph.
• m (middle) - Anchor is horizontally centered with the text.
• r (right) - Anchor is to the right of the text. This is the advanced origin of the last glyph.
• s (baseline) - Anchor is at the baseline of the text. This is the recommended alignment because
it doesn’t change regardless of the specific glyphs of the given text.
• a (ascender / top) - Anchor is at the ascender line of the first line of text.
• m (middle) - Anchor is vertically centered with the text. This is the midpoint of the first ascender
line and the last descender line.
• s (baseline) - Anchor is at the baseline of the first line of text, only descenders extend below
the anchor.
• d (descender / bottom) - Anchor is at the descender line of the last line of text.
Chapter 9 - Drawing Text 224
• t (top) — Anchor is at the top of the text. This is the origin of the first glyph.
• m (middle) - Anchor is vertically centered with the text.
• b (bottom) - Anchor is at the bottom of the text. This is the advanced origin of the last glyph.
Anchor Examples
Anchors are hard to visualize if all you do is talk about them. It helps a lot if you create some
examples to see what happens. Pillow provides an example in their documentation²³ on anchors
along with some very helpful images.
You can take their example and adapt it a bit to make it more useful. To see how, you need to create
a new file and name it create_anchor.py. Then add this code to it:
1 # create_anchor.py
2
3 from PIL import Image, ImageDraw, ImageFont
4
5
6 def anchor(xy=(100, 100), anchor="la"):
7 font = ImageFont.truetype("Gidole-Regular.ttf", 32)
8 image = Image.new("RGB", (200, 200), "white")
9 draw = ImageDraw.Draw(image)
10 draw.line(((0, 100), (200, 100)), "gray")
11 draw.line(((100, 0), (100, 200)), "gray")
12 draw.text((100, 100), "Python", fill="black", anchor=anchor, font=font)
13 image.save(f"anchor_{anchor}.jpg")
14
15 if __name__ == "__main__":
16 anchor(anchor)
You can run this code as-is. The default anchor is “la”, but you explicitly call that out here. You also
draw a cross-hair to mark where the xy position is. If you run this with other settings, you can see
how the anchor affects it.
Here is a screenshot from six different runs using six different anchors:
²³https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html
Chapter 9 - Drawing Text 225
You can try running this code with some of the other anchors that aren’t shown here. You can also
adjust the position tuple and re-run it again with different anchors. You could even create a loop to
loop over the anchors and create a set of examples if you wanted to.
Now you are ready to create a GUI that allows you to apply some of the things you have learned in
this chapter!
• Font type
• Font color
• Font-size
• Text position
Now it’s time for you to code up the GUI. Open up your Python editor and create a new file. Then
name it text_gui.py and enter the following code:
Chapter 9 - Drawing Text 227
1 # text_gui.py
2
3 import glob
4 import io
5 import os
6 import PySimpleGUI as sg
7 import shutil
8 import tempfile
9
10 from PIL import Image
11 from PIL import ImageColor
12 from PIL import ImageDraw
13 from PIL import ImageFont
14
15 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
16 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
17
18
19 def get_value(key, values):
20 value = values[key]
21 if value.isdigit():
22 return int(value)
23 return 0
24
25
26 def apply_text(values, window):
27 global ttf_files
28 image_file = values["-FILENAME-"]
29 font_name = values["-TTF-"]
30 font_size = get_value("-FONT_SIZE-", values)
31 color = values["-COLORS-"]
32 x, y = get_value("-TEXT-X-", values), get_value("-TEXT-Y-", values)
33 text = values["-TEXT-"]
34
35 if image_file and os.path.exists(image_file):
36 shutil.copy(image_file, tmp_file)
37 image = Image.open(tmp_file)
38 image.thumbnail((400, 400))
39
40 if text:
41 draw = ImageDraw.Draw(image)
42 if font_name == "Default Font":
43 font = None
Chapter 9 - Drawing Text 228
44 else:
45 font = ImageFont.truetype(
46 ttf_files[font_name], size=font_size)
47 draw.text((x, y), text=text, font=font, fill=color)
48 image.save(tmp_file)
49
50 bio = io.BytesIO()
51 image.save(bio, format="PNG")
52 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
53
54
55 def create_row(label, key, file_types):
56 return [
57 sg.Text(label),
58 sg.Input(size=(25, 1), key=key),
59 sg.FileBrowse(file_types=file_types),
60 ]
61
62
63 def get_ttf_files(directory=None):
64 if directory is not None:
65 ttf_files = glob.glob(directory + "/*.ttf")
66 else:
67 ttf_files = glob.glob("*.ttf")
68 if not ttf_files:
69 return {"Default Font": None}
70 ttf_dict = {}
71 for ttf in ttf_files:
72 ttf_dict[os.path.basename(ttf)] = ttf
73 return ttf_dict
74
75
76 def save_image(values):
77 save_filename = sg.popup_get_file(
78 "File", file_types=file_types, save_as=True, no_window=True,
79 )
80 if save_filename == values["-FILENAME-"]:
81 sg.popup_error(
82 "You are not allowed to overwrite the original image!",
83 )
84 else:
85 if save_filename:
86 shutil.copy(tmp_file, save_filename)
Chapter 9 - Drawing Text 229
87 sg.popup(f"Saved: {save_filename}")
88
89
90 def update_ttf_values(window):
91 global ttf_files
92 directory = sg.popup_get_folder("Get TTF Directory")
93 if directory is not None:
94 ttf_files = get_ttf_files(directory)
95 new_values = list(ttf_files.keys())
96 window["-TTF-"].update(values=new_values, value=new_values[0])
97
98
99 def main():
100 colors = list(ImageColor.colormap.keys())
101 ttf_files = get_ttf_files()
102 ttf_filenames = list(ttf_files.keys())
103
104 menu_items = [["File", ["Open Font Directory"]]]
105
106 layout = [
107 [sg.Menu(menu_items)],
108 [sg.Image(key="-IMAGE-", size=(400,400))],
109 create_row("Image File:", "-FILENAME-", file_types),
110 [sg.Button("Load Image")],
111 [sg.Text("Text:"), sg.Input(key="-TEXT-", enable_events=True)],
112 [
113 sg.Text("Text Position"),
114 sg.Text("X:"),
115 sg.Input("10", size=(5, 1), enable_events=True,
116 key="-TEXT-X-"),
117 sg.Text("Y:"),
118 sg.Input("10", size=(5, 1), enable_events=True,
119 key="-TEXT-Y-"),
120 ],
121 [
122 sg.Combo(colors, default_value=colors[0], key='-COLORS-',
123 enable_events=True, readonly=True),
124 sg.Combo(ttf_filenames, default_value=ttf_filenames[0],
125 key='-TTF-', enable_events=True, readonly=True),
126 sg.Text("Font Size:"),
127 sg.Input("12", size=(5, 1), key="-FONT_SIZE-", enable_events=True),
128
129 ],
Chapter 9 - Drawing Text 230
There are over 100 lines of code in this GUI! To make it easier to understand what’s going on, you
will go over the code in smaller chunks.
Here are the first few lines of code:
1 # text_gui.py
2
3 import glob
4 import io
5 import os
6 import PySimpleGUI as sg
7 import shutil
8 import tempfile
9
10 from PIL import Image
11 from PIL import ImageColor
12 from PIL import ImageDraw
13 from PIL import ImageFont
14 from PIL import ImageTk
15
16 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
17 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
Chapter 9 - Drawing Text 231
This is the import section of your code. You import all the necessary modules and packages that you
need to make your code work. You also set up a couple of variables there at the end. The file_types
defines what file types your user interface can load. The tmp_file is a temporary file that you create
to save your image changes to until the user is ready to save the file where they want it.
The first function in your code is called get_value(). Here is its code:
This function comes from the drawing shapes GUI that you created in the previous chapter. Its job
is to convert strings to integers and to return zero if the user empties a control that requires a value.
Now you can move on to learn about apply_text():
Here is where the magic happens. This code runs whenever the user opens a new image, changes the
font, changes the font size, changes the font color, changes the text itself, or modifies the position
of the text. If the user hasn’t entered any text, but they have chosen an image, only the image will
change. If the user has entered text, then the other changes will be updated too.
Note: When the image is saved, it saves as the thumbnail version. So you will be saving a
smaller version of the image, but with text on it.
The next function you will look at is called create_row():
You have seen this function before. It is used to create three Elements:
• A label (sg.Text)
• A text box (sg.Input)
• A file browse button (sg.FileBrowse)
These Elements are returned in a Python list. This will create a horizontal row of Elements in your
user interface.
The next function to learn about is get_ttf_files():
1 def get_ttf_files(directory=None):
2 if directory is not None:
3 ttf_files = glob.glob(directory + "/*.ttf")
4 else:
5 ttf_files = glob.glob("*.ttf")
6 if not ttf_files:
7 return {"Default Font": None}
8 ttf_dict = {}
9 for ttf in ttf_files:
10 ttf_dict[os.path.basename(ttf)] = ttf
11 return ttf_dict
This code uses Python’s glob module to find all the TrueType font files in the directory that your
GUI file is in or by searching the directory that you have passed in. If the GUI doesn’t find any
TTF files, it will load Pillow’s default font.
Chapter 9 - Drawing Text 233
No matter what happens, your code will return a Python dictionary that maps the name of the font
to the absolute path to the font file. If your code does not find any TTF files, it will map “Default
Font” to None to indicate to your code that no fonts were found, so you will use Pillow’s default font
instead.
The next function defines how to save your edited image:
1 def save_image(values):
2 save_filename = sg.popup_get_file(
3 "File", file_types=file_types, save_as=True, no_window=True,
4 )
5 if save_filename == values["-FILENAME-"]:
6 sg.popup_error(
7 "You are not allowed to overwrite the original image!",
8 )
9 else:
10 if save_filename:
11 shutil.copy(tmp_file, save_filename)
12 sg.popup(f"Saved: {save_filename}")
You created this code in a GUI from a previous chapter. It can be reused here without any changes. It
asks the user what to name their new image using a popup dialog. Then it checks to make sure that
the user doesn’t overwrite their original image. You prevent that from happening here by showing
them an error message if they try to do that.
Otherwise, you save the file by copying the temporary file over to the new location that the user
chose.
Now you are ready to learn about the update_ttf_values() function:
1 def update_ttf_values(window):
2 directory = sg.popup_get_folder("Get TTF Directory")
3 if directory is not None:
4 ttf_files = get_ttf_files(directory)
5 new_values = list(ttf_files.keys())
6 window["-TTF-"].update(values=new_values, value=new_values[0])
This function is called from the File menu in your user interface. When it gets called, it will show
a dialog asking the user where their TTF files are located. If the user presses Cancel, no directory
is returned. But if the user does choose a directory and accepts the dialog, then you call get_ttf_-
files() and it will search for any TTF files that are in the folder. Then it will update your TTF
combobox’s contents so that you can then choose a TTF font to apply to your text.
The last function you’ll need to review is the main() one:
Chapter 9 - Drawing Text 234
1 def main():
2 colors = list(ImageColor.colormap.keys())
3 ttf_files = get_ttf_files()
4 ttf_filenames = list(ttf_files.keys())
5
6 menu_items = [["File", ["Open Font Directory"]]]
7
8 layout = [
9 [sg.Menu(menu_items)],
10 [sg.Image(key="-IMAGE-", size=(400,400))],
11 create_row("Image File:", "-FILENAME-", file_types),
12 [sg.Button("Load Image")],
13 [sg.Text("Text:"), sg.Input(key="-TEXT-", enable_events=True)],
14 [
15 sg.Text("Text Position"),
16 sg.Text("X:"),
17 sg.Input("10", size=(5, 1), enable_events=True,
18 key="-TEXT-X-"),
19 sg.Text("Y:"),
20 sg.Input("10", size=(5, 1), enable_events=True,
21 key="-TEXT-Y-"),
22 ],
23 [
24 sg.Combo(colors, default_value=colors[0], key='-COLORS-',
25 enable_events=True, readonly=True),
26 sg.Combo(ttf_filenames, default_value=ttf_filenames[0],
27 key='-TTF-', enable_events=True, readonly=True),
28 sg.Text("Font Size:"),
29 sg.Input("12", size=(5, 1), key="-FONT_SIZE-", enable_events=True),
30
31 ],
32 [sg.Button("Save Image")],
33 ]
34
35 window = sg.Window("Draw Text GUI", layout)
The first three lines in this function create a Python list of colors and a list of ttf_filenames. Next,
you create a nested list that represents your menu for your user interface. Then you create an layout
list, which you use to layout all the Elements that are used to create your user interface. Once you
have that, you can pass it along to your sg.Window, which will layout your Elements and show them
to the user.
The last chunk of code is your user interface’s event handler:
Chapter 9 - Drawing Text 235
1 while True:
2 event, values = window.read()
3 if event == "Exit" or event == sg.WIN_CLOSED:
4 break
5 if event in ["Load Image", "-COLORS-", "-TTF-", "-FONT_SIZE-",
6 "-TEXT-X-", "-TEXT-Y-", "-TEXT-"]:
7 apply_text(values, window)
8 if event == "Save Image" and values["-FILENAME-"]:
9 save_image(values)
10 if event == "Open Font Directory":
11 update_ttf_values(window)
12
13 window.close()
14
15 if __name__ == "__main__":
16 main()
Here you check if the user has closed the window. If they did, then you exit the loop and end the
program.
The next conditional statement checks for all the other events except “save” and the menu events. If
any of the other Elements that have events enabled are triggered, then you will run apply_text().
This updates the image according to the setting(s) that you change.
The next conditional saves the image when you are finished editing it. It will call save_image()
which will ask the user where they want to save the image.
The last conditional is for your File menu event. If the user chooses the “Open Font Directory” option,
it will end up calling update_ttf_values(), which lets you choose a new TTF folder to search in.
Wrapping Up
At this point, you have a good understanding of how to draw text using Pillow. You learned how to
do all of the following:
• Drawing Text
• Loading TrueType Fonts
• Changing Text Color
• Drawing Multiple Lines of Text
• Aligning Text
• Changing Text Opacity
• Learning About Text Anchors
• Creating a Text Drawing GUI
Chapter 9 - Drawing Text 236
You can now take what you have learned and practice it. There are many things you can improve in
the user interface you created, such as handling invalid positions or other error conditions. You can
also explore loading OpenType fonts instead of TrueType. There are lots of examples in this chapter
that you can use as jumping-off points to create new applications!
Chapter 10 - Channel Operations
Pillow provides a special module called ImageChops. This module has several arithmetical image
operations that are known as Channel Operations or “chops”. According to Pillow’s documentation,
this module can be used for various purposes, including special effects, image compositions,
algorithmic painting, and more.
All of the functions within the ImageChops module return an Image object. At the time of writing,
these methods are only implemented for 8-bit images (i.e., “L” or “RGB”). These functions apply
different blend modes to one or two images. You can read more about blend modes on Wikipedia²⁴.
For additional details on ImageChops, see the documentation²⁵.
If you are interested in additional pre-made operations, you can check out the ImageOps module,
which will be covered in the next chapter. Note that the operations in ImageOps may be other types
of operations rather than additional channel operations.
In this chapter, you will learn about the following:
• ImageChop Aliases
• Adding Images
• Using ImageChops.darker()
• Using ImageChops.lighter()
• Finding Differences in Images
• Inverting Images
• Using Soft Light on Images
• Using Hard Light on Images
• Overlay Images
• Creating a Blending GUI
Some of the functions in ImageChops will be relatively slow to run because they do pixel-by-pixel
comparisons. So keep that in mind, especially if you are working with high-resolution images and
happen to be using one of those functions. You will not be covering all of the blend modes in
ImageChops here.
The first topic you will cover is the ImageChop aliases. Let’s get started!
²⁴https://en.wikipedia.org/wiki/Blend_modes
²⁵https://pillow.readthedocs.io/en/stable/reference/ImageChops.html
Chapter 10 - Channel Operations 238
ImageChop Aliases
The ImageChop module contains several aliases for other functions in Pillow’s Image module. Here
is the list:
These functions work in the same way as their Image counterparts because they are the same code
but with a different name. You have already learned about these functions in previous chapters, so
they will not be covered here.
The first function you will learn about from ImageChops is its add() function!
Adding Images
The documentation for the add() function is pretty sparse. You only have the docstring of the
function to go on. It is reproduced here:
So this seems to indicate that it opens up two images and adds the two image objects together. Then
it divides those images by the scale amount and then adds an offset. But what does that mean?
Wikipedia has one answer²⁶.
It says that you are adding the pixel values of one layer onto another. It doesn’t make mention of
scale and offset though.
That means that the best way to find the answer to that question is to try out some code! But first,
you will need some images. You will be reusing the Murex shell photo:
²⁶https://en.wikipedia.org/wiki/Blend_modes#Addition
Chapter 10 - Channel Operations 239
You aren’t required to use images that are the same size here. But for demonstration purposes, it is
nice to have them be the same.
Now you are ready to code. Open up your Python editor and create a new file named add_images.py.
Then add the following code to it:
1 # add_images.py
2
3 from PIL import Image, ImageChops
4
5 def add_images(image_path_one, image_path_two, output_path,
6 scale=1.0, offset=0):
7 image_one = Image.open(image_path_one)
8 image_two = Image.open(image_path_two)
9 image_three = ImageChops.add(
10 image_one, image_two, scale=scale, offset=offset,
11 )
12 image_three.save(output_path)
13
14
15 if __name__ == "__main__":
Chapter 10 - Channel Operations 241
This code takes in the paths of the two images that you want to add together as well as a path for
where to save the result. You can also modify the scale and offset. For now, just use the defaults.
When you run this code, you will end up with the following image:
The result of using ImageChops.add() appears to be a type of composite. If you increase the scale
amount, the image will become darker in general with the first image being slightly more transparent.
If you increase the offset, the entire image becomes lighter.
The next function to discover is ImageChops.darker()!
Using ImageChops.darker()
The ImageChops.darker() function takes in only two arguments. These are two Image objects. It
will then compare the two images, pixel-by-pixel, and return a new image that contains the darker
values.
Here is the function definition:
Chapter 10 - Channel Operations 242
It’s kind of hard to visualize what this function does in your head unless you have tried it on some
images. You will use the two images you used in the previous section for this example, too.
Create a new file and name it darken_image.py. Then enter this code:
1 # darken_image.py
2
3 from PIL import Image, ImageChops
4
5 def darken(image_path_one, image_path_two, output_path):
6 image_one = Image.open(image_path_one)
7 image_two = Image.open(image_path_two)
8 image_three = ImageChops.darker(image_one, image_two)
9 image_three.save(output_path)
10
11
12 if __name__ == "__main__":
13 darken("shell.png", "skyline.png", "darker_image.jpg")
This function is pretty straight-forward. You open up two images using Image.open(). Then you
pass those two Image objects to ImageChops.darker(). It then returns the darker values as a new
image object, which you then save.
When you run this code, you will end up with the following output:
Chapter 10 - Channel Operations 243
For the images that you used here, the darken() function blacked out the background and made the
shell the centerpiece of the image.
Now you can find out how to use a function that does the opposite of darker()!
Using ImageChops.lighter()
The lighter() function will compare two image objects, pixel-by-pixel, and return a new image
object that contains the lighter values of the two images, doing the opposite of the darker() function.
Here is the function’s definition:
Chapter 10 - Channel Operations 244
This function can be used in the same way as the darken() function. You can take the code from the
previous section and copy and paste it into a new file. Then name it lighten_image.py.
Now you need to make a few small edits to try out the lighten() function:
1 # lighten_image.py
2
3 from PIL import Image, ImageChops
4
5 def lighten(image_path_one, image_path_two, output_path):
6 image_one = Image.open(image_path_one)
7 image_two = Image.open(image_path_two)
8 image_three = ImageChops.lighter(image_one, image_two)
9 image_three.save(output_path)
10
11
12 if __name__ == "__main__":
13 lighten("shell.png", "skyline.png", "lighter_image.jpg")
Here you rename your custom function to lighten(). Then you change ImageChops.darker() to
ImageChops.lighter(). That’s it!
Run the code, and you’ll end up with the following image:
Chapter 10 - Channel Operations 245
This time the background is still in the image and it looks a lot like a blended image would. However,
here you can’t control the transparency of the images.
Now let’s learn how to compare two images to find the differences between them.
To make the comparison somewhat interesting, you can use this version of the butterfly that has a
logo superimposed on it:
Chapter 10 - Channel Operations 247
As you can see, it takes the two images and subtracts one from the other. Then it tries to apply an
absolute value to the result. It’s more complicated than that, but that’s the general idea that the
docstring describes. You can read more about it on Wikipedia²⁷.
Go ahead and create a new file named diff_image.py with the following code:
²⁷https://en.wikipedia.org/wiki/Blend_modes#Difference
Chapter 10 - Channel Operations 248
1 # diff_image.py
2
3 from PIL import Image, ImageChops
4
5 def diff(image_path_one, image_path_two, output_path):
6 image_one = Image.open(image_path_one)
7 image_two = Image.open(image_path_two)
8 image_three = ImageChops.difference(image_one, image_two)
9 image_three.save(output_path)
10
11
12 if __name__ == "__main__":
13 diff("yellow_butterfly.jpg", "watermarked_butterfly.jpg",
14 "diff_image.jpg")
Here you pass in the butterfly image as well as the watermarked butterfly image. You also tell your
function, diff(), where to save the output.
When you run this code, you will get the following output:
The black parts in the resulting image represent the pixels that are the same between the two images.
The watermark is only present in one of them, so it is highlighted in the output. If the image has
only a couple of pixels that are different, it can be hard to tell where the differences are. You may
need to write some extra code to make it more obvious in that case.
Now let’s move on and learn how to invert an image with ImageChops!
Inverting Images
The concept of inverting an image can mean a few things. One possible meaning is that you are
flipping an image. However, in this chapter, you are using ImageChops, which means that you are
doing a channel operation. In that context, it means that you are inverting the colors or creating a
negative of the image.
Here is the definition of the invert() function:
1 def invert(image):
2 """
3 Invert an image (channel).
4
5 out = MAX - image
6 """
This takes in an image object and does some magic to invert the image.
For this example, you will re-use this image of a lovely butterfly:
Chapter 10 - Channel Operations 250
Create a new file and name it invert_image.py. Then add the following code to your file:
1 # invert_image.py
2
3 from PIL import Image, ImageChops
4
5 def invert(image_path, output_path):
6 image = Image.open(image_path)
7 inverted_image = ImageChops.invert(image)
8 inverted_image.save(output_path)
9
10
11 if __name__ == "__main__":
12 invert("yellow_butterfly.jpg", "inverted.jpg")
Your code is a bit simpler than the previous examples in this chapter. That is because you are only
working with one image rather than two.
When you run your code, you will end up with this result:
Chapter 10 - Channel Operations 251
The ImageChop.invert() function does not give you any control over the inversion process. But it
does give you an easy way to create a negative version of your images.
Now let’s learn how to use ImageChops.soft_light()!
Well, that’s not very informative. Wikipedia²⁸ has a decent write-up on what it means though.
²⁸https://en.wikipedia.org/wiki/Blend_modes#Soft_Light
Chapter 10 - Channel Operations 252
First off, “soft light” is a blending technique. It is most closely related to an Overlay. According to
the link above, there are several different soft light algorithms. It’s hard to say which one Pillow
is using though. But it appears that no matter which one you use, it provides gamma correction.
Gamma is an operation that is used to encode and decode luminance in images.
There are mathematical algorithms mentioned in that URL if you’d like to dig into the nitty-gritty
details.
To see how you can apply the soft light algorithm yourself, create a new file and name it apply_-
soft_light.py. Then add this code to your new file:
1 # apply_soft_light.py
2
3 from PIL import Image, ImageChops
4
5 def soft_light(image_path_one, image_path_two, output_path):
6 image_one = Image.open(image_path_one)
7 image_two = Image.open(image_path_two)
8 image_three = ImageChops.soft_light(image_one, image_two)
9 image_three.save(output_path)
10
11
12 if __name__ == "__main__":
13 soft_light("shell.png", "skyline.png", "soft_light.jpg")
This code takes in two image paths and an output path. Then you create two Image objects and pass
them on to ImageChops.soft_light(). Finally, you save off the result.
You are applying a blending filter to your image. Here is the result:
Chapter 10 - Channel Operations 253
This looks nice. You should try it out with a few different pairs of images to see what you’ll get.
Now you’re ready to learn about “hard light”.
²⁹https://en.wikipedia.org/wiki/Blend_modes#Multiply_and_Screen
Chapter 10 - Channel Operations 254
All you can get from that is that it does sound like a blending technique. So once again, it is best to
write some code and see how it works.
For this example, create a file named apply_hard_light.py and fill it with the following:
1 # apply_hard_light.py
2
3 from PIL import Image, ImageChops
4
5 def hard_light(image_path_one, image_path_two, output_path):
6 image_one = Image.open(image_path_one)
7 image_two = Image.open(image_path_two)
8 image_three = ImageChops.hard_light(image_one, image_two)
9 image_three.save(output_path)
10
11
12 if __name__ == "__main__":
13 hard_light("shell.png", "skyline.png", "hard_light.jpg")
This code works the same way as the function you created in the last section. The only difference is
in the output.
Here it is:
Chapter 10 - Channel Operations 255
That looks different. As with all blending techniques or filters, beauty is in the eye of the beholder.
If you like it, then you may want to try it out with some other images.
Now let’s go and find out how to use overlay!
Overlay Images
The soft light algorithm produces a very similar look to the overlay. Pillow’s definition is that it is
superimposing two images using the “Overlay” algorithm.
You can see the code definition here:
Even if it did, that doesn’t mean that Pillow necessarily follows what Wikipedia says anyway. So
to find out what this blend mode does, create a new file and name it apply_overlay.py. Then enter
the following code:
1 # apply_overlay.py
2
3 from PIL import Image, ImageChops
4
5 def overlay(image_path_one, image_path_two, output_path):
6 image_one = Image.open(image_path_one)
7 image_two = Image.open(image_path_two)
8 image_three = ImageChops.overlay(image_one, image_two)
9 image_three.save(output_path)
10
11
12 if __name__ == "__main__":
13 overlay("shell.png", "skyline.png", "overlay.jpg")
When you run this code, it will overlay the two images and you’ll get a new image. Your new image
will look like this:
Chapter 10 - Channel Operations 257
If you compare this output to the output from the “soft light” code you wrote, you will see that
they are very similar. The overlay is a bit lighter / brighter, but the backgrounds of both are nearly
identical.
As with all of these different blending modes, it is best to try them out using different images to see
which one provides the type of effect you want.
To make that easier, in the next section you will create a simple GUI that lets you try out these
blending techniques.
Now open up your Python editor once more and create a new file named imagechops_gui.py. Then
enter the following code:
Chapter 10 - Channel Operations 259
1 # imagechops_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from add_images import add_images
10 from apply_hard_light import hard_light
11 from apply_overlay import overlay
12 from apply_soft_light import soft_light
13 from darken_image import darken
14 from diff_image import diff
15 from invert_image import invert
16 from lighten_image import lighten
17 from PIL import Image
18
19 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
20
21 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
22
23 effects = {
24 "Normal": shutil.copy,
25 "Addition": add_images,
26 "Darken": darken,
27 "Lighten": lighten,
28 "Difference": diff,
29 "Negative": invert,
30 "Hard Light": hard_light,
31 "Soft Light": soft_light,
32 "Overlay": overlay,
33 }
34
35
36 def apply_effect(values, window):
37 selected_effect = values["-EFFECTS-"]
38 image_file_one = values["-FILENAME_ONE-"]
39 image_file_two = values["-FILENAME_TWO-"]
40 if os.path.exists(image_file_one):
41 shutil.copy(image_file_one, tmp_file)
42 if selected_effect in ["Normal", "Negative"]:
43 effects[selected_effect](image_file_one, tmp_file)
Chapter 10 - Channel Operations 260
44 elif os.path.exists(image_file_two):
45 effects[selected_effect](
46 image_file_one, image_file_two, tmp_file,
47 )
48 elif selected_effect not in ["Normal", "Negative"]:
49 sg.popup(
50 "You need both images selected to apply this effect!",
51 )
52 return
53
54 image = Image.open(tmp_file)
55 image.thumbnail((400, 400))
56 bio = io.BytesIO()
57 image.save(bio, format="PNG")
58 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
59
60
61 def create_row(label, key, file_types):
62 return [
63 sg.Text(label),
64 sg.Input(size=(25, 1), key=key),
65 sg.FileBrowse(file_types=file_types),
66 ]
67
68
69 def save_image(*filenames):
70 save_filename = sg.popup_get_file(
71 "File", file_types=file_types, save_as=True, no_window=True,
72 )
73 if save_filename in filenames:
74 sg.popup_error(
75 "You are not allowed to overwrite the original images!",
76 )
77 else:
78 if save_filename:
79 shutil.copy(tmp_file, save_filename)
80 sg.popup(f"Saved: {save_filename}")
81
82
83 def main():
84 effect_names = list(effects.keys())
85 layout = [
86 [sg.Image(key="-IMAGE-", size=(400,400))],
Chapter 10 - Channel Operations 261
There are a lot of lines of code here. You will go over each piece of this code in smaller, digestible
chunks.
Here is the first chunk:
Chapter 10 - Channel Operations 262
1 # imagechops_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from add_images import add_images
10 from apply_hard_light import hard_light
11 from apply_overlay import overlay
12 from apply_soft_light import soft_light
13 from darken_image import darken
14 from diff_image import diff
15 from invert_image import invert
16 from lighten_image import lighten
17 from PIL import Image, ImageTk
18
19 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
20
21 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
22
23 effects = {
24 "Normal": shutil.copy,
25 "Addition": add_images,
26 "Darken": darken,
27 "Lighten": lighten,
28 "Difference": diff,
29 "Negative": invert,
30 "Hard Light": hard_light,
31 "Soft Light": soft_light,
32 "Overlay": overlay,
33 }
This first chunk of code shows all the imports you will need. There are a lot because you created
many modules in this chapter.
There are also three variables that you create:
This function should look mostly familiar if you have been reading the book front-to-back. Here you
pull out the currently selected effect as well as the two image paths that you want to use. Additionally,
you do a few extra checks.
First, you check if the effect is “Negative”. In that case, you can only pass one of the chosen image
files to the function. For this code, you chose the first file as the one to be inverted.
The next elif statement checks whether the image_file_two file exists. If it does, then it calls the
specified channel ops effect.
The final elif statement only runs if the second image file doesn’t exist and the user has chosen
an effect other than “Normal” or “Negative”. Those are the only effects that can be run with only
the first image. If they try to run a blending effect with only one image, it wouldn’t work. So you
display a warning to the user and prevent them from doing that.
The next function to check out is create_row():
Chapter 10 - Channel Operations 264
Here you create three Elements that make up a horizontal row in your user interface. These consist
of a label (sg.Text), a text input (sg.Input), and a file browse button (sg.FileBrowse).
Now you can turn your attention to the save_image() function:
1 def save_image(*original_filenames):
2 save_filename = sg.popup_get_file(
3 "File", file_types=file_types, save_as=True, no_window=True,
4 )
5 if save_filename in original_filenames:
6 sg.popup_error(
7 "You are not allowed to overwrite the original image!",
8 )
9 else:
10 if save_filename:
11 shutil.copy(tmp_file, save_filename)
12 sg.popup(f"Saved: {save_filename}")
This function pops up a dialog that asks the user where to save their new file. If the user tries to
overwrite one of the two original files, you show a popup message that tells them they are not
allowed to do that.
Otherwise, you copy the new file over to the location that they chose.
The last function to learn about is main():
1 def main():
2 effect_names = list(effects.keys())
3 layout = [
4 [sg.Image(key="-IMAGE-", size=(400,400))],
5 create_row("Image File 1:", "-FILENAME_ONE-", file_types),
6 [sg.Button("Load Image")],
7 create_row("Image File 2:", "-FILENAME_TWO-", file_types),
8 [
9 sg.Text("Effect"),
10 sg.Combo(
11 effect_names, default_value="Normal", key="-EFFECTS-",
Chapter 10 - Channel Operations 265
12 enable_events=True, readonly=True
13 ),
14 ],
15 [sg.Button("Save")],
16 ]
17
18 window = sg.Window("ImageChops GUI", layout, size=(450, 600))
The first half of main() defines your user interface. The effect_names are a Python list that comes
from the effects dictionary that you created at the beginning of your code. Then you create the
various Elements that will make up your user interface.
Once that is done, you pass that list of elements to sg.Window which will layout your interface.
Here is the rest of main():
This is your GUI’s event handler. If the user closes the window, it will exit your event loop and close
the GUI. If the user changes either of the images or chooses a different blending effect, then you will
call apply_effect() and update the display accordingly.
The last conditional statement checks if the user has pressed the “save” button and that there is a
value in the first filename text control. If both of those conditions are True, then you call save_-
image().
Chapter 10 - Channel Operations 266
Wrapping Up
You learned a lot in this chapter. The ImageChops module has lots of goodies. You can do a lot with
this module!
You covered these topics in this chapter:
• ImageChop Aliases
• Adding Images
• Using ImageChops.darker()
• Using ImageChops.lighter()
• Finding Differences in Images
• Inverting Images
• Using Soft Light on Images
• Using Hard Light on Images
• Overlay Images
• Creating a Blending GUI
This is a lot of information, but you didn’t even cover everything that ImageChops does. You should
check out the documentation to see the other bits and pieces. They work in much the same way as
the functions already covered here.
A good challenge would be to create some code around those other functions and add them to your
user interface. You could also update the user interface to invert both the images if they exist. Or
add a button to swap the two file choices and see if that affects the channel ops effect.
Chapter 11 - The ImageOps Module
Pillow’s ImageOps module is full of “ready-made” image processing operations that you can use on
your images. The documentation says that this module is somewhat experimental and most of the
functions only work with “L” and “RGB” modes.
If you need more information, you should check out the full documentation³¹.
In this chapter, you will learn about the following:
That is a lot of topics to cover. Get comfortable. This may take a while!
The first topic that you’ll learn about is applying automatic contrast.
You can see that in addition to adding a cutoff to the histogram, you can also tell autocontrast()
to ignore a specified background pixel value and provide a mask. To see how to use autocontrast(),
you will need an image.
You can use this cute photo of ducklings that can be found in the book’s GitHub code repository³²:
³²https://github.com/driscollis/image_processing_with_python
Chapter 11 - The ImageOps Module 269
Now fire up your Python editor and create a new file named apply_autocontrast.py. Then enter
this code:
1 # apply_autocontrast.py
2
3 from PIL import Image, ImageOps
4
5
6 def autocontrast(image_path, output_path, cutoff=0, ignore=None):
7 image = Image.open(image_path)
8 converted_image = ImageOps.autocontrast(image, cutoff, ignore)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 autocontrast("ducklings.jpg", "autocontrast.jpg", cutoff=0.5)
Your brand new code allows you to change the cutoff and ignore values that you pass along to
autocontrast(). In this example, you set the cutoff to 0.5.
Chapter 11 - The ImageOps Module 270
When you run this code, you will get the following output:
Give it a try and see how it works for you. Try setting the parameters to different settings and see if
that changes the output the way that you expect.
Let’s move on and learn about colorizing photos!
Colorizing Photos
Pillow provides a function for colorizing grayscale images. However, this colorize function doesn’t
use neural networks or other types of machine learning. Instead, it takes in a “black” and “white”
color and attempts to colorize the grayscale image based on what you set the black and white to.
You can also add a “mid” color to help fine tune the colorizing effect.
Here is the official colorize() definition:
Chapter 11 - The ImageOps Module 271
The colorize() function has several other parameters that you can use to help dial in your colorizing.
If you have a simple photo that only contains one or two colors, this function may work well enough
for you.
However, if you have a function with multiple colors, you should look into using OpenCV, Keras, or
another solution.
Back in chapter 2, you converted a Monarch caterpillar image to grayscale:
Chapter 11 - The ImageOps Module 272
You can use this image to try out the colorize() function. To get started, create a new file and name
it apply_colorize.py. Then enter the following code:
1 # apply_colorize.py
2
3 from PIL import Image, ImageOps
4
5
6 def colorize(image_path, output_path, black, white):
7 image = Image.open(image_path)
8 converted_image = ImageOps.colorize(image, black, white)
9 converted_image.save(output_path)
10
11
Chapter 11 - The ImageOps Module 273
12 if __name__ == "__main__":
13 colorize("gray_caterpillar.jpg", "color_caterpillar.jpg",
14 black="green", white="white")
This is an attempt to colorize the photo. When you run this code, you will end up with the following
result:
That added a nice green overlay, but it didn’t colorize the photo. The reason for that is because Pillow
isn’t harnessing a trained model, so it is on you to keep tweaking it until you get what you want.
Now you are ready to learn how to add padding to an image.
Padding an Image
The ImageOps.pad() function is used for creating a sized and padded version of the image. It will
expand to fill the requested aspect ratio and size that is passed to it. What this does is that it will
add a border if the new image cannot fit into the new size.
Here is the function definition so that you can see all the parameters that pad() takes:
Chapter 11 - The ImageOps Module 274
The defaults that pad() uses are good. Note that color defaults to None. This maps to the color black.
If you’d rather use a different color, you can pass it a color tuple, integer, or a supported color name.
For this section, you will be using this image:
Chapter 11 - The ImageOps Module 275
Now go ahead and create a new file. You will name it apply_padding.py. Then you can enter the
following code:
1 # apply_padding.py
2
3 from PIL import Image, ImageOps
4
5
6 def pad(image_path, output_path, size):
7 image = Image.open(image_path)
8 converted_image = ImageOps.pad(image, size=size, color="red")
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 pad("flowers.jpg", "flowers_padded.jpg", size=(1200, 600))
In this example, you change the default background color to “red”. You also set the file size to 1200
x 600 pixels. The pad() function will resize the image to fit those dimensions while maintaining the
image’s aspect ratio.
Chapter 11 - The ImageOps Module 276
When you run this code, you will get the following:
That looks nice! You should give this code a spin with a few of your photos.
Now it’s time to learn about a closely related function called expand().
Adding a Border
The expand() function is rather poorly named. Its express purpose is to add a border to an image. It
is kind of surprising that it isn’t named as such.
Here is the function’s definition:
The expand() function takes in an image object, the border width (in pixels), and the color (fill)
that the border should be. Then it returns a new image object with a new border added to it.
Chapter 11 - The ImageOps Module 277
You can see how this works by creating a new file and naming it apply_expand.py. Then add this
code to it:
1 # apply_expand.py
2
3 from PIL import Image, ImageOps
4
5
6 def expand(image_path, output_path, border, fill):
7 image = Image.open(image_path)
8 converted_image = ImageOps.expand(image, border, fill)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 expand("flowers.jpg", "flower_border.jpg", border=100, fill="yellow")
Here you pass in a border of 100 pixels and you tell Pillow that you want the border to be yellow.
When you run this code, you will end up with the following image:
Chapter 11 - The ImageOps Module 278
You can use any of the pre-defined color names that are included with Pillow. Alternatively, you
could pass in an RGB tuple for more fine-grained control over the border color. Give it a try and see
how your images look with a nice, new border!
Now let’s find out how to remove a border using ImageOps!
Removing a Border
Pillow’s ImageOps module supports removing a border too. You can do so using the crop() method.
Here is its definition:
Chapter 11 - The ImageOps Module 279
This one is a bit simpler than the expand() function was. The crop() function takes in two arguments:
the image object and border. The border is the number of pixels you want to remove from all sides.
You can test this out by using the image you created in the previous example: flower_border.jpg.
Create a new file and name it apply_crop.py. Then enter the following code:
1 # apply_crop.py
2
3 from PIL import Image, ImageOps
4
5 def crop(image_path, output_path, border):
6 image = Image.open(image_path)
7 converted_image = ImageOps.crop(image, border)
8 converted_image.save(output_path)
9
10
11 if __name__ == "__main__":
12 crop("flower_border.jpg", "flower_no_border.jpg", border=100)
This code will take in your flower photo with the yellow border and remove the border from it. The
photo should now look like the original flower photo. You can use this method to crop off regular
pixels too. It doesn’t care if there is a border there or not.
Now you are ready to learn how to scale an image.
Scaling an Image
The ImageOps module provides a scale() function. Scaling is the process of expanding or contracting
an image while maintaining its aspect ratio (i.e., not stretching the photo).
Here is how Pillow defines scale():
Chapter 11 - The ImageOps Module 280
If you set the factor to a value between 0 and 1, the image will be scaled down. For example, if you
chose 0.2, it would scale the image down to 20% (i.e. 80% smaller). If you were to set the factor to
something above 1, it will scale the image up. For example, if you set the value to 1.5, Pillow would
take the width and height of the image and scale the image 1.5 times larger (i.e. 50% bigger).
Pillow allows you to set the resample parameter to something other than Image.BICUBIC. Image
resampling is when an algorithm changes the number of pixels in an image. Pillow supports
the following resampling types: PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC, and
PIL.Image.ANTIALIAS.
Note that if you expand the image, the result may be blurrier than the original as you are, in
effect, adding new pixels to the image that weren’t there originally.
To see how this works, create a new file named apply_scale.py. Then enter the following code:
1 # apply_scale.py
2
3 from PIL import Image, ImageOps
4
5
6 def scale(image_path, output_path, factor):
7 image = Image.open(image_path)
8 converted_image = ImageOps.scale(image, factor)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 scale("flowers.jpg", "flower_scaled.jpg", factor=0.6)
For this example, you will set the factor to 0.6, which will reduce the image’s size by 40%. The
scaled-down image will look the same as the original, but its size will be greatly reduced. The file
size will be different as well.
Chapter 11 - The ImageOps Module 281
Now you can move on and learn about equalizing a photo using its histogram!
For this example, you will re-use the flower image from earlier. Here it is again for reference:
Chapter 11 - The ImageOps Module 282
Now create a new file and name it apply_equalize.py. Then add this code:
1 # apply_equalize.py
2
3 from PIL import Image, ImageOps
4
5
6 def equalize(image_path, output_path):
7 image = Image.open(image_path)
8 converted_image = ImageOps.equalize(image)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 equalize("flowers.jpg", "flowers_equalized.jpg")
This will take in your image, extract the image’s histogram and then use that to equalize the photo.
This is easy to say, but hard to imagine.
If you run this code though, you can see the result will look like this:
Chapter 11 - The ImageOps Module 283
The image is paler than before and not as vibrant. But it’s not bad either. You should try this code
out on some other photos to get a feel for how it behaves.
Now you are ready to learn how to resize and crop a photo in one fell swoop!
This docstring is quite nice and explains what each of the parameters is for. You will use the flower
photo again for this example.
To see how this works, create a file named apply_fit.py and add this code to your new file:
1 # apply_fit.py
2
3 from PIL import Image, ImageOps
4
5
6 def fit(image_path, output_path, size, centering=(0.5, 0.5)):
7 image = Image.open(image_path)
8 converted_image = ImageOps.fit(image, size, centering=centering)
9 converted_image.save(output_path)
Chapter 11 - The ImageOps Module 285
10
11
12 if __name__ == "__main__":
13 fit("flowers.jpg", "flowers_fitted.jpg", size=(400, 400))
This code will let you set the size of your output image as well as change the center of the image.
You set the size parameter to be 400x400 pixels. You leave centering at its default value to produce
a centered crop.
When you run this code, your new image will look like this:
Your code has cropped the right and left sides of the image off. Now go back and set centering to
(1.0, 0.0).
Now when you run the code, fit() will crop from the bottom left corner and your result will look
like this:
Chapter 11 - The ImageOps Module 286
You should try some other centering settings so you can better understand how they work.
The next topic to learn about is how to flip an image with ImageOps!
Flipping an Image
Pillow’s ImageOps module provides a flip() function that will flip an image vertically from top to
bottom. Back in chapter 5, you learned how to use transpose() to flip images. This is the same idea
here except with slightly less code.
The definition of the function is straightforward:
Chapter 11 - The ImageOps Module 287
1 def flip(image):
2 """
3 Flip the image vertically (top to bottom).
4
5 :param image: The image to flip.
6 :return: An image.
7 """
You might use a function like this if you happened to take a photo while standing on your head. Or,
more likely, if you took a photo with your camera upside down. Of course, some people like to flip
images for artistic purposes.
For this example, you will re-use the duckling’s photo that you used at the beginning of the chapter:
Now to flip those ducks, you will need to create a new file and name it apply_flip.py. Then enter
this code in your new file:
Chapter 11 - The ImageOps Module 288
1 # apply_flip.py
2
3 from PIL import Image, ImageOps
4
5
6 def flip(image_path, output_path):
7 image = Image.open(image_path)
8 converted_image = ImageOps.flip(image)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 flip("ducklings.jpg", "ducklings_flipped.jpg")
This code simply takes in the input image_path and the output_path of the newly flipped image.
When you run this code, your ducks will be flipped:
Take some of your favorite photos and give them a new orientation. Maybe it will take a bland photo
and make it look more appealing. It’s a conversation starter at least!
Chapter 11 - The ImageOps Module 289
The next function is similar to flip() except that it will mirror the image rather than flipping it.
Mirroring an Image
The ImageOps module also contains a mirror() function. This is equivalent to calling transpose(Image.FLIP_-
LEFT_RIGHT) on the image object, which was covered back in chapter 5.
1 def mirror(image):
2 """
3 Flip image horizontally (left to right).
4
5 :param image: The image to mirror.
6 :return: An image.
7 """
This function works in much the same way as the flip() function except that it flips the image
horizontally rather than vertically. You can take the code you wrote in the last section, create a new
file named apply_mirror.py and paste that code in there.
Then change the code so that it calls mirror() instead.:
1 # apply_mirror.py
2
3 from PIL import Image, ImageOps
4
5
6 def mirror(image_path, output_path):
7 image = Image.open(image_path)
8 converted_image = ImageOps.mirror(image)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 mirror("ducklings.jpg", "ducklings_mirrored.jpg")
You also change the name of your function to mirror() to match the ImageOps one and to keep
everything named consistently.
When you run this code, you will see that the ducks are now mirrored, rather than flipped:
Chapter 11 - The ImageOps Module 290
That output is what you would expect to happen when you mirror an image. Give this a try on your
own and see how it works.
Now you’ll learn about inverting an image in the next section.
Inverting an Image
You may recall that in chapter 10, you learned how to invert an image using ImageChops.invert().
The ImageOps module also has an invert() method. Both methods produce a negative image using
the image object that is passed in.
Here is the image() function’s definition:
Chapter 11 - The ImageOps Module 291
1 def invert(image):
2 """
3 Invert (negate) the image.
4
5 :param image: The image to invert.
6 :return: An image.
7 """
If you open up the actual code for the invert() function and compare it with the ImageChops.invert()
function, you will see that they work in very different ways. They are not simply aliases of each
other. ImageChops inverts the channel while ImageOps uses a lookup table to do the inversion.
To see how you might use this function, create a new file named apply_invert.py and add this code
to it:
1 # apply_invert.py
2
3 from PIL import Image, ImageOps
4
5
6 def invert(image_path, output_path):
7 image = Image.open(image_path)
8 converted_image = ImageOps.invert(image)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 invert("ducklings.jpg", "ducklings_inverted.jpg")
This time you will create a negative image of the duckling’s photo.
When you run this code, you will get the following:
Chapter 11 - The ImageOps Module 292
You should try running the ducklings photo through the ImageChops.invert() function and see if
the output is any different.
Now let’s move on and learn what it means to posterize an image.
Posterize an Image
You can use Pillow to posterize an image. This is a fancy term for color contraction. You pass in the
image you want to posterize as well as the number of bits you want to be in each color channel.
Wikipedia defines Posterization as “the conversion of a continuous gradation of tone to several
regions of fewer tones, with abrupt changes from one tone to another.”
You can read more about the process on Wikipedia³³.
Posterization was originally used to create posters, hence the name.
Here is Pillow’s posterize() definition:
³³https://en.wikipedia.org/wiki/Posterization
Chapter 11 - The ImageOps Module 293
For this example, you will use this new image of a jellyfish:
Go ahead and create a new file named apply_posterize.py. Then add this new code to it:
Chapter 11 - The ImageOps Module 294
1 # apply_posterize.py
2
3 from PIL import Image, ImageOps
4
5
6 def posterize(image_path, output_path, bits):
7 image = Image.open(image_path)
8 converted_image = ImageOps.posterize(image, bits=bits)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 posterize("jellyfish.jpg", "jelly_posterize.jpg", bits=2)
Here you pass in your photo of a jellyfish along with where you want your code to create the output
image. Then you tell Pillow that you only want two bits per channel.
When you run this code, your output will look like this:
That looks cool. You could even make a poster out of it! Go try it out on a few of your photos to see
what you can do.
Now it’s time to learn how to solarize an image.
Solarize an Image
Solarization is another fancy photography term. It is used to describe the effect of tone reversal,
which you will sometimes see in photos that have been extremely overexposed. This is especially
noticeable in photos that included the sun or other bright lights. When they are extremely
overexposed, the sun will turn from white to black or gray.
You can read more about the general concepts of Solarization on [.
Pillow’s solarize() function lets you specify a threshold from 0-255. Anything above the threshold
will be inverted. The default is 128.
Here is the function signature:
You will re-use the jellyfish photo for the solarize effect, even though it is not overexposed. To get
started, create a new file and name it apply_solarize.py.
Now add this code to the file:
1 # apply_solarize.py
2
3 from PIL import Image, ImageOps
4
5
6 def solarize(image_path, output_path, threshold=128):
7 image = Image.open(image_path)
8 converted_image = ImageOps.solarize(image, threshold=threshold)
9 converted_image.save(output_path)
10
11
12 if __name__ == "__main__":
13 solarize("jellyfish.jpg", "jelly_solarize.jpg")
Chapter 11 - The ImageOps Module 296
In this example, you set the threshold to 128, which is the default.
When you run this code, the output will look like this:
Go back and change the threshold and then re-run the code. By trying out different thresholds with
different photographs, you will discover how to create very different effects.
Now you are ready to learn about the exif_transpose() function!
You can see the full code for exif_transpose() reproduced below:
1 def exif_transpose(image):
2 """
3 If an image has an EXIF Orientation tag, return a new image that is
4 transposed accordingly. Otherwise, return a copy of the image.
5
6 :param image: The image to transpose.
7 :return: An image.
8 """
9 exif = image.getexif()
10 orientation = exif.get(0x0112)
11 method = {
12 2: Image.FLIP_LEFT_RIGHT,
13 3: Image.ROTATE_180,
14 4: Image.FLIP_TOP_BOTTOM,
15 5: Image.TRANSPOSE,
16 6: Image.ROTATE_270,
17 7: Image.TRANSVERSE,
18 8: Image.ROTATE_90,
19 }.get(orientation)
20 if method is not None:
21 transposed_image = image.transpose(method)
22 del exif[0x0112]
23 transposed_image.info["exif"] = exif.tobytes()
24 return transposed_image
25 return image.copy()
After exif_transpose() transposes the image to the correct orientation, it modifies the orientation
Exif tag in the new version of the image that it returns.
You will use this photo of a snowman that was purposely photographed with the camera being held
upside down:
Chapter 11 - The ImageOps Module 298
You might be wondering why this snowman isn’t upside down if the camera itself was upside down.
Most cameras will record the orientation that it is upside down, but when they display the image, it
will display it right-side up. This is also true of most image viewing applications on computers.
To see this function in action, create a new file named apply_exif_transpose.py and add this code
to it:
1 # apply_exif_transpose.py
2
3 from PIL import Image, ImageOps
4
5
6 def exif_transpose(image_path, output_path):
7 image = Image.open(image_path)
8
9 exif = image.getexif()
10 orientation = exif.get(0x0112)
11 print(f"Orientation = {orientation}")
12
13 converted_image = ImageOps.exif_transpose(image)
Chapter 11 - The ImageOps Module 299
14 converted_image.save(output_path)
15
16
17 if __name__ == "__main__":
18 exif_transpose("snowman.jpg", "snowman_exif_transposed.jpg")
This code borrows a couple of lines of code from the actual ImageOps.exif_transpose() function to
get the orientation of the input image. It prints out the orientation to stdout so that you know if the
image will be transposed. In this case, the output should mention that the orientation is an 8, which
means it is upside-down.
The result will show the image right-side-up. You can experiment with your camera by taking photos
of different things and seeing if it automatically turns them right-side-up or not. Most photos of
animals and people will automatically be turned right-side up. Try this code out on some of your
photos and see what happens!
Now it’s time to find out how to wrap some of these functions inside of a GUI.
You can always come back and rework the GUI to add your favorite function if it is not already
Chapter 11 - The ImageOps Module 301
1 # imageops_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from apply_autocontrast import autocontrast
10 from apply_equalize import equalize
11 from apply_flip import flip
12 from apply_invert import invert
13 from apply_mirror import mirror
14 from apply_solarize import solarize
15 from PIL import Image
16
17 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
18 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
19
20 effects = {
21 "Normal": shutil.copy,
22 "Autocontrast": autocontrast,
23 "Equalize": equalize,
24 "Flip": flip,
25 "Mirror": mirror,
26 "Negative": invert,
27 "Solarize": solarize,
28 }
29
30
31 def apply_effect(image_file_one, effect, image_obj):
32 if os.path.exists(image_file_one):
33 effects[effect](image_file_one, tmp_file)
34 image = Image.open(tmp_file)
35 image.thumbnail((400, 400))
36 bio = io.BytesIO()
37 image.save(bio, format="PNG")
38 image_obj.update(data=bio.getvalue(), size=(400,400))
Chapter 11 - The ImageOps Module 302
39
40
41 def create_row(label, key, file_types):
42 return [
43 sg.Text(label),
44 sg.Input(size=(25, 1), key=key),
45 sg.FileBrowse(file_types=file_types),
46 ]
47
48
49 def save_image(filename_one):
50 save_filename = sg.popup_get_file(
51 "File", file_types=file_types, save_as=True, no_window=True,
52 )
53 if save_filename == filename_one:
54 sg.popup_error("You are not allowed to overwrite the original image!")
55 else:
56 if save_filename:
57 shutil.copy(tmp_file, save_filename)
58 sg.popup(f"Saved: {save_filename}")
59
60
61 def main():
62 effect_names = list(effects.keys())
63 layout = [
64 [sg.Image(key="-IMAGE-", size=(400,400))],
65 create_row("Image File 1:", "-FILENAME_ONE-", file_types),
66 [sg.Button("Load Image")],
67 [
68 sg.Text("Effect"),
69 sg.Combo(
70 effect_names,
71 default_value="Normal",
72 key="-EFFECTS-",
73 enable_events=True,
74 readonly=True,
75 ),
76 ],
77 [sg.Button("Save")],
78 ]
79
80 window = sg.Window("ImageOps GUI", layout, size=(450, 550))
81 image = window["-IMAGE-"]
Chapter 11 - The ImageOps Module 303
82
83 events = ("Load Image", "-EFFECTS-")
84 while True:
85 event, values = window.read()
86 if event == "Exit" or event == sg.WIN_CLOSED:
87 break
88
89 filename_one = values["-FILENAME_ONE-"]
90 effect = values["-EFFECTS-"]
91
92 if event in events:
93 apply_effect(filename_one, effect, image)
94 if event == "Save" and filename_one:
95 save_image(filename_one)
96
97 window.close()
98
99
100 if __name__ == "__main__":
101 main()
This is a good-sized chunk of code. You will go over it in pieces to make it easier to digest.
The first part to understand are the imports and module-level variables:
1 # imageops_gui.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 import shutil
7 import tempfile
8
9 from apply_autocontrast import autocontrast
10 from apply_equalize import equalize
11 from apply_flip import flip
12 from apply_invert import invert
13 from apply_mirror import mirror
14 from apply_solarize import solarize
15 from PIL import Image
16
17 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]
18 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
Chapter 11 - The ImageOps Module 304
19
20 effects = {
21 "Normal": shutil.copy,
22 "Autocontrast": autocontrast,
23 "Equalize": equalize,
24 "Fit": fit,
25 "Flip": flip,
26 "Mirror": mirror,
27 "Negative": invert,
28 "Posterize": posterize,
29 "Solarize": solarize,
30 }
This piece of the puzzle shows you all the required imports that make your GUI possible. You also
create three variables:
• file_types - the types of files you can select when opening or saving
• tmp_file - where to save an intermediary image file
• effects - a dictionary of effects that can be applied to the selected image
The first function you will learn about defines how to apply an effect to your image:
The code above may look familiar to you if you have read the last few chapters. It is a variation
on the same function from those chapters, after all. Here you check that a source image has been
selected. Next, you apply the effect by calling one of the mapped functions from the dictionary.
Finally, you update the displayed image so that the user can see how the effect has changed the
image. The shown image is now the copied file.
The next function is called create_row():
Chapter 11 - The ImageOps Module 305
This is a direct copy from the imagechops_gui.py file that you created in the previous chapter. It
creates a list of Elements that will be displayed horizontally. This list consists of a label (sg.Text), a
text box (sg.Input), and a file browse (sg.FileBrowse) button.
Now you can turn your attention to save_image():
1 def save_image(filename_one):
2 save_filename = sg.popup_get_file(
3 "File", file_types=file_types, save_as=True, no_window=True,
4 )
5 if save_filename == filename_one:
6 sg.popup_error("You are not allowed to overwrite the original image!")
7 else:
8 if save_filename:
9 shutil.copy(tmp_file, save_filename)
10 sg.popup(f"Saved: {save_filename}")
The save_image() function will save your new image off to a new location. It asks the user where
to save their new image and prevents the user from overwriting the original.
The last function to learn about is main(). Here is the first half of that function:
1 def main():
2 effect_names = list(effects.keys())
3 layout = [
4 [sg.Image(key="-IMAGE-", size=(400,400))],
5 create_row("Image File 1:", "-FILENAME_ONE-", file_types),
6 [sg.Button("Load Image")],
7 [
8 sg.Text("Effect"),
9 sg.Combo(
10 effect_names,
11 default_value="Normal",
12 key="-EFFECTS-",
13 enable_events=True,
14 readonly=True,
15 ),
Chapter 11 - The ImageOps Module 306
16 ],
17 [sg.Button("Save")],
18 ]
19
20 window = sg.Window("ImageOps GUI", layout, size=(450, 550))
This block of code defines your user interface. It creates all the Elements that are shown. The last
line creates the Window and lays out your interface for you.
The last portion of the code contains your event loop and a couple of variables:
1 image = window["-IMAGE-"]
2
3 events = ("Load Image", "-EFFECTS-")
4 while True:
5 event, values = window.read()
6 if event == "Exit" or event == sg.WIN_CLOSED:
7 break
8
9 filename_one = values["-FILENAME_ONE-"]
10 effect = values["-EFFECTS-"]
11
12 if event in events:
13 apply_effect(filename_one, effect, image)
14 if event == "Save" and filename_one:
15 save_image(filename_one)
16
17 window.close()
18
19
20 if __name__ == "__main__":
21 main()
Here you grab the image Element and assign it to a variable. You also create a tuple of events. You
will then check if the current event is contained in the tuple. If it is, then you call apply_effect().
The other two events that are handled here are the “Exit” event, which closes your application, and
the “Save” event, which will call save_image() to save your new image.
Give your user interface a try and see what new creations you can make. Then start modifying the
user interface so that it can do more!
Chapter 11 - The ImageOps Module 307
Wrapping Up
The ImageOps modules have tons of functionality built-in. You can do many different tasks with it
and the functions are almost all straight-forward. The documentation for the module is very brief,
but the docstrings on the actual functions are clear and to-the-point.
In this chapter, you covered the following:
Go give the ImageOps module a try. You will find it is very useful and easy to use. The GUI you made
to wrap its functions only enhances your ability to use the module!
Chapter 12 - Pillow Integration with
GUI Toolkits
The Pillow package is a dependency of many other packages. You can use Pillow to help display
images within many Python GUI toolkits. In this section, you will learn how Pillow is used by some
of the most popular Python GUI toolkits.
You will cover Pillow’s integration (or lack thereof) with the following GUI toolkits:
• Kivy
• PySimpleGUI
• PyQt
• Tkinter
• wxPython
You will be using the following image in the code examples for this chapter:
Chapter 12 - Pillow Integration with GUI Toolkits 309
Kivy
Kivy is one of the only Python GUI toolkits that allow Android and iOS development. It also runs
on Linux, Mac, and Windows. Kivy does not use a native widget on any platform. Instead, a Kivy
application draws its widgets, so it will always look the same no matter which system it runs on.
Kivy does not come with Python. You can learn more about Kivy and how to install it here³⁴.
Kivy does not have Pillow integration built-in. Pillow also does not support Kivy. To display an
image in Kivy, you would need to use one of its widgets, such as this one:
• kivy.uix.image.Image
Kivy’s Image widget supports many different image types out of the box. You don’t need Pillow
unless it supports something that Kivy does not.
³⁴https://kivy.org/
Chapter 12 - Pillow Integration with GUI Toolkits 310
While you can’t use a Pillow module to convert a PIL.Image object to something readable by Kivy,
there is a workaround that you can use. That workaround is Python’s io module.
To get started, fire up your Python editor and create a new file named kivy_pillow_demo.py. Then
enter the following code:
1 # kivy_pillow_demo.py
2
3 from io import BytesIO
4 from kivy.app import App
5 from kivy.core.image import Image as CoreImage
6 from kivy.uix.image import Image
7 from PIL import Image as PilImage
8
9 class MyApp(App):
10
11 def build(self):
12 image = Image(source="")
13 pil_image = PilImage.open("pink_flower.jpg")
14
15 # Save PIL image to memory
16 img_data = BytesIO()
17 pil_image.save(img_data, format='png')
18 img_data.seek(0)
19
20 # Update Kivy Image
21 image.texture = CoreImage(img_data, ext="png").texture
22 image.reload()
23
24 return image
25
26 # run the App
27 MyApp().run()
Here you import a few different items from Kivy. You also import Image from PIL, but you renamed
it to PilImage because Kivy has its own Image class. The next step is to subclass Kivy’s App() class.
Here you need to override build(). This will initialize your application with a widget tree.
Inside of build(), you create an empty Kivy Image and then create a Pillow Image object. To make
the PIL object work with Kivy, you need to save it to a BytesIO object. This causes the image to be
saved to memory. You set the format to PNG. Then you read that data into a new BytesIO object.
The next to last step is to pass that image into a CoreImage() class.
That allows you to set the original Kivy Image texture to the CoreImage texture. Yes, it’s very
Chapter 12 - Pillow Integration with GUI Toolkits 311
convoluted and weird. Now finish the function by calling reload(), which will refresh the image
with your PIL image.
When you run this code, you will get an application that looks similar to this:
1 # kivy_image.py
2
3 from kivy.app import App
4 from kivy.uix.image import Image
5
6 class ImageViewer(App):
7
8 def build(self):
9 return Image(source="pink_flower.jpg")
10
11 # run the App
12 ImageViewer().run()
That is much shorter and the application will display in the same way as before.
Now you are ready to learn about Pillow’s integration with PySimpleGUI.
PySimpleGUI
You have been using PySimpleGUI throughout this book. The default version of PySimpleGUI wraps
the Tkinter GUI toolkit. With that in mind, you can use a special module from Pillow to convert
Pillow Images into something that PySimpleGUI can understand. That special module is called
ImageTk.
Note: Tkinter only supports GIF and PGM/PPM image formats. If you want to display other
image formats, you may need to use Pillow or an alternative library.
The ImageTk module was created to support Tkinter’s BitmapImage and PhotoImage objects. You can
read more about it in the Pillow documentation³⁵.
To use ImageTk in PySimpleGUI, create a new file and name it psg_pillow_demo.py. Then add the
following code:
1 # psg_pillow_demo.py
2
3 import PySimpleGUI as sg
4 from PIL import Image, ImageTk
5
6 file_types = [("JPEG (*.jpg)", "*.jpg"),
7 ("All files (*.*)", "*.*")]
8
9
10 def main():
³⁵https://pillow.readthedocs.io/en/stable/reference/ImageTk.html
Chapter 12 - Pillow Integration with GUI Toolkits 313
11 elements = [
12 [sg.Image(key="image")],
13 [
14 sg.Text("Image File"),
15 sg.Input(size=(25, 1), enable_events=True, key="file"),
16 sg.FileBrowse(file_types=file_types),
17 ],
18 ]
19
20 window = sg.Window("Image Viewer", elements)
21
22 while True:
23 event, values = window.read()
24 if event == "Exit" or event == sg.WIN_CLOSED:
25 break
26 if event == "file":
27 image = Image.open(values["file"])
28 image.thumbnail((400, 400))
29 photo_img = ImageTk.PhotoImage(image)
30 window["image"].update(data=photo_img)
31
32 window.close()
33
34
35 if __name__ == "__main__":
36 main()
This example is a modified version of the image_viewer.py program that you wrote back in chapter
1.
The key chunk of code that you should focus on is here:
1 image = Image.open(values["file"])
2 image.thumbnail((400, 400))
3 photo_img = ImageTk.PhotoImage(image)
4 window["image"].update(data=photo_img)
You use ImageTk.PhotoImage() to convert the PIL image object into something that PySimpleGUI
can display in its sg.Image() Element.
When you run this code, your GUI will look like this:
Chapter 12 - Pillow Integration with GUI Toolkits 314
Now take a look at a simplified version of the original code for image_viewer.py:
1 # image_viewer.py
2
3 import io
4 import os
5 import PySimpleGUI as sg
6 from PIL import Image
7
8
9 file_types = [("JPEG (*.jpg)", "*.jpg"),
10 ("All files (*.*)", "*.*")]
11
12 def main():
13 layout = [
14 [sg.Image(key="-IMAGE-")],
15 [
16 sg.Text("Image File"),
Chapter 12 - Pillow Integration with GUI Toolkits 315
In this case, you use Python’s io module to convert the PIL image object into something that
sg.Image can understand.
1 image = Image.open(values["-FILE-"])
2 image.thumbnail((400, 400))
3 bio = io.BytesIO()
4 image.save(bio, format="PNG")
5 window["-IMAGE-"].update(data=bio.getvalue())
This code saves the image object into memory using BytesIO(). Then you read the data from memory
and pass that to your Image object to update it on-screen. The main benefit of doing it this way is
that you no longer depend on ImageTk and this allows you to use the same code in PySimpleGUIWx
and PySimpleGUIQt.
Now let’s move on and learn if Pillow has support for PyQt!
Chapter 12 - Pillow Integration with GUI Toolkits 316
PyQt
Qt is a cross-platform GUI framework written in C++. There are two Python bindings for it.
PyQt5 and Qt for Python (PySide6). PyQt5 was created by Riverbank Computing. Qt for Python
is the official Python bindings for Qt. You can use either of these frameworks almost completely
interchangeably by just changing which one you are importing.
The PyQt and Qt for Python frameworks support most major image formats. For a full listing, you
can visit Qt’s documentation³⁶.
Both of these frameworks can be installed using pip. Here is how you would install PyQt:
1 # pyqt_pillow_demo.py
2
3 import sys
4
5 from PIL import Image, ImageQt
6 from PyQt5.QtWidgets import QWidget, QLabel
7 from PyQt5.QtWidgets import QVBoxLayout, QApplication
8
9
10 class ImageViewer(QWidget):
11
12 def __init__(self):
13 QWidget.__init__(self)
14 self.setWindowTitle("PyQt Image Viewer")
15
16 # Open up image in Pillow
17 image = Image.open("pink_flower.jpg")
18 pixmap = ImageQt.toqpixmap(image)
19
20 self.image_label = QLabel('')
³⁶https://doc.qt.io/qt-5/qtimageformats-index.html
Chapter 12 - Pillow Integration with GUI Toolkits 317
21 self.image_label.setPixmap(pixmap)
22
23 self.main_layout = QVBoxLayout()
24 self.main_layout.addWidget(self.image_label)
25 self.setLayout(self.main_layout)
26
27
28 if __name__ == "__main__":
29 app = QApplication(sys.argv)
30 viewer = ImageViewer()
31 viewer.show()
32 app.exec_()
Here you import Image and ImageQt from PIL. Then you import a couple of items from PyQt5. The
PyQt5 package needs you to have a main window of some sort. You are using QWidget for that
purpose here by subclassing it. Then you open up the flower image using Pillow.
Next, you transform the Image object into a QPixmap object that PyQt can display. To show the image
to the user, you use the QLabel widget and set its pixmap to the pixmap object you created. Then
you add your label to a layout object (QVBoxLayout).
Layout objects are used by PyQt5 for controlling how multiple widgets are laid out in your
application. They are also used to help with resizing your application, although in this case, you
don’t need that functionality.
The last few lines of code create a QApplication object, instantiate your ImageViewer() class, and
then shows the viewer to the user. The last line starts the event loop so that your new user interface
can accept mouse and keyboard events.
When you run this code, your PyQt GUI will look like this:
Chapter 12 - Pillow Integration with GUI Toolkits 318
If you only need to display an image in PyQt, you can simplify the code a bit to look like this:
1 # pyqt_image_viewer.py
2
3 import sys
4
5 from PyQt5.QtGui import QPixmap
6 from PyQt5.QtWidgets import QWidget, QLabel
7 from PyQt5.QtWidgets import QVBoxLayout, QApplication
8
9
10 class ImageViewer(QWidget):
11
12 def __init__(self):
13 QWidget.__init__(self)
14 self.setWindowTitle("PyQt Image Viewer")
15
16 self.image_label = QLabel()
Chapter 12 - Pillow Integration with GUI Toolkits 319
17 self.image_label.setPixmap(QPixmap('./pink_flower.jpg'))
18
19 self.main_layout = QVBoxLayout()
20 self.main_layout.addWidget(self.image_label)
21 self.setLayout(self.main_layout)
22
23
24 if __name__ == "__main__":
25 app = QApplication(sys.argv)
26 viewer = ImageViewer()
27 viewer.show()
28 app.exec_()
Here you remove the Pillow-related imports and add a new import of the QPixmap. This takes in a
path to the image file and turns it into something that the label can display. The rest of the code is
the same.
You should give PyQt a try if you haven’t already. It is one of the most full-featured toolkits out
there.
Now let’s find out if Pillow has support for the Tkinter GUI toolkit!
Tkinter
Tkinter comes with Python on Windows. It sometimes comes with Python on Mac and Linux, but
you may have to install it separately. Tkinter is based on a language named TCL. Out of the box, it
only supports the display of GIF and PGM/PPM image types. If you want to be able to view other
image types in Tkinter, you will want to use Pillow.
Pillow includes the ImageTk module, which converts a Pillow Image object into something that
Tkinter can display to the user.
You can see how this works by creating a new file named tk_pillow_demo.py and entering this code
into it:
1 # tk_pillow_demo.py
2
3 from tkinter import Tk, Canvas, NW
4 from PIL import Image, ImageTk
5
6
7 def main():
8 root = Tk()
9 root.title("Tkinter Image Viewer")
Chapter 12 - Pillow Integration with GUI Toolkits 320
10 pil_img = Image.open("pink_flower.jpg")
11 width, height = pil_img.size
12 canvas = Canvas(root, width=width, height=height)
13 canvas.pack()
14 img = ImageTk.PhotoImage(pil_img)
15 canvas.create_image(20, 20, anchor=NW, image=img)
16 root.mainloop()
17
18 if __name__ == "__main__":
19 main()
Tkinter doesn’t rely on classes as PyQt and wxPython do. You can use them if you want, but you
can also write your code in a very functional manner. In this case, you create a main() function that
creates a root object. This is based on Tk(), which is the “Window” that holds all the other widgets
that you use in Tkinter.
Next, you open an image in Pillow and get its size. Then you use that size to create a Tkinter Canvas
object that you can use to display the image. The Canvas object’s parent is root, which means that
the Canvas widget is put inside of the root window. You call pack() to put the Canvas into a geometry
manager (AKA a layout).
The next step is to convert the Pillow Image into a Tkinter-friendly object using ImageTk.PhotoImage().
Then you can add that new object to your Canvas using the create_image() method. The image is
positioned relative to the (x, y) coordinates that you pass in as the first two arguments. The anchor
tells tells Tkinter how to position the image relative to their context with NW being the top left
position.
The last line of code in the main() function starts Tkinter’s event loop and displays the GUI to the
user.
When you run your code, your GUI will look like this:
Chapter 12 - Pillow Integration with GUI Toolkits 321
That looks pretty similar to the other examples you have seen in this chapter.
Now let’s move on and learn if Pillow supports wxPython!
wxPython
The wxPython GUI toolkit’s wx.Image widget supports most standard image formats out of the box.
However, wxPython includes Pillow as a dependency because the wx.lib.agw uses Pillow in some
of its custom widgets.
Pillow does not have built-in support for converting from a Pillow image into a wxPython native
format. However, you can work around this by converting the Image object into bytes.
To see how this works, create a new file named wx_pillow_demo.py and add this code to it:
Chapter 12 - Pillow Integration with GUI Toolkits 322
1 # wx_pillow_demo.py
2
3 import wx
4 from PIL import Image
5
6
7 class ImagePanel(wx.Panel):
8 def __init__(self, parent, image_size):
9 super().__init__(parent)
10
11 pil_img = Image.open("pink_flower.jpg")
12 width, height = pil_img.size
13 bitmap = wx.BitmapFromBuffer(width, height, pil_img.tobytes())
14
15 self.image_ctrl = wx.StaticBitmap(self, bitmap=wx.Bitmap(bitmap))
16
17 main_sizer = wx.BoxSizer(wx.VERTICAL)
18 main_sizer.Add(self.image_ctrl, 0, wx.ALL, 5)
19
20 self.SetSizer(main_sizer)
21 main_sizer.Fit(parent)
22 self.Layout()
23
24
25 class MainFrame(wx.Frame):
26 def __init__(self):
27 super().__init__(None, title="wxPython Image Viewer")
28 panel = ImagePanel(self, image_size=(240, 240))
29 self.Show()
30
31
32 if __name__ == "__main__":
33 app = wx.App(redirect=False)
34 frame = MainFrame()
35 app.MainLoop()
The wxPython toolkit uses classes to work with its widgets. In this case, you subclass wx.Panel,
which is a widget used to hold other widgets. In that class, you open up the flower image using
Pillow as usual. Then you extract the width and height of the image and convert the image itself into
bytes. You pass these values to wx.BitmapFromBuffer, which is taking the Pillow image in memory
and converting it into an object that wxPython understands.
Next, you create a wx.StaticBitmap() object that takes in the bitmap you created. This widget goes
Chapter 12 - Pillow Integration with GUI Toolkits 323
into a wx.BoxSizer, which is a geometry manager / layout that manages the layout and resizing of
the widgets that it contains.
The last few lines of the panel subclass will set a sizer for the class, fit the sizer to the parent widget
and call Layout(), which redraws the image.
The second subclass is for wx.Frame, which is the window-like object that holds the panel. You have
to call Show() to make the frame visible to the user.
The last few lines of code instantiate the wx.App() object, which is needed to manage the event as
well as the frame subclass you created.
When you run this code, your GUI will look like this:
wxPython has many widgets built-in to it and includes a wonderful demo package. Check it out if
you get a chance.
Wrapping Up
Pillow has built-in integration for Tkinter and PyQt via the ImageTk and ImageQt modules,
respectively. You can also load Pillow images into other GUI toolkits with a little work.
Chapter 12 - Pillow Integration with GUI Toolkits 324
In this chapter, you learned how to integrate Pillow with the following toolkits:
• Kivy
• PySimpleGUI
• PyQt
• Tkinter
• wxPython
All of these toolkits are useful in their way. One of them may “fit your brain” best. It’s always a good
idea to try out a couple of different toolkits to see which one makes the most sense for you. Also, be
mindful of each toolkit’s licensing restrictions or lack thereof. That can matter a lot depending on
what you are planning to do with your application.
Chapter 13 - Alternatives to Pillow
There are a lot of different packages you can install to enhance Python. But there aren’t a lot of
general-purpose image processing packages. Pillow is the only one that is so comprehensive and
full-featured for general image processing tasks.
However, there are other packages that you can do image processing with. In this chapter, you will
learn a little about each of the following:
• NumPy
• OpenCV
• Wand (ImageMagick)
The first three libraries in this list are often used by data scientists for machine learning and related
activities. Wand is the only package that is more general-purpose in use, although it doesn’t have
all the same features as Pillow.
You will get started by learning about NumPy first!
NumPy
NumPy is a scientific computing library for Python. It is very powerful and used with or by many
other scientific Python packages. Pillow integrates well with NumPy so that you can use NumPy to
speed up some of the pixel-by-pixel operations that Pillow is too slow for.
You can use conda or pip to install NumPy. Here is how you would install with pip:
For more information about NumPy, you can go to their website³⁷. When it comes to importing
NumPy, it is convention to do the import like this:
1 import numpy as np
Now that you have NumPy installed, let’s look at how you can use NumPy with Pillow.
³⁷https://numpy.org/
Chapter 13 - Alternatives to Pillow 326
Concatenating Images
NumPy isn’t so much a replacement for Pillow as it is a way to enhance Pillow’s capabilities. You
can use NumPy to do some of the things that Pillow does natively. For the examples in this section,
you will use this photo of the author:
To get a feel for how you might use NumPy with Pillow, you will create a new Python program
that concatenates several images together. Open up your Python editor and create a new file named
concatenating.py. Then enter this code in it:
1 # concatenating.py
2
3 import numpy as np
4 from PIL import Image
5
6
7 def concatenate(input_image_path, output_path):
8 image = np.array(Image.open(input_image_path))
9
Chapter 13 - Alternatives to Pillow 327
10 red = image.copy()
11 red[:, :, (1, 2)] = 0
12
13 green = image.copy()
14 green[:, :, (0, 2)] = 0
15
16 blue = image.copy()
17 blue[:, :, (0, 1)] = 0
18
19 rgb = np.concatenate((red, green, blue), axis=1)
20 output = Image.fromarray(rgb)
21 output.save(output_path)
22
23 if __name__ == "__main__":
24 concatenate("author.jpg", "stacked.jpg")
This code will open up the image using Pillow. However, rather than saving that image off as an
Image object, you pass that object into a Numpy array(). Then you create three copies of the array
and use some matrix math to zero out the other color channels. For example, for red, you zero out
the green and blue channels, leaving the red channel alone.
When you do this, it will create three tinted versions of the original image. You will now have a red,
green, and blue version of the photo. Then you use NumPy to concatenate the three images together
into one.
To save this new image, you use Pillow’s Image.fromarray() method to transform the NumPy array
back into a Pillow Image object.
After running this code, you will see the following result:
do even more when you combine NumPy with other scientific Python packages, such as SciPy or
Pandas.
Now let’s move on and take a look at OpenCV.
OpenCV
OpenCV is a computer vision package written in C++. There are Python bindings for OpenCV called
opencv-python that you can install with pip. Computer vision is a type of machine learning where
you “teach” a computer how to understand images or video. You train a computer using sets of
images. The process is more complicated than this book is going to cover.
If you’d like to know about Computer Vision, see the following resources:
• OpenCV Website³⁸
• Wikipedia³⁹
• PyImageSearch⁴⁰
The Python package for OpenCV is not included with Python. You can install it with pip though:
If you have any problems installing this package, you should visit the OpenCV website or PyImage-
Search as they both cover installing OpenCV and common problems that you might encounter.
³⁸https://opencv.org/
³⁹https://en.wikipedia.org/wiki/Computer_vision
⁴⁰https://www.pyimagesearch.com/
Chapter 13 - Alternatives to Pillow 329
1 # face_finder.py
2
3 import cv2
4 import os
5
6
7 def find_faces(image_path):
8 image = cv2.imread(image_path)
9
10 # Make a copy to prevent us from modifying the original
11 color_img = image.copy()
12
13 filename = os.path.basename(image_path)
14
15 # OpenCV works best with gray images
16 gray_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2GRAY)
17
18 # Use OpenCV's built-in Haar classifier
19 haar_classifier = cv2.CascadeClassifier("haarcascade_frontalface_alt.xml")
20
21 faces = haar_classifier.detectMultiScale(
22 gray_img, scaleFactor=1.1, minNeighbors=5,
23 )
24 print("Number of faces found: {faces}".format(faces=len(faces)))
25
26 for (x, y, width, height) in faces:
27 cv2.rectangle(
28 color_img,
29 (x, y), (x + width, y + height),
30 (0, 255, 0), 2,
31 )
32
33 # Show the faces found
34 cv2.imshow(filename, color_img)
35 cv2.waitKey(0)
36 cv2.destroyAllWindows()
37
38
39 if __name__ == "__main__":
40 find_faces("author.jpg")
The first thing to notice here is the imports. The OpenCV bindings are called cv2 in Python. Then
you create a function, find_faces(), that accepts a path to an image file. You use OpenCV’s imread()
Chapter 13 - Alternatives to Pillow 330
function to read the image file and then you create a copy of it to prevent you from accidentally
modifying the original image.
Next, you convert the image to grayscale. You will find that computer vision almost always works
better with gray than it does in color or at least that is the case with OpenCV.
The next step is to load up the Haar classifier using OpenCV’s XML file. This XML file is included
in the book’s GitHub repository⁴¹. You can also find them in your Python installation’s site-
packages/cv2/data folder.
Haar works by looking at a series of positive and negative images. Basically someone went and
tagged the features in a bunch of photos as either relevant or not and then ran it through a machine
learning algorithm or a neural network. Haar looks at edge, line, and four-rectangle features. There’s
a good explanation on the OpenCV site.
Now you can attempt to find faces in your image using the classifier object’s detectMultiScale()
method. You print out the number of faces that you found if any.
The classifier object returns an iterator of tuples. Each tuple contains the x/y coordinates of the face
it found as well as the width and height of the face. You use this information to draw a rectangle
around the face that was found using OpenCV’s rectangle method.
Here is the result of running this code:
⁴¹https://github.com/driscollis/image_processing_with_python
Chapter 13 - Alternatives to Pillow 331
OpenCV also has a Haar Cascade eye XML file for finding the eyes in photos. If you do a lot of
photography, you probably know that when you do portraiture, you want to try to focus on the
eyes. Some cameras even have an eye autofocus capability.
You can use this extra XML file to refine OpenCV’s search so that it looks only for eyes in the
photograph instead of the face. Take the code you have and create a new file named eye_finder.py.
Then put this code into it:
Chapter 13 - Alternatives to Pillow 332
1 # eye_finder.py
2
3 import cv2
4 import os
5
6
7 def find_eyes(image_path):
8 image = cv2.imread(image_path)
9
10 # Make a copy to prevent us from modifying the original
11 color_img = image.copy()
12
13 filename = os.path.basename(image_path)
14
15 # OpenCV works best with gray images
16 gray_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2GRAY)
17
18 # Use OpenCV's built-in Haar classifier
19 haar_classifier = cv2.CascadeClassifier("haarcascade_frontalface_alt.xml")
20 eye_cascade = cv2.CascadeClassifier("haarcascade_eye.xml")
21
22 faces = haar_classifier.detectMultiScale(
23 gray_img, scaleFactor=1.1, minNeighbors=5,
24 )
25 print("Number of faces found: {faces}".format(faces=len(faces)))
26
27 for (x, y, width, height) in faces:
28 cv2.rectangle(
29 color_img,
30 (x, y), (x + width, y + height),
31 (0, 255, 0), 2,
32 )
33 roi_gray = gray_img[y : y + height, x : x + width]
34 roi_color = color_img[y : y + height, x : x + width]
35 eyes = eye_cascade.detectMultiScale(roi_gray)
36 for (ex, ey, ew, eh) in eyes:
37 cv2.rectangle(
38 roi_color,
39 (ex, ey), (ex + ew, ey + eh),
40 (0, 255, 0), 2,
41 )
42
43 # Show the faces / eyes found
Chapter 13 - Alternatives to Pillow 333
44 cv2.imshow(filename, color_img)
45 cv2.waitKey(0)
46 cv2.destroyAllWindows()
47
48
49 if __name__ == "__main__":
50 find_eyes("author.jpg")
Here you add a second cascade classifier object. This time around, you use OpenCV’s built-in
haarcascade_eye.xml file. The other change is in your loop where you loop over the faces found.
Here you attempt to find the eyes and loop over them while drawing rectangles around them.
When you run this code, your output will look like this:
Chapter 13 - Alternatives to Pillow 334
You can use OpenCV to do much more than finding facial features. You can use it to figure out
gender or age, count the number of objects in an image, and much, much more. Check out the
PyImageSearch website for all kinds of different examples.
Now let’s learn about using Wand!
You have to have ImageMagick installed too. Depending on which operating system you are using,
you may need to set up an environment variable as well. See the documentation⁴³ for Wand if you
have any issues getting it installed or configured.
Wand can do many different image processing tasks. In the next few sections, you will see how
capable it is. You will start by learning about Wand’s image effects!
• Blur
• Despeckle
• Edge
• Emboss
• Kuwahara
• Shade
• Sharpen
• Spread
Some of these effects are present in Pillow and some are not. For example, Pillow does not have
Despeckle or Kuwahara.
To see how you can use these effects, you will use this ducklings photo from a previous chapter:
⁴²https://imagemagick.org/index.php
⁴³https://docs.wand-py.org
Chapter 13 - Alternatives to Pillow 336
You will try out Wand’s edge() method. Create a new Python file and name it wand_edger.py. Then
enter the following code:
1 # wand_edger.py
2
3 from wand.image import Image
4
5
6 def edge(input_image_path, output_path):
7 with Image(filename=input_image_path) as img:
8 img.transform_colorspace("gray")
9 img.edge(radius=3)
10 img.save(filename=output_path)
11
12 if __name__ == "__main__":
13 edge("ducklings.jpg", "edged.jpg")
The first new item here is the import: from wand.image import Image. The Image class is your
primary method of working with photos in Wand. Next, you create an edge() function and open up
Chapter 13 - Alternatives to Pillow 337
the duckling’s photo. Then you change the image to grayscale. Then you apply edge(), which takes
in a radius argument. The radius is an aperture-like setting.
When you run this code, the output will look like this:
You should experiment with different values for radius. It changes the result.
Now let’s take a look at the special effects that Wand provides.
Special Effects
Wand supports quite a few other effects that they have dubbed “Special Effects”. Here is a list of
what is currently supported:
• Add Noise
• Blue Shift
• Charcoal
• Color Matrix
• Colorize
• FX
Chapter 13 - Alternatives to Pillow 338
• Implode
• Polaroid
• Sepia Tone
• Sketch
• Solarize
• Stereogram
• Swirl
• Tint
• Vignette
• Wave
• Wavelet Denoise
Some of these are fun or interesting. The documentation has before-and-after photos for all these
examples.
You will try using Vignette in this section using the photo of the author. Create a new file named
wand_vignette.py and add this code to it:
1 # wand_vignette.py
2
3 from wand.image import Image
4
5
6 def vignette(input_image_path, output_path):
7 with Image(filename=input_image_path) as img:
8 img.vignette(x=10, y=10)
9 img.save(filename=output_path)
10
11
12 if __name__ == "__main__":
13 vignette("author.jpg", "vignette.jpg")
In this example, you call vignette(). It takes several different arguments, but you only supply x and
y. These arguments control the amount of edging that goes around the image that you are adding a
vignette to.
When you run this code, you will get the following output:
Chapter 13 - Alternatives to Pillow 339
That looks nice. This is a fun way to make your photos look unique and classy. Give it a try with
some of your photos!
Now you are ready to learn how to crop with Wand.
1 # wand_crop.py
2
3 from wand.image import Image
4
5
6 def crop(input_image_path, output_path, left, top, width, height):
7 with Image(filename=input_image_path) as img:
8 img.crop(left, top, width=width, height=height)
9 img.save(filename=output_path)
10
11
12 if __name__ == "__main__":
13 crop("ducklings.jpg", "cropped.jpg", 100, 800, 800, 800)
For this example, you supply left, top, width, and height. When you run this code, the photo will
be cropped to look like this:
You can experiment with different values to see how it affects the crop.
Wand can do much more than what is demonstrated in this section. It can do most of the same things
Chapter 13 - Alternatives to Pillow 341
as Pillow and more. Pillow’s main benefit over Wand is that Pillow is written in Python and doesn’t
require an external binary like Wand does (i.e., ImageMagick).
Wrapping Up
Pillow isn’t the only Python package you can use for image processing. But Pillow is one of the most
comprehensive. In this chapter, you learned about three alternatives:
• NumPy
• OpenCV
• Wand (ImageMagick)
In addition to these packages, there are also scikit-image, mahotas, SciPy, and more. Most of
these other packages focus on scientific image manipulation, machine learning, or computer vision
applications.
NumPy is a great way to enhance your use of Pillow. You can use it with Pillow to make Pillow
faster. Pillow is also compatible with the OpenCV package, so you could use it there as well.
You should check out these other packages. They may be useful to you someday.
Chapter 14 - Batch Processing
Batch processing is a term used to describe working on multiple items. In the context of this book,
batch processing would be processing multiple photos either in serial or in parallel. You will learn
how to apply a process to a series of images using Pillow.
Specifically, you will learn the following:
You will start by learning how to make your application work via the command line!
To get started, open up your Python editor and create a new file named image_resizer.py. Then
enter the following code:
Chapter 14 - Batch Processing 343
1 # image_resizer_cli.py
2
3 import argparse
4 import glob
5 import os
6 import time
7
8 from PIL import Image
9
10
11 def get_image_paths(search_path, recursive):
12 if "/" != search_path[-1]:
13 search_path += "/"
14 if recursive:
15 search_path += "**/"
16 search_path += "*.png"
17 image_paths = glob.glob(search_path, recursive=recursive)
18 return image_paths
19
20
21 def resize_images(image_paths, width, height, output_dir):
22 start = time.time()
23 if width is None:
24 width = height
25 if height is None:
26 height = width
27
28 if not os.path.exists(output_dir):
29 try:
30 os.makedirs(output_dir)
31 except OSError:
32 print(f"Error creating {output_dir}")
33 return
34
35 images_converted = 0
36 for image_path in image_paths:
37 pil_image = Image.open(image_path)
38 im_w, im_h = pil_image.size
39 image_name = os.path.basename(image_path)
40 output = os.path.join(output_dir, image_name)
41 if height < im_h or width < im_w:
42 # convert image and inform user
43 pil_image.thumbnail((width, height), Image.ANTIALIAS)
Chapter 14 - Batch Processing 344
87
88 if input_dir == output_dir:
89 print("The output folder cannot be the same as the input")
90 return
91 else:
92 if validate_directory(input_dir):
93 image_paths = get_image_paths(input_dir, args.recursive)
94 resize_images(image_paths, args.width, args.height,
95 output_dir)
96
97
98 if __name__ == "__main__":
99 main()
That’s a bit overwhelming! To help you understand the code easier, you will go over it piece by piece.
Here’s the import section to start you off:
1 # image_resizer_cli.py
2
3 import argparse
4 import glob
5 import os
6 import time
7
8 from PIL import Image
These are the various modules you need to create an image resizer program. You will use argparse
to parse arguments that are passed on the command line. The glob module is great for searching a
folder for a certain file type. The os module will be used for verifying paths are real.
The time module is used to time your program and see how fast it is. Finally, you’ll use Pillow to
resize the images.
Now you’re ready to learn how to search a path for image files:
Chapter 14 - Batch Processing 346
This function takes in two arguments: path and recursive. The path is the input folder that you want
to search for images within. The recursive argument tells Python’s glob module how to process
a /**/ directory – False means only matching files in subfolders will be returned, while True will
include matching files in the ** directory itself. Try practicing with glob directly to get a better feel
for how it works.
If glob doesn’t find any images, it will return an empty list. Otherwise, it will return a list of strings
where the strings are fully qualified paths to the images that it found.
Note that this code is only looking for PNG files. You will fix this in the GUI version of the code
later on in this chapter.
The next function resizes the images that you found:
The resize_images() function does a lot of the heavy-lifting of your application. First, you grab
the current timestamp in seconds. Next, you check if the user specifies only the width or only the
height, then you set them to be equal. You also test that the output_dir exists and print a message
out if it does not.
The for loop is where you loop over the images that glob found. If the height or width that you
specified is smaller than the actual height or width, you resize the image using Pillow’s thumbnail()
method. Then you save the resized image to the output folder, using the original file name.
If the specified height or width is larger than the actual image’s height or width, you skip resizing it.
The reason for this is that enlarging a photo usually gives you an image of low quality. Either way,
the image is then saved to the output folder.
Your code also keeps track of how many images are converted. At the end of the function, you grab
the time in seconds again and then print out how long it took for the conversion process to run as
well as where the files were saved.
There is a potential bug here. Because all the images are output to a single folder, if you used the
recursive option, there is the potential that some of the photos may have the same name. They
could then overwrite each other when they are resized. You can take on the challenge of fixing that
yourself.
The next function is a validator:
1 def validate_directory(path):
2 if not os.path.isdir(path):
3 print(f"{path} is not a directory")
4 return False
5 return True
This function will validate that the passed-in path is a directory. It returns True or False. Technically,
you could shorten this to the following:
Chapter 14 - Batch Processing 348
However, this doesn’t print out a useful message. You may want to add some other validation to this
function that could make it more complex in the future as well.
Now you’re ready for the main() function:
1 def main():
2 parser = argparse.ArgumentParser("Image Resizer")
3 parser.add_argument("-i", "--infolder", help="Input folder",
4 required=True, dest="input_dir")
5 parser.add_argument("-r", "--recursive",
6 help="Search sub-folders recursively",
7 dest="recursive", default=False,
8 action="store_true")
9 parser.add_argument("--height",
10 help="The new height the image should be",
11 dest="height", type=int)
12 parser.add_argument("--width",
13 help="The new width the image should be",
14 dest="width", type=int)
15 parser.add_argument("-o", "--out", help="Output folder",
16 required=True, dest="output_dir")
17 args = parser.parse_args()
18
19 if args.width is None and args.height is None:
20 print("You need to specify width or height or both")
21 return
22
23 input_dir = args.input_dir
24 output_dir = args.output_dir
25
26 if input_dir == output_dir:
27 print("The output folder cannot be the same as the input")
28 return
29 else:
30 if validate_directory(input_dir):
31 image_paths = get_image_paths(input_dir, args.recursive)
32 resize_images(image_paths, args.width, args.height,
33 output_dir)
34
35
Chapter 14 - Batch Processing 349
36 if __name__ == "__main__":
37 main()
Here you create an ArgumentParser() object. Then you add in the arguments that your application
will support. You add a conditional statement to verify that either the width or height was set. If not,
you display a message to the user and exit.
Assuming that everything is supplied that is required, you verify that the user isn’t trying to
overwrite the input folder. If they are not, then you search the input folder for PNG files and then
resize them.
Python’s argparse automatically adds a -h option. So when you run your program, you can get
some useful information about how it works:
1 $ python3 image_resizer_cli.py -h
2 usage: Image Resizer [-h] -i INPUT_DIR [-r] [--height HEIGHT] [--width WIDTH]
3 -o OUTPUT_DIR
4
5 optional arguments:
6 -h, --help show this help message and exit
7 -i INPUT_DIR, --infolder INPUT_DIR
8 Input folder
9 -r, --recursive Search sub-folders recursively
10 --height HEIGHT The new height the image should be
11 --width WIDTH The new width the image should be
12 -o OUTPUT_DIR, --out OUTPUT_DIR
13 Output folder
This application was tested on a folder that had 156 PNG images in it. It was able to resize 155 of
those images in 22.5 seconds. One of the images was too small, so it wasn’t resized.
While being able to resize over 150 images in 22.5 seconds is great, you can do better. You will find
out how in the next section!
Note: A thread pool is a design pattern that maintains multiple threads that are waiting for
tasks to be added to them.
The concurrent.futures library is a handy built-in library that wraps Python’s threading and
multiprocessing modules so that you can use the two almost interchangeably.
To get started, you’ll be using the code from the previous section and modifying it to use threads.
Open up a new file and name it image_resizer_cli_threads.py.
Then enter the following code:
1 # image_resizer_cli_threads.py
2
3 import argparse
4 import glob
5 import os
6 import time
7
8 from concurrent.futures import ThreadPoolExecutor
9 from concurrent.futures import as_completed
10 from PIL import Image
11
12
13 def get_image_paths(search_path, recursive):
14 if "/" != search_path[-1]:
15 search_path += "/"
16 if recursive:
17 search_path += "**/"
18 search_path += "*.png"
19 image_paths = glob.glob(search_path, recursive=recursive)
20 return image_paths
21
22
23 def resize_image(image_path, width, height, output_dir):
24 pil_image = Image.open(image_path)
25 im_w, im_h = pil_image.size
26 image_name = os.path.basename(image_path)
27 output = os.path.join(output_dir, image_name)
28 if height < im_h or width < im_w:
29 pil_image.thumbnail((width, height), Image.ANTIALIAS)
30 pil_image.save(output)
31 return f"{image_path} converted to {output}"
32 else:
33 pil_image.save(output)
34 return f"{image_path} copied to {output}."
Chapter 14 - Batch Processing 351
35
36
37 def resize_images(image_paths, width, height, output_dir):
38 start = time.time()
39 if width is None:
40 width = height
41 if height is None:
42 height = width
43
44 if not os.path.exists(output_dir):
45 try:
46 os.makedirs(output_dir)
47 except OSError:
48 print(f"Error creating {output_dir}")
49 return
50
51 images_converted = 0
52 with ThreadPoolExecutor(max_workers=5) as executor:
53 futures = [
54 executor.submit(
55 resize_image, image_path, width, height, output_dir,
56 )
57 for image_path in image_paths
58 ]
59 for future in as_completed(futures):
60 result = future.result()
61 if "converted" in result:
62 images_converted += 1
63 print(future.result())
64
65 end = time.time()
66 print(f"Converted {images_converted} image(s) in {end-start} seconds.")
67 print(f"Output folder is: {output_dir}")
68
69
70 def validate_directory(path):
71 if not os.path.isdir(path):
72 print(f"{path} is not a directory")
73 return False
74 return True
75
76
77 def main():
Chapter 14 - Batch Processing 352
The first change is these new imports that were added to the top of the file in the import section.
These give you the pieces you’ll need to create a thread pool.
The next change is this new function:
The resize_image() function is broken out of the resize_images() function. You will use resize_-
image() in each of your threads. It takes in the path to the image that you want to resize; its width
and height; and the output directory you want to save to. It returns strings that will be printed by
the thread pool supervisor.
The only other change was to the resize_images() function itself:
17 futures = [
18 executor.submit(
19 resize_image, image_path, width, height, output_dir,
20 )
21 for image_path in image_paths
22 ]
23 for future in as_completed(futures):
24 result = future.result()
25 if "converted" in result:
26 images_converted += 1
27 print(result)
28
29 end = time.time()
30 print(f"Converted {images_converted} image(s) in {end-start} seconds.")
31 print(f"Output folder is: {output_dir}")
The first half of this code is the same as before. So you will focus on only the lines that have changed:
Here you create a ThreadPoolExecutor() object and tell it to manage five threads. Then you use a list
comprehension to submit all the images as jobs to the executor, which is the thread pool supervisor.
These are often called futures.
The executor will run up to 5 threads at once. As they complete, it will print out the result() of the
threaded call.
When this code was run against the same folder as the non-threaded code, it was able to resize 155
files in 6.25 seconds! Most computers can run more than five threads simultaneously, so feel free to
modify the code to change the pool size and see how that affects your run time.
The next step in the process is to make your code modular.
Chapter 14 - Batch Processing 355
1 # controller.py
2
3 import glob
4 import os
5 import time
6
7 from concurrent.futures import ThreadPoolExecutor
8 from concurrent.futures import as_completed
9 from PIL import Image
10
11
12 def get_image_paths(search_path, recursive, file_format="png"):
13 if "/" != search_path[-1]:
14 search_path += "/"
15 if recursive:
16 search_path += "**/"
17 search_path += f"*.{file_format}"
18 image_paths = glob.glob(search_path, recursive=recursive)
19 return image_paths
20
21
22 def resize_image(image_path, width, height, output_dir):
23 pil_image = Image.open(image_path)
24 im_w, im_h = pil_image.size
25 image_name = os.path.basename(image_path)
26 output = os.path.join(output_dir, image_name)
27 if height < im_h or width < im_w:
28 pil_image.thumbnail((width, height), Image.ANTIALIAS)
Chapter 14 - Batch Processing 356
29 pil_image.save(output)
30 return f"{image_path} converted to {output}"
31 else:
32 pil_image.save(output)
33 return f"{image_path} copied to {output}."
34
35
36 def resize_images(image_paths, width, height, output_dir):
37 start = time.time()
38 if width is None:
39 width = height
40 if height is None:
41 height = width
42
43 if not os.path.exists(output_dir):
44 try:
45 os.makedirs(output_dir)
46 except OSError:
47 print(f"Error creating {output_dir}")
48 return
49
50 images_converted = 0
51 with ThreadPoolExecutor(max_workers=5) as executor:
52 futures = [
53 executor.submit(
54 resize_image, image_path, width, height, output_dir,
55 )
56 for image_path in image_paths
57 ]
58 for future in as_completed(futures):
59 result = future.result()
60 if "converted" in result:
61 images_converted += 1
62 print(future.result())
63
64 end = time.time()
65 print(f"Converted {images_converted} image(s) in {end-start} seconds.")
66 print(f"Output folder is: {output_dir}")
67
68
69 def validate_directory(path):
70 if not os.path.isdir(path):
71 print(f"{path} is not a directory")
Chapter 14 - Batch Processing 357
72 return False
73 return True
You are already familiar with all of these functions. They are all from the threaded version of the
CLI program you made earlier. There is one small change to get_image_paths() in that it now lets
you pass in the file_format that you want to search for. Otherwise, all the code is the same.
To see how you can use this code in your CLI application, create a new file named image_resizer_-
cli_modular.py and add this code:
1 # image_resizer_cli_modular.py
2
3 import argparse
4 import controller
5
6
7 def main():
8 parser = argparse.ArgumentParser("Image Resizer")
9 parser.add_argument("-i", "--infolder", help="Input folder",
10 required=True, dest="input_dir")
11 parser.add_argument("-r", "--recursive",
12 help="Search sub-folders recursively",
13 dest="recursive", default=False,
14 action="store_true")
15 parser.add_argument("--height",
16 help="The new height the image should be",
17 dest="height", type=int)
18 parser.add_argument("--width",
19 help="The new width the image should be",
20 dest="width", type=int)
21 parser.add_argument("-o", "--out", help="Output folder",
22 required=True, dest="output_dir")
23 args = parser.parse_args()
24
25 if args.width is None and args.height is None:
26 print("You need to specify width or height or both")
27 return
28
29 input_dir = args.input_dir
30 output_dir = args.output_dir
31
32 if input_dir == output_dir:
33 print("The output folder cannot be the same as the input")
Chapter 14 - Batch Processing 358
34 return
35 else:
36 if controller.validate_directory(input_dir):
37 image_paths = controller.get_image_paths(input_dir, args.recursive)
38 controller.resize_images(
39 image_paths, args.width, args.height, output_dir,
40 )
41
42
43 if __name__ == "__main__":
44 main()
This imports argparse and the controller you created. That’s it! Now you only have a main()
function which needs only a couple of small changes at the end:
1 if controller.validate_directory(input_dir):
2 image_paths = controller.get_image_paths(input_dir, args.recursive)
3 controller.resize_images(
4 image_paths, args.width, args.height, output_dir,
5 )
Here you call the same functions as before, but you call them by prepending the function names
with controller.. Isn’t that nice? Now you have a way to reuse about half of your code.
Now you’re ready to try using the controller in a GUI!
Adding a GUI
Creating a GUI makes choosing your input and output directories so much easier. You don’t have
to worry so much about typographical errors using the GUI when it comes to choosing your input
and output paths.
When you are finished creating your GUI, it will look like this:
Chapter 14 - Batch Processing 359
Now it’s time to start coding. Open up your editor and create a new file named image_resizer_-
gui.py. Then enter this code:
1 # image_resizer_gui.py
2
3 import controller
4 import PySimpleGUI as sg
5
6
7 def create_row(label, key):
8 return [
9 sg.Text(label),
10 sg.Input(size=(25, 1), key=key, readonly=True),
11 sg.FolderBrowse(),
12 ]
13
14
15 def resize(values):
16 input_folder = values["-INPUT_FOLDER-"]
17 output_folder = values["-OUTPUT_FOLDER-"]
18 width = values['-WIDTH-'] if values['-WIDTH-'] else None
19 height = values['-HEIGHT-'] if values['-HEIGHT-'] else None
Chapter 14 - Batch Processing 360
20
21 verified = verify(input_folder, output_folder, width, height)
22 if not verified:
23 return
24 if width is not None:
25 width = int(width)
26 if height is not None:
27 height = int(height)
28
29 image_paths = controller.get_image_paths(
30 input_folder,
31 values["-RECURSIVE-"],
32 values["-FORMAT-"])
33 if len(image_paths) < 1:
34 sg.popup("No images found")
35 return
36 controller.resize_images(image_paths, width, height, output_folder)
37
38
39 def verify(input_folder, output_folder, width, height):
40 if not width and not height:
41 sg.popup("Width or height has to be set")
42 return False
43 if not input_folder:
44 sg.popup("Input folder not set")
45 return False
46 if not output_folder:
47 sg.popup("Output folder not set")
48 return False
49 if input_folder == output_folder:
50 sg.popup("input folder cannot be the same as output")
51 return False
52 return True
53
54
55 def main():
56 layout = [
57 create_row("Input Image Folder:", "-INPUT_FOLDER-"),
58 [sg.Checkbox("Recursive Search", key="-RECURSIVE-",
59 enable_events=True),
60 sg.Text("Format:"),
61 sg.Combo(values=["jpg","png"], default_value="png",
62 readonly=True, key="-FORMAT-", enable_events=True)],
Chapter 14 - Batch Processing 361
Once again you have a large piece of code. As usual, you will now go over this code one chunk at a
time!
1 # image_resizer_gui.py
2
3 import controller
4 import PySimpleGUI as sg
There are only two imports in this code. The first is your controller, which contains all your resizing
logic. The second is PySimpleGUI, the GUI toolkit you are using for your user interface.
The first function to look at is create_row():
Chapter 14 - Batch Processing 362
You have seen this function before. It is used to create three Elements horizontally in your user
interface. It will add a label (sg.Text), a read-only text box (sg.Input), and a browse button
(sg.FolderBrowse) that lets the user choose which folder they want to open.
The next function in your application is named resize():
1 def resize(values):
2 input_folder = values["-INPUT_FOLDER-"]
3 output_folder = values["-OUTPUT_FOLDER-"]
4 width = values['-WIDTH-'] if values['-WIDTH-'] else None
5 height = values['-HEIGHT-'] if values['-HEIGHT-'] else None
6
7 verified = verify(input_folder, output_folder, width, height)
8 if not verified:
9 return
10 if width is not None:
11 width = int(width)
12 if height is not None:
13 height = int(height)
14
15 image_paths = controller.get_image_paths(
16 input_folder,
17 values["-RECURSIVE-"],
18 values["-FORMAT-"])
19 if len(image_paths) < 1:
20 sg.popup("No images found")
21 return
22 controller.resize_images(image_paths, width, height, output_folder)
This function extracts the data from the GUI to pass along to the controller. It verifies that
everything that is required to resize an image is present by calling your verify() function. It also
converts the width and height to an integer if they are not None.
You get the image list as you did before, except that in the GUI you let the user choose a file type to
search for (JPG or PNG). If the search of the input folder returns no results, you display a message
to the user.
Otherwise, you call controller.resize_images().
Chapter 14 - Batch Processing 363
The verify() function is used to verify that your user has supplied a width or height as well as an
input and output folder. It also verifies that the user hasn’t set the input and output paths to the
same location. If verify() passes all its checks, it returns True. Otherwise, it will return False.
The verify() function is called from the resize() function above.
The last function is your main() function:
1 def main():
2 layout = [
3 create_row("Input Image Folder:", "-INPUT_FOLDER-"),
4 [sg.Checkbox("Recursive Search", key="-RECURSIVE-",
5 enable_events=True),
6 sg.Text("Format:"),
7 sg.Combo(values=["jpg", "png"], default_value="png",
8 readonly=True, key="-FORMAT-", enable_events=True)],
9 create_row("Output Image Folder", "-OUTPUT_FOLDER-"),
10 [
11 sg.Text("Width"),
12 sg.Input(key="-WIDTH-", enable_events=True, size=(10, 5)),
13 sg.Text("Height"),
14 sg.Input(key="-HEIGHT-", enable_events=True, size=(10, 5))
15 ],
16 [sg.Output(size=(80, 3))],
17 [sg.Button("Resize")],
18 ]
19
20 window = sg.Window("Image Resizer", layout, size=(450, 250))
Chapter 14 - Batch Processing 364
The user interface for this GUI isn’t too complicated. This code creates all the Elements you need to
make up the entire interface. Note that it also calls create_row() in two places to add the Elements
for the input and output folders.
The sg.Output Element will redirect stdout and stderr to that Element. This lets you use Python’s
print() function to tell the user what is happening because it gets redirected to the output element.
Once your layout is complete, you pass it along to sg.Window which arranges the Elements in your
display and also lets you set the title for your application.
The last few lines of code are your event handler logic:
1 while True:
2 event, values = window.read()
3 if event == "Exit" or event == sg.WIN_CLOSED:
4 break
5 if event == "-WIDTH-" and values["-WIDTH-"]:
6 if not values["-WIDTH-"][-1].isdigit():
7 window["-WIDTH-"].update(values["-WIDTH-"][:-1])
8 elif event == "-HEIGHT-" and values["-HEIGHT-"]:
9 if not values['-HEIGHT-'][-1].isdigit():
10 window["-HEIGHT-"].update(values["-HEIGHT-"][:-1])
11 elif event == "Resize":
12 resize(values)
13
14 window.close()
15
16
17 if __name__ == "__main__":
18 main()
Here you check the events and respond to them accordingly. You care about the following events:
• "-WIDTH-"
• "-HEIGHT-"
• "Resize"
The width and height-related events are used to prevent the user from entering anything other than
integers in those fields. The “Resize” event is what triggers your code to attempt to resize the images.
It will call resize() which verifies that the user has entered everything needed to start the resizing
process.
At this point, you have a complete program. Give it a try and see how it works out for you. Then
you can start thinking about all the new features you can add. For example, wouldn’t it be fun if
you could select some effects to be applied instead of resizing?
Chapter 14 - Batch Processing 365
Wrapping Up
Creating applications with Python is fun. You can do so much with very few lines of code. The
Pillow package has all kinds of different functions that you can apply to your images.
In this chapter, you learned about the following:
You can take the concepts you learned in this chapter and apply them to any of the other functions
you learned about in this book. You could add watermarks, add filters, extract Exif data, and much,
much more.
The only limit is your imagination!
Afterword
Pillow is a great package. You can do so much with it and so many other Python packages depend
on Pillow. I personally learned a lot about Pillow’s capabilities while writing this book. It is my hope
that you have learned a lot and will be able to use Pillow effectively in your applications.
I had lots of help while writing this book. The Pillow team was kind enough to answer all my
questions. The PySimpleGUI team was also helpful in resolving any issues I ran into with my GUI
examples. Python developers continue to be some of the friendliest and most helpful developer
communities that I have been a part of.
My two technical reviewers have helped me polish this book and pushed me to make it the best that
I could.
Thank you for reading this book.
Mike