0% found this document useful (0 votes)
15 views

Pillow

The document is a book titled 'Pillow: Image Processing with Python' by Michael Driscoll, published on October 22, 2021. It covers various aspects of image processing using the Pillow library, including basics, color manipulation, image metadata, and filters. The book is designed for readers interested in learning about image processing techniques in Python.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
15 views

Pillow

The document is a book titled 'Pillow: Image Processing with Python' by Michael Driscoll, published on October 22, 2021. It covers various aspects of image processing using the Pillow library, including basics, color manipulation, image metadata, and filters. The book is designed for readers interested in learning about image processing techniques in Python.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 372

Pillow: Image Processing with Python

Michael Driscoll
This book is for sale at http://leanpub.com/pillow

This version was published on 2021-10-22

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.

© 2020 - 2021 Michael Driscoll


Contents

About the Technical Reviewers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1


Ethan Furman . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Alessia Marcolini . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

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 1 - Pillow Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7


Opening Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Saving Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Changing Compression During Saving . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Changing DPI During Saving . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Reading Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Creating Thumbnails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Creating an Image Viewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

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 3 - Getting Image Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49


Getting Exif Tag Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Getting GPS Exif Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Getting TIFF Tag Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Creating an Exif GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

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

Chapter 5 - Cropping, Rotating & Resizing Images . . . . . . . . . . . . . . . . . . . . . . . . . 105


How Coordinates Work in Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Cropping Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Rotating Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Mirroring Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Resizing Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Scaling Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
Creating an Image Rotator GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122

Chapter 6 - Enhancing Images with ImageEnhance . . . . . . . . . . . . . . . . . . . . . . . . . 124


Adjust Color Balance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
Adjust Image Contrast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Adjust Image Brightness . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Adjust Image Sharpness . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Enhancing Photos with a GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
CONTENTS

Chapter 7 - Combining Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144


Pasting an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
Tiling Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
Concatenating Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Watermarking an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
Compositing Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Blending Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
Creating a Watermark GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176

Chapter 8 - Drawing Shapes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177


Common Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Drawing Lines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Drawing Arcs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Drawing Chords . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Drawing Ellipses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
Drawing Pie Slices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
Drawing Polygons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
Drawing Rectangles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
Creating a Drawing GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206

Chapter 9 - Drawing Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207


Drawing Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Loading TrueType Fonts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Changing Text Color . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Drawing Multiple Lines of Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Aligning Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
Changing Text Opacity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Learning About Text Anchors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Creating a Text Drawing GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235

Chapter 10 - Channel Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237


ImageChop Aliases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
Adding Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
Using ImageChops.darker() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
Using ImageChops.lighter() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
Finding Differences in Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
Inverting Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
Using Soft Light on Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
Using Hard Light on Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
Overlay Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Creating a Blending GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
CONTENTS

Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266

Chapter 11 - The ImageOps Module . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267


Applying Automatic Contrast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Colorizing Photos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Padding an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
Adding a Border . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Removing a Border . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
Scaling an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Equalizing the Histogram . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
Sizing and Cropping an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Flipping an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Mirroring an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Inverting an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 290
Posterize an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292
Solarize an Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Transpose Image Using Exif Orientation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
Creating an ImageOps GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307

Chapter 12 - Pillow Integration with GUI Toolkits . . . . . . . . . . . . . . . . . . . . . . . . . . 308


Kivy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
PySimpleGUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312
PyQt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316
Tkinter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
wxPython . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323

Chapter 13 - Alternatives to Pillow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325


NumPy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
OpenCV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
Wand (ImageMagick Bindings) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334
Cropping with Wand . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341

Chapter 14 - Batch Processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342


Creating a Batch CLI Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
Running the Batch Application with Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
Modularizing Your Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Adding a GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365

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

Thank you all for your help!


Alex Clark, the creator of the Pillow fork, has also been encouraging. Thank you to everyone who
has been and who is currently a core developer of Pillow. You are amazing!
And to anyone that I am forgetting and to you, thank you too!
Thank you for reading this 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.

Who is this book for?


This book is targeted at intermediate level developers. The ideal person reading this book will know
the Python language already and understand how to install 3rd party packages.
Introduction 4

About the Author


Mike Driscoll has been programming with the Python language for more than a decade. When Mike
isn’t programming for work, he writes about Python on his blog¹ and contributes to Real Python.
He has worked with Packt Publishing and No Starch Press as a technical reviewer. Mike has also
written several books.
You can see a full listing of Mike’s books on his blog² too.

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!

When referring to code in a sentence you will see it in monospace.


Images will be marked with a chapter and image number for easy reference.
Other than that, there are no conventions!

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:

1 python3 -m pip install --upgrade pip


2 python3 -m pip install --upgrade Pillow

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

1 python3 -m pip install PySimpleGUI

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__);"

If this prints then you have Pillow installed!


To add PySimpleGUI, use the following command:

1 conda install -c conda-forge pysimplegui

Book Source Code


The book’s source code can be found on Github:

• 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:

[email protected]

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

Let’s get started by learning how to open an image!

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

Fig. 1-1: Displaying an Image with Pillow

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:

1 >>> from PIL import Image


2 >>> image = Image.open("flowers.jpg")
3 >>> dir(image)
4 ['_Image__transformer', '__array_interface__', '__class__', '__copy__',
5 '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__',
6 '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__',
7 '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
8 '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
9 '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__',
10 '__subclasshook__', '__weakref__', '_close_exclusive_fp_after_loading',
Chapter 1 - Pillow Basics 10

11 '_copy', '_crop', '_dump', '_ensure_mutable', '_exclusive_fp', '_exif',


12 '_expand', '_get_safe_box', '_getexif', '_getmp', '_min_frame', '_new',
13 '_open', '_repr_png_', '_seek_check', '_size', 'alpha_composite', 'app',
14 'applist', 'bits', 'category', 'close', 'convert', 'copy', 'crop',
15 'custom_mimetype', 'decoderconfig', 'decodermaxblock', 'draft',
16 'effect_spread', 'entropy', 'filename', 'filter', 'format',
17 'format_description', 'fp', 'frombytes', 'get_format_mimetype', 'getbands',
18 'getbbox', 'getchannel', 'getcolors', 'getdata', 'getexif', 'getextrema',
19 'getim', 'getpalette', 'getpixel', 'getprojection', 'height', 'histogram',
20 'huffman_ac', 'huffman_dc', 'icclist', 'im', 'info', 'layer', 'layers',
21 'load', 'load_djpeg', 'load_end', 'load_prepare', 'load_read', 'mode',
22 'palette', 'paste', 'point', 'putalpha', 'putdata', 'putpalette',
23 'putpixel', 'pyaccess', 'quantization', 'quantize', 'readonly', 'reduce',
24 'remap_palette', 'resize', 'rotate', 'save', 'seek', 'show', 'size',
25 'split', 'tell', 'thumbnail', 'tile', 'tobitmap', 'tobytes', 'toqimage',
26 'toqpixmap', 'transform', 'transpose', 'verify', 'width']

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:

1 >>> from PIL import Image


2 >>> image = Image.open("flowers.jpg")
3 >>> image.size
4 (2048, 1365)
5 >>> width, height = image.size
6 >>> width
7 2048
8 >>> height
9 1365
10 >>> image.filename
11 'flowers.jpg'
12 >>> image.format
13 'JPEG'
14 >>> image.format_description
15 'JPEG (ISO 10918)'

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

Changing Compression During Saving


Pillow supports saving files using different quality settings. This can help reduce file size. The
following photo is 7.7 MB:

Fig. 1-2: Bluish Flowers

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

10 image.save(output_file_path, quality=quality, optimize=True)


11
12 if __name__ == "__main__":
13 image_quality("blue_flowers.jpg",
14 "blue_flowers_compressed.jpg",
15 quality=95)

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!

Changing DPI During Saving


The bluish flowers in the previous section have a DPI of 240. The DPI is an acronym for dots-per-inch.
Most photos that you want to post on a website should be 72 dpi so that they can load fast.
You can change the DPI for a photo in Pillow when you are saving the file. Create a new file named
save_image_with_new_dpi.py and add this code to it:

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:

• Using a context manager


• Reading image data from memory
• Reading an image from a tar archive

Let’s get started!

Using a Context Manager


Pillow’s open() method can be used as a context manager. What that means is that it works with
Python’s with statement. To see that in action, create a new file called open_image_context.py and
add the following to it:

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.

Reading Image Data From Memory


Pillow can read images that you have loaded into memory. The Pillow documentation refers to this
as reading in binary data. You can make this work using Python’s io module. Go ahead and create
a file named open_image_from_memory.py and enter this code:
Chapter 1 - Pillow Basics 15

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.

Reading an Image From a Tar Archive


Pillow includes a TarIO module that you can use to load an image from a tar file. It takes in two
arguments:

• The tar file path


• The name of the image file within the 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:

• input_file_path - The file path you want to create a thumbnail from


• thumbnail_path - The file path to the thumbnail you are creating
• thumbnail_size - A tuple of integers that represents the thumbnail size.

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.

Creating an Image Viewer


Pillow lets you view photos using it’s show() method. This opens up your operating system’s default
image viewer. While this is handy, you can write your image viewer using Python. This allows you
to view images using your own code. You will also be able to add new features to your application
so you can apply some of the other image capabilities that Pillow comes with in your own code. In
effect, you will be creating a series of applications that mimic portions of a regular photo editor, but
written entirely in Python.
For this example, you will be using PySimpleGUI to make your image viewer. Before you get started,
you should know a little about graphical user interfaces. The first concept to understand is the term
Elements. An Element can be either a button, text control, or on-screen item that you can interact
with inside an application. Even text in an application is represented as a Element. Other GUI toolkits
use the term widgets instead of Elements.
The other major concept to understand is events. An event happens when the user presses a button,
hits a key on their keyboard, moves their mouse, or interacts in some other way with your application.
PySimpleGUI returns these events to you, and you can react to them or ignore them.
Standard GUI toolkits use an event loop to monitor these events, and will run appropriate code
when an event happens. With PySimpleGUI you have to write you own event loop, if one is needed
– for simple scripts displaying a form and getting user feed back once is sufficient, so a loop is not
necessary.
Now that you know the basics of how a GUI works, you can get started creating your own by making
a file named image_viewer.py and adding this code:
Chapter 1 - Pillow Basics 18

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:

• Text - A label Element


• Input - A text entry Element
• FileBrowse - A button that opens a file browser dialog
Chapter 1 - Pillow Basics 20

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

Fig. 1-3: Image Viewer without an image loaded

That is how the Image Viewer looks when an image is not loaded. If you load an image, it will look
like this:

Fig. 1-4: Image Viewer with an image loaded

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:

• Red - (255, 0, 0, 255)


• Green - (0, 255, 0, 255)
• Blue - (0, 0, 255, 255)
• White - (255, 255, 255, 255)
Chapter 2 - Colors 24

• Black - (0, 0, 0, 255)


• Gray - (128, 128, 128, 255)
• Yellow - (255, 255, 0, 255)

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!

Using Pillow to Get RGB Values


Pillow provides you with the tools you need to get colors without needing to memorize the
RGB values. You may use ImageColor.getcolor() to get the RGB values using a more human-
understandable name. This function takes in two arguments:

• color - The name of the color as a string


• mode - Which color mode to use, such as RGBA

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

1 >>> from PIL import ImageColor


2 >>> ImageColor.getcolor('#ff0000', 'RGBA')
3 (255, 0, 0, 255)
4 >>> ImageColor.getcolor('red', 'RGBA')
5 (255, 0, 0, 255)
6 >>> ImageColor.getrgb("hsv(0, 100%, 100%)")
7 (255, 0, 0)

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:

1 aliceblue = (240, 248, 255, 255)


2 antiquewhite = (250, 235, 215, 255)
3 aqua = (0, 255, 255, 255)

Now let’s find out how to get colors from images!

Getting Colors from Images


Pillow allows you to get a listing of all the colors in an image by using its getcolors() function
from the Image module. One use case for this functionality is to check for the existence of a specific
color in an image or to check if a color is not present.
Before you write some code, you need to find an image. You can use this image of a Cape Thick
Knee bird that is included with the book’s code on GitHub⁶:

Fig. 2-1: Cape Thick Knee

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

Fig. 2-2: Butterfly

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!

Changing Pixel Colors


The first step in learning how to change pixels is to figure out which pixels you want to change. Of
course, that requires you to have an image. To make things simpler, you can use Pillow to create a
blank image in memory and add some color to your image.
When you are working with an image, you need to keep in mind that the image’s pixels are addressed
using x and y-coordinates. These specify the pixel’s horizontal and vertical location, respectively, in
the image. The starting point is known as the origin and is defined as (0, 0), which is the top left
corner of the image. You will learn about pixels and coordinates in chapter 5.
You can get started by creating a new file named create_image.py and then add the following:
Chapter 2 - Colors 29

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

Fig. 2-2: Lines of Color

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!

Converting to Black and White


Back in the old days, all photographs were shot in black and white. Now you can take high-resolution
photos in full color with your mobile phone. However, creating black and white photos can be fun.
It can make a somewhat boring photo into something more interesting.
Pillow includes a convert() method in its Image module that you can use to convert photos to
grayscale as well as black and white. You will be using the following Monarch butterfly photo:
Chapter 2 - Colors 31

Fig. 2-3: Color Monarch Butterfly

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:

1 def convert(self, mode=None, matrix=None, dither=None, palette=WEB, colors=256):

The mode argument can be one of the following:

• L - use the ITU-R 601-2 luma transform to convert color to grayscale


• 1 - uses Floyd-Steinberg dither to approximate the original image luminosity levels
• P - palette mode
• RGB - converts to RGB
• CYMK - converts to CYMK

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

Fig. 2-4: Gray Scale Monarch Butterfly

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

Fig. 2-5: Black and White (No Dithering) Monarch Butterfly

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:

Fig. 2-6: Black and White Monarch Butterfly with Dithering

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

Creating 4-Color Photos


You can change a photo’s color depth by modifying the palette during the conversion process. If you
go back to the signature of the convert() method, you will see that the color depth is 256 by default.
You can change the color depth and apply a palette by using the P mode. To see how this works,
create a new Python file named create_4_color.py and add this code to it:

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

Fig. 2-7: Monarch with Color Depth of 4

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.

Creating a Sepia Photo


A lot of people like to apply a sepia filter or palette to their photos. You can do this with Pillow
too! You will be using the Image module’s putpalette() method. This method attaches a palette to
the image object. According to Pillow’s documentation, the image must be a “P”, “PA”, “L” or “LA”
image, and the palette sequence must contain 768 integer values, where each group of three values
represent the red, green, and blue values for the corresponding pixel index. Instead of an integer
sequence, you can use an 8-bit string.
Chapter 2 - Colors 39

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:

Fig. 2-8: Sepia Monarch

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

Creating an Image Converter GUI


One of the benefits of having a graphical user interface is that you can load an image up and apply
each of the image conversions you have created in this chapter in quick succession. You’ll be able to
see what kind of effect they are having almost in real-time. Not only that, but you can swap images
without editing your original code!
After you have completed the code and run it, you will end up with a GUI that looks something like
this:
Chapter 2 - Colors 42

Fig. 2-9: Image Converter

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:

1 file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")]


2
3 tmp_file = tempfile.NamedTemporaryFile(suffix=".jpg").name
4
5 effects = {
6 "Normal": shutil.copy,
7 "Black and White": black_and_white,
8 "Grayscale": grayscale,
9 "Sepia": sepia,
10 }

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”.

Here are the last few lines of code:

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:

• Getting Exif Tag Data


• Getting GPS Exif Data
• Getting TIFF Tag Data
• Creating an Exif GUI

Let’s get started by learning how to load Exif data with Pillow!

Getting Exif Tag Data


The Exchangeable Image File Format contains quite a bit of data about your photographs. It
includes your camera’s settings when you took the image, such as the ISO, aperture, focal length,
brightness value, and much more. Some cameras also include geographic coordinates in their Exif
tags.
You will be using one of the author’s photos of a local bridge from the state of Iowa as it contains
good Exif data:
⁷https://pillow.readthedocs.io/en/stable/reference/ExifTags.html
⁸https://pillow.readthedocs.io/en/stable/reference/TiffTags.html
Chapter 3 - Getting Image Metadata 50

Fig. 3-1: Iowan Bridge

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

Getting GPS Exif Data


Camera phones have the ability to tag your photos with location data when you take a photo. Most
phones have this off by default, but you can turn it on if you want to. Some newer cameras have their
own Global Positioning System (GPS) built-in to them that provide this ability. The digital single-
lens reflex (DSLR) cameras usually have some kind of accessory you can attach to your camera to
have it record GPS too. Or you can add the GPS tags yourself on your computer!
Regardless of how the GPS information ends up in your photo, you can use Pillow to get it. For this
example, you will use this image that was taken at Jester Park in Granger, Iowa:

Fig. 3-2: Jester Park

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!

Getting TIFF Tag Data


The TIFF format also has its metadata. Pillow provides a similar dictionary for TIFF images in its
TiffTags module. If you need a TIFF image, you can use this one, which is a cover from one of the
author’s other books on ReportLab:

Fig. 3-3: “ReportLab: PDF Processing with Python” cover art

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!

Creating an Exif GUI


Viewing the Exif data via the command line or a script is nice, but it can be difficult to read for the
user. You can make this easier by adding a graphical user interface.
Your viewer utility will end up looking like this:

Fig. 3-4: Exif Viewer

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.

You can move on to the main() function next:

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:

• Getting Exif Tag Data


Chapter 3 - Getting Image Metadata 62

• Getting TIFF Tag Data


• Creating an Exif GUI

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!

The BLUR Filter


Most photographers prefer their photos to be very sharp and clear. However, sometimes you may
want to add some blur to reduce the noise in a photo. For those of you who need to do that, the BLUR
filter is for you!
You will need to create a new file named blur_image.py and then add this code to it to see how to
blur your photos:
Chapter 4 - Filters 64

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

Fig. 4-1: A butterfly image

Now run your code and you will get the following output:
Chapter 4 - Filters 66

Fig. 4-2: Butterfly with BLUR Filter

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!

The CONTOUR Filter


Pillow’s CONTOUR filter allows you to find the contours in an image. This effect implements a type of
edge detection. To see how it works, create a file named contour_image.py. You can copy the code
from the previous section and change the filter line to use the new filter:
Chapter 4 - Filters 67

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:

Fig. 4-3: Flowers from Dallas, TX

When you run this code, you will get the following image as your output:
Chapter 4 - Filters 68

Fig. 4-4: Flowers with CONTOUR Filter

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!

The DETAIL Filter


Pillow’s DETAIL filter is used to make details more apparent in a photo. It is kind of like sharpening a
photo. This is a fairly subtle enhancement that you may or may not notice all that much depending
on the image you use it on.
Go ahead and create a new file named detail_image.py and add the following code to it:
Chapter 4 - Filters 69

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:

Fig. 4-5: A butterfly image

After running your code, the new image will look like this:
Chapter 4 - Filters 70

Fig. 4-6: Butterfly with DETAIL Filter

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!

The EDGE_ENHANCE Filters


There are two edge enhancement filters included in Pillow’s ImageFilter module:

• 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")

For this example, you will be using this photo of a cactus:

Fig. 4-7: Cactus

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

Fig. 4-8: Cactus with EDGE_ENHANCE Filter

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

Fig. 4-9: Cactus with EDGE_ENHANCE_MORE Filter

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!

The EMBOSS Filter


Embossing is a fancy term for stamping, carving, or molding a design. The EMBOSS filter takes in an
image and makes the image look like it has been embossed. The result kind of looks like your photo
was dipped in metal.
As usual, it is easier to see what the result is. So go ahead and create a file named emboss_image.py
and add this code:
Chapter 4 - Filters 74

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:

Fig. 4-10: Hummingbird

When you run the code above, it will “emboss” the hummingbird and make the image look like this:
Chapter 4 - Filters 75

Fig. 4-11: Hummingbird with EMBOSS Filter

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!

The FIND_EDGES Filter


Pillow has a basic edge detection filter built-in to the ImageFilter module that you can use
via FIND_EDGES. But what is edge detection anyway? Edge detection is a variety of different
mathematical methods that identify points in your image where the image brightness changes
sharply. These points are represented as a set of curved line segments called edges.
This book isn’t really about the math behind edge detection though. Besides that, Pillow isn’t the
best tool for this sort of thing anyway. You should use a library that is dedicated to machine learning
if you want to do different types of edge detection. You should check out OpenCV or scikit-image
for more accurate or sophisticated methods.
However, since Pillow does include the FIND_EDGES filter, you can still get to see what it does. Create
a new file called find_edges_image.py and add this code:
Chapter 4 - Filters 76

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:

Fig. 4-12: Buffalo

When you run this code, it will transform the buffalo above into the new buffalo image below:
Chapter 4 - Filters 77

Fig. 4-13: Buffalo with FIND_EDGES Filter

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.

The SHARPEN Filter


Photographers are almost always trying to take sharp photographs. But sometimes the object being
photographed moves or the photographer moves. When that happens, the photo tends to be slightly
(or even badly) out of focus. One way to correct a slightly out of focus photo is to apply a sharpening
filter.
Pillow provides just such a filter which is called SHARPEN. Create a new file named sharpen_image.py
and add the following code to it:
Chapter 4 - Filters 78

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:

Fig. 4-14: Grasshopper

When you run the code above, the result will look like this:
Chapter 4 - Filters 79

Fig. 4-15: Grasshopper with SHARPEN Filter

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!

The SMOOTH Filters


Smoothing filters are used to reduce the amount of noise in a photo. You will see noise in photos
that are taken without a lot of light. Most cameras aren’t very good at taking photos in low light. To
make up for that, you can increase the ISO level. This increases the camera’s ability to capture light.
Most cameras have an ISO range of 200 to 1600. You will start seeing some graininess when you go
above 1600. Sometimes you’ll notice the graininess sooner than that. It all depends on the quality of
the camera that you are using.
Pillow has two filters you can use to apply smoothing:

• 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

Fig. 4-16: Spider

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

Fig. 4-17: Spider with SMOOTH Filter

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

Fig. 4-18: Spider with SMOOTH_MORE Filter

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:

• Wikipedia: Kernel (image processing)⁹


• Pillow documentation on ImageFilter¹⁰ (See the Kernel section)

Pillow supports three types of RankFilters. They are as follows:

• 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:

Fig. 4-19: Giraffe

When you run this code on the giraffe photo, you will end up with this as your output:
Chapter 4 - Filters 86

Fig. 4-20: Giraffe with MinFilter Filter

MinFilter darkens the image slightly. Try changing the code to use MaxFilter instead and then
re-run the code.
Chapter 4 - Filters 87

Fig. 4-21: Giraffe with MaxFilter Filter

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:

Fig. 4-22: Tyrannosaurus Rex

Let’s get started by learning about the BoxBlur filter!

The BoxBlur Filter


A BoxBlur is also known as a box linear filter. When applied, it will take each pixel in the image
and average it out based on the values of its neighbors. You can use box blurs to approximate a
Gaussian blur. You can read more about this filter on Wikipedia if you want to know more:

• Wikipedia: Box blur¹¹

Here is the code for Pillow’s BoxBlur class:


¹¹https://en.wikipedia.org/wiki/Box_blur
Chapter 4 - Filters 89

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

Fig. 4-23: Tyrannosaurus Rex with the BoxBlur Filter

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!

The GaussianBlur Filter


Gaussian blurs are used to reduce image noise and reduce detail in the image. The result will look
like you are viewing the image through a translucent screen. Some computer vision algorithms also
use Gaussian smoothing to pre-process an image. One use-case is to filter out high frequencies and
noise from images to make detection of edges or patterns easier. There is a lot more information on
this topic on Wikipedia here:

• Wikipedia: Gaussian blur¹²

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

Fig. 4-24: Tyrannosaurus Rex with the GaussianBlur Filter

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!

The Color3DLUT Filter


The Color3DLUT is more commonly known as a 3D Lookup Table. You use this type of filter to
map one color space to another. A color space is a specific organization of colors. Examples include
sRGB, Adobe RGB, and the NCS System. If you’d like to dive down that rabbit hole, these links will
get you started:

• Wikipedia: 3D lookup table¹³


• Wikipedia: Color space¹⁴
¹³https://en.wikipedia.org/wiki/3D_lookup_table
¹⁴https://en.wikipedia.org/wiki/Color_space
Chapter 4 - Filters 93

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:

• Wikipedia: Digital intermediate¹⁵

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:

Fig. 4-25: Tyrannosaurus Rex with the Color3DLUT Filter

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!

The UnsharpMask Filter


The last filter is UnsharpMask. Interestingly enough, Unsharp masking (USM) is a sharpening
technique that uses a blurred (or “unsharp”) version of the image as a mask on the image. By
combining a blurred image with the original image, you end up with a sharper image. You can
read more about this technique here:

• Wikipedia: Digital unsharp masking¹⁶

Pillow’s implementation of unsharp masking is below:


¹⁶https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
Chapter 4 - Filters 95

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)

You can see that the UnsharpMask() takes three arguments:

• 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:

Fig. 4-26: Tyrannosaurus Rex without Filters

And here is the dinosaur with the filter applied to it:


Chapter 4 - Filters 97

Fig. 4-27: Tyrannosaurus Rex with UnsharpMask Filters

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.

Using Filters in a GUI


One of the nicest things about having code examples already written is that you can take it and
re-use the code. The filters that you learned about in this chapter can be used pretty easily in the
GUI you wrote back in chapter 2.
You should go grab that code (it’s in image_converter.py) and copy and paste it into a new file
named image_filter_gui.py. Next, modify the import statements so they look like this:
Chapter 4 - Filters 98

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

1 def apply_effect(values, window):


2 selected_effect = values["-EFFECTS-"]
3 image_file = values["-FILENAME-"]
4 if os.path.exists(image_file):
5 effects[selected_effect](image_file, tmp_file)
6 image = Image.open(tmp_file)
7 image.thumbnail((400, 400))
8 bio = io.BytesIO()
9 image.save(bio, format="PNG")
10 window["-IMAGE-"].update(data=bio.getvalue())

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

1 window = sg.Window("Image Filter App", layout)


2
3 while True:
4 event, values = window.read()
5 if event == "Exit" or event == sg.WIN_CLOSED:
6 break
7 if event in ["Load Image", "-EFFECTS-"]:
8 apply_effect(values, window)
9 if event == "Save" and values["-FILENAME-"]:
10 save_image(values)
11
12 window.close()

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

26 "Edge Enhance": edge_enhance,


27 "Emboss": emboss,
28 "Find Edges": find_edges,
29 }
30
31 def apply_effect(values, window):
32 selected_effect = values["-EFFECTS-"]
33 image_file = values["-FILENAME-"]
34 if os.path.exists(image_file):
35 effects[selected_effect](image_file, tmp_file)
36 image = Image.open(tmp_file)
37 image.thumbnail((400, 400))
38 bio = io.BytesIO()
39 image.save(bio, format="PNG")
40 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
41
42
43 def save_image(values):
44 save_filename = sg.popup_get_file(
45 "File", file_types=file_types, save_as=True, no_window=True
46 )
47 if save_filename == values["-FILENAME-"]:
48 sg.popup_error(
49 "You are not allowed to overwrite the original image!"
50 )
51 else:
52 if save_filename:
53 shutil.copy(tmp_file, save_filename)
54 sg.popup(f"Saved: {save_filename}")
55
56
57 def main():
58 effect_names = list(effects.keys())
59 layout = [
60 [sg.Image(key="-IMAGE-", size=(400,400))],
61 [
62 sg.Text("Image File"),
63 sg.Input(size=(25, 1), key="-FILENAME-"),
64 sg.FileBrowse(file_types=file_types),
65 sg.Button("Load Image")
66 ],
67 [
68 sg.Text("Effect"),
Chapter 4 - Filters 102

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

Fig. 4-28: Image Filter App

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:

• How Coordinates Work in Images


• Cropping Images
• Rotating Images
• Mirroring Images
• Resizing Images
• Scaling Images
• Creating an Image Rotator GUI

To kick things off, you’ll learn about Pillow’s coordinate system in the first section!

How Coordinates Work in Images


When you’re working with an image, you need to keep in mind that the image’s pixels are addressed
using x and y-coordinates. These specify the pixel’s horizontal and vertical location, respectively, in
the image.
The origin is a special term that describes where (0,0) is located. In images, it refers to the top-left
corner. The first zero is the x-coordinate. It starts at zero and as you increase, you go from left-to-
right. The second zero follows and represents the y-coordinate. As you increase this second value,
you go from top-to-bottom.
Another way to look at it is to say that increasing the x-coordinate moves the pixel to the right while
increasing the y-coordinate moves the pixel down.
You’ll discover that many of Pillow’s functions and methods take in a Python tuple that is known
as a box tuple. This type of tuple is four integer coordinates that represent a rectangular region in
Chapter 5 - Cropping, Rotating & Resizing Images 106

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:

Fig. 5-1: Praying Mantis

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")

Your crop_image() function takes 3 parameters:

• image_path – The file path to the file you want to crop


• coords – A box tuple that contains the beginning and end coordinates to crop the image to
• saved_location – The file path to save the cropped file to

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

Fig. 5-2: Praying Mantis Cropped

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

Fig. 5-3: Dragonfly

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:

Fig. 5-4: Dragonfly Rotated

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:

Fig. 5-5: Brown 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:

Fig. 5-6: Mirrored Brown Praying Mantis

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

1 def resize(self, size, resample=BICUBIC, box=None, reducing_gap=None):

Here is an explanation of these arguments, according to the docstring of the method:

• size: The requested size in pixels, as a 2-tuple.


• resample: An optional resampling filter.
• box: An optional 4-tuple of floats providing the source image region to be scaled.
• reducing_gap: Apply optimization by resizing the image in two steps. First, reducing the image
by integer times using PIL.Image.Image.reduce. Second, resizing using regular resampling.
The last step changes size no less than by reducing_gap times.

For this example, you’ll be using this photo from the Pilot Knob State Park, which is in Iowa:

Fig. 5-7: Pilot Knob

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

Fig. 5-8: Pilot Knob Resized

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:

1 def thumbnail(self, size, resample=BICUBIC, reducing_gap=2.0):


2 """
3 Make this image into a thumbnail. This method modifies the
4 image to contain a thumbnail version of itself, no larger than
5 the given size. This method calculates an appropriate thumbnail
6 size to preserve the aspect of the image, calls the
7 :py:meth:`~PIL.Image.Image.draft` method to configure the file reader
8 (where applicable), and finally resizes the image.
9 """

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!

Creating an Image Rotator GUI


In this section, you’ll create a GUI that lets you rotate and mirror images using some of the code
from this chapter. You can take the GUI that you wrote in chapter 4 (image_filter_gui.py) and
copy the code into a new file named image_rotator_gui.py.
When you’re done modifying this code, the GUI will look like this:
Chapter 5 - Cropping, Rotating & Resizing Images 118

Fig. 5-9: Image Rotator GUI

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

87 event, values = window.read()


88 if event == "Exit" or event == sg.WIN_CLOSED:
89 break
90 if event in ["Load Image", "-EFFECTS-"]:
91 apply_effect(values, window)
92 image_filename = values["-FILENAME-"]
93 if event == "Save" and image_filename:
94 save_image(image_filename)
95
96 window.close()
97
98
99 if __name__ == "__main__":
100 main()

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

1 def apply_rotate(image_file, effect):


2 if effect == "Rotate 90":
3 rotate(image_file, 90, tmp_file)
4 elif effect == "Rotate 180":
5 rotate(image_file, 180, tmp_file)
6 elif effect == "Rotate 270":
7 rotate(image_file, 270, tmp_file)

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():

1 def apply_effect(values, window):


2 selected_effect = values["-EFFECTS-"]
3 image_file = values["-FILENAME-"]
4 if os.path.exists(image_file):
5 if "Rotate" in selected_effect:
6 apply_rotate(image_file, selected_effect)
7 else:
8 effects[selected_effect](image_file, tmp_file)
9 image = Image.open(tmp_file)
10 image.thumbnail((400, 400))
11 bio = io.BytesIO()
12 image.save(bio, format="PNG")
13 window["-IMAGE-"].update(data=bio.getvalue(), size=(400, 400))

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:

• How Coordinates Work in Images


• Cropping Images
• Rotating Images
• Mirroring Images
Chapter 5 - Cropping, Rotating & Resizing Images 123

• 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:

• Adjust Color Balance


• Adjust Image Contrast
• Adjust Image Brightness
• Adjust Image Sharpness

You can do all of these adjustments using ImageEnhance. Let’s get started by learning how to change
the color balance of an image!

Adjust Color Balance


Pillow allows you to modify an image’s color balance. You can do something similar on most
televisions and computer monitors. You use a floating point number that Pillow calls a factor,
which it then uses to change the color balance. If you use a factor of 0.0, the image will be in black
and white. A factor of 1.0 returns a copy of the original image.
Factors lower than 1.0 will result in less color in the image. Factors that are higher than 1.0 will
enhance the color. If you increase the factor too much, the colors will be too bright. There really are
no bounds for the factor, but in most cases you won’t want to go higher than 3 or 4.
For this example, you will be using this image of a Goldenrod Soldier Beetle:
¹⁷https://pillow.readthedocs.io/en/3.0.x/reference/ImageEnhance.html
Chapter 6 - Enhancing Images with ImageEnhance 125

Fig. 6-1: Goldenrod Soldier Beetle

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:

Fig. 6-2: Goldenrod Soldier Beetle with Enhancement Factor 0.0

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

Fig. 6-3: Goldenrod Soldier Beetle with Enhancement Factor 0.5

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

Fig. 6-4: Goldenrod Soldier Beetle with Enhancement Factor 2.5

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

Fig. 6-5: Goldenrod Soldier Beetle Side-by-Side Comparison

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!

Adjust Image Contrast


Contrast is defined as the difference between dark and light in an image. A high contrast image will
have bright highlights and nice, dark shadows. Colors are usually bold and display texture. A low
contrast image, on the other hand, will seem dull or flat in comparison. The colors will be muted as
well.
In Pillow, you will need to create an instance of ImageEnhance.Contrast() and then use it to apply
an enhancement factor to your image. If you apply an enhancement factor of 0.0 as you did in the
previous example, you will get a solid gray image. A factor of 1.0 will return a copy of the original
image, which is what happens with all of the enhancers.
To see how it works, you will use a couple of other enhancement factors that actually change the
way the image looks. For this example, you will be using this photo of a Madison County bridge:
Chapter 6 - Enhancing Images with ImageEnhance 130

Fig. 6-6: Madison County Bridge

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:

Fig. 6-7: Madison County Bridge with Enhancement Factor 2.5

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!

Adjust Image Brightness


Image brightness is a way to measure the intensity or luminosity of an image after it has been
captured. Pillow’s ImageEnhance module allows you to modify the brightness of an image using the
Brightness() class. If you use an enhancement factor of 0.0, Pillow will return a solid black image.
When you use an enhancement factor of 1.0, you will get a copy of the original image returned.
For this example, you will be using a photo of Silver Falls from the state of Oregon:
Chapter 6 - Enhancing Images with ImageEnhance 132

Fig. 6-8: Silver Falls in Oregon

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:

Fig. 6-9: Silver Falls in Oregon with Enhancement Factor 1.5

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

Adjust Image Sharpness


You learned about sharpening images using Pillow’s filters back in chapter 4. As you may recall,
photographers try to take photos that are in focus, or sharp. When they fail to do so, the images are
blurry. You can sometimes make a blurry image look better by using a sharpening technique. You
can also sharpen an image that is already in focus to make it look even better. You should try both
methods on your photos as one may work better depending on what part of your image needs to be
sharpened.
If you apply an enhancement factor of 0.0, your image will be blurry. If you use 1.0, you will get a
copy of the original image back. An enhancement factor of 2.0 will result in a sharpened image.
You will be using this photo of a hummingbird from Northern Minnesota:

Fig. 6-10: Hummingbird

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

Fig. 6-11: Hummingbird Sharpened with an Enhancement Factor of 2.5

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!

Enhancing Photos with a GUI


It would be nice to try different enhancement factors and see how they turn out before saving the
result to a new file. You can do that with a GUI. You’ll be using the same basic code that you used in
the last few chapters. This code does save the image off in a temporary location, but the GUI allows
you to save the file where you want it after applying your changes.
This is a screenshot of the GUI when it is finished:
Chapter 6 - Enhancing Images with ImageEnhance 137

Fig. 6-12: Image Enhancer GUI

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

1 def apply_effect(values, window):


2 selected_effect = values["-EFFECTS-"]
3 image_file = values["-FILENAME-"]
4 factor = values["-FACTOR-"]
5 if image_file:
6 if selected_effect == "Normal":
7 effects[selected_effect](image_file, tmp_file)
8 else:
9 effects[selected_effect](image_file, factor, tmp_file)
10
11 image = Image.open(tmp_file)
12 image.thumbnail((400, 400))
13 bio = io.BytesIO()
14 image.save(bio, format="PNG")
15 window["-IMAGE-"].update(data=bio.getvalue(), size=(400, 400))

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:

• Adjust Color Balance


• Adjust Image Contrast
• Adjust Image Brightness
• Adjust Image Sharpness

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:

• im - The Image object


• box - A 2-tuple of the upper left corner or a 4-tuple defining the left, upper, right, and lower
pixel coordinate, or None.
• mask - If the mask is given, paste() will only update the regions indicated by the mask.
Chapter 7 - Combining Images 145

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:

Fig. 7-1: Hummingbird

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:

Fig. 7-2: Hummingbird image with Pasted Copy

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

Fig. 7-3: Hummingbird Tiled Image

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:

• Crop one image down to match


• Resize the images to match
• Make up the difference by adding a margin

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")

For this example, you create two functions:

• concatenate_vertically() - for concatenating two images from top-to-bottom


• concatenate_horizontally() - for concatenating two images from left-to-right
Chapter 7 - Combining Images 150

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

Fig. 7-4: Silver Falls Vertical Concatenation

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

Fig. 7-5: Silver Falls Horizontal Concatenation

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

15 new_image.paste(image_one, (0, 0))


16 new_image.paste(image_two, (0, image_one.height))
17
18 new_image.save(output_image_path)
19
20
21 def concatenate_horizontally(
22 first_image_path, second_image_path, output_image_path,
23 ):
24 image_one = Image.open(first_image_path)
25 image_two = Image.open(second_image_path)
26 width = image_one.width + image_two.width
27 height = min(image_one.height, image_two.height)
28 new_image = Image.new("RGB", (width, height))
29
30 new_image.paste(image_one, (0, 0))
31 new_image.paste(image_two, (image_one.width, 0))
32
33 new_image.save(output_image_path)
34
35
36 if __name__ == "__main__":
37 concatenate_horizontally(
38 "hummingbird.jpg", "silver_falls2.jpg", "h_combined.jpg",
39 )
40 concatenate_vertically(
41 "hummingbird.jpg", "silver_falls2.jpg", "v_combined.jpg",
42 )

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

Fig. 7-6: Hummingbird and Silver Falls Horizontal Concatenation

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:

Fig. 7-7: Mouse Vs Python Logo


Chapter 7 - Combining Images 155

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:

• input_image_path - the file you want to add a watermark to


• output_image_path - the file that you save with the watermark applied
• watermark_image_path - the watermark image file path
• position - the position to place the watermark at

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

Fig. 7-8: Hummingbird with watermark

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

14 transparent.paste(base_image, (0, 0))


15 transparent.paste(watermark, position, mask=watermark)
16 transparent.save(output_image_path)
17
18
19 if __name__ == "__main__":
20 watermark_with_transparency(
21 "hummingbird.jpg", "hummingbird_watermarked2.jpg", "logo.png",
22 position=(0, 0),
23 )

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:

Fig. 7-9: Hummingbird with watermark using transparency


Chapter 7 - Combining Images 158

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:

1 def composite(image1, image2, mask):


2 """
3 Create composite image by blending images using a transparency mask.
4
5 :param image1: The first image.
6 :param image2: The second image. Must have the same mode and
7 size as the first image.
8 :param mask: A mask image. This image can have mode
9 "1", "L", or "RGBA", and must have the same size as the
10 other two images.
11 """
12 image = image2.copy()
13 image.paste(image1, None, mask)
14 return image

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

Fig. 7-10: Pilot Knob

You’ll also use this grasshopper photo from a different chapter:


Chapter 7 - Combining Images 160

Fig. 7-11: Grasshopper

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:

Fig. 7-12: Composited Images

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

1 def blend(im1, im2, alpha):


2 """
3 Creates a new image by interpolating between two input images, using
4 a constant alpha.::
5
6 out = image1 * (1.0 - alpha) + image2 * alpha
7
8 :param im1: The first image.
9 :param im2: The second image. Must have the same mode and size as
10 the first image.
11 :param alpha: The interpolation alpha factor. If alpha is 0.0, a
12 copy of the first image is returned. If alpha is 1.0, a copy of
13 the second image is returned. There are no restrictions on the
14 alpha value. If necessary, the result is clipped to fit into
15 the allowed output range.
16 :returns: An :py:class:`~PIL.Image.Image` object.
17 """

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

Fig. 7-13: Skyline

The second image will be of this murex shell:


Chapter 7 - Combining Images 164

Fig. 7-14: Murex shell

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:

Fig. 7-15: Blending to Images with Alpha of 0.2

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

Fig. 7-16: Blending two Images with Alpha of 0.4

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!

Creating a Watermark GUI


Wouldn’t it be nice if you could position the watermark on an image exactly where you wanted it?
In this section, you will create a graphical user interface that lets you specify the position of the
watermark. You will do so by entering the X and Y position that the watermark should go to. Then
you can apply the watermark to the image and see where it is on the image.
When you are finished, your GUI will look like this:
Chapter 7 - Combining Images 167

Fig. 7-17: Watermark GUI

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

87 sg.Input("0", size=(5, 1), enable_events=True,


88 key="-WATERMARK-Y-"),
89 ],
90 [
91 sg.Button("Apply Watermark", enable_events=True),
92 sg.Button("Save Image", enable_events=True),
93 ],
94 ]
95
96 window = sg.Window("Watermark GUI", layout)
97
98 while True:
99 event, values = window.read()
100
101 if event == "Exit" or event == sg.WIN_CLOSED:
102 break
103
104 watermark_x = values["-WATERMARK-X-"]
105 watermark_y = values["-WATERMARK-Y-"]
106
107 if event == "Load Image":
108 filename = values["-FILENAME-"]
109 if os.path.exists(filename):
110 photo_img = convert_image(filename)
111 window["-IMAGE-"].update(data=photo_img, size=(400,400))
112 original_image = filename
113 shutil.copy(original_image, tmp_file)
114 if event in ["-WATERMARK-X-", "-WATERMARK-Y-"]:
115 # filter watermark position to integers
116 if watermark_x and watermark_y:
117 if not watermark_x[-1].isdigit():
118 window["-WATERMARK-X-"].update(watermark_x[:-1])
119 if not watermark_y[-1].isdigit():
120 window["-WATERMARK-Y-"].update(watermark_y[:-1])
121 if event == "Apply Watermark":
122 if check_for_errors(values):
123 continue
124 position = (int(watermark_x), int(watermark_y))
125 apply_watermark(original_image, values, position, window)
126 if event == "Save Image" and values["-FILENAME-"]:
127 save_image(values)
128
129 window.close()
Chapter 7 - Combining Images 171

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

Next up, you’ll learn how to create a row of Elements in create_row():

1 def create_row(label, key, file_types, save=False):


2 if save:
3 return [
4 sg.Text(label),
5 sg.Input(size=(25, 1), key=key),
6 sg.FileSaveAs(file_types=file_types),
7 ]
8 else:
9 return [
10 sg.Text(label),
11 sg.Input(size=(25, 1), key=key),
12 sg.FileBrowse(file_types=file_types),
13 ]

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:

1 def apply_watermark(original_image, values, position, window):


2 watermark_with_transparency(
3 original_image, tmp_file, values["-WATERMARK-"], position,
4 )
5 photo_img = convert_image(tmp_file)
6 window["-IMAGE-"].update(data=photo_img, size=(400,400))

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:

• (x1, y1, x2, y2, x3, y3...)


• ((x1, y1), (x2, y2), (x3, y3)...)

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

The default is None or no fill.

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

1 def line(self, xy, fill=None, width=0, joint=None):


2 """Draw a line, or a connected sequence of line segments."""

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:

Fig. 8-1: Madison County Bridge

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

10 colors = ["red", "green", "blue", "yellow", "purple", "orange"]


11
12 for i in range(0, 100, 20):
13 draw.line((i, 0) + image.size, width=5,
14 fill=random.choice(colors))
15
16 image.save(output_path)
17
18 if __name__ == "__main__":
19 line("madison_county_bridge_2.jpg", "lines.jpg")

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:

Fig. 8-2: Madison County Bridge with Lines

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

Fig. 8-3: Jointed Lines

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

Fig. 8-4: Lines without Joint Set

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:

1 def arc(self, xy, start, end, fill=None, width=1):


2 """Draw an arc."""

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

Fig. 8-5: Drawing Arcs

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():

1 def chord(self, xy, start, end, fill=None, outline=None, width=1):


2 """Draw a 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

Fig. 8-6: Drawing Chords

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:

1 def ellipse(self, xy, fill=None, outline=None, width=1):


2 """Draw an ellipse."""

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

Fig. 8-7: Drawing Ellipses

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!

Drawing Pie Slices


A pie slice is the same as arc()), but also draws straight lines between the endpoints and the center
of the bounding box.
Here is how the pieslice() method is defined:

1 def pieslice(self, xy, start, end, fill=None, outline=None, width=1):


2 """Draw a pieslice."""

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

Fig. 8-8: Drawing Pie Slices

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:

1 def polygon(self, xy, fill=None, outline=None):


2 """Draw a polygon."""

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

Fig. 8-9: Drawing Polygons

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:

1 def rectangle(self, xy, fill=None, outline=None, width=1):


2 """Draw a rectangle."""

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

Fig. 8-10: Drawing Rectangles

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!

Creating a Drawing GUI


The purpose of this user interface is to show you how you can make a GUI that wraps multiple
shapes. It doesn’t wrap all the shapes that you have covered here. In fact, this GUI only supports the
following:

• 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

Fig. 8-11: Drawing GUI

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

Now you can take a look at your first function:

1 def get_value(key, values):


2 value = values[key]
3 if value.is_digit():
4 return int(value)
5 return 0

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

1 def apply_drawing(values, window):


2 image_file = values["-FILENAME-"]
3 shape = values["-SHAPES-"]
4 begin_x = get_value("-BEGIN_X-", values)
5 begin_y = get_value("-BEGIN_Y-", values)
6 end_x = get_value("-END_X-", values)
7 end_y = get_value("-END_Y-", values)
8 width = get_value("-WIDTH-", values)
9 fill_color = values["-FILL_COLOR-"]
10 outline_color = values["-OUTLINE_COLOR-"]
11
12 if image_file and os.path.exists(image_file):
13 shutil.copy(image_file, tmp_file)
14 image = Image.open(tmp_file)
15 image.thumbnail((400, 400))
16 draw = ImageDraw.Draw(image)
17 if shape == "Ellipse":
18 draw.ellipse(
19 (begin_x, begin_y, end_x, end_y),
20 fill=fill_color,
21 width=width,
22 outline=outline_color,
23 )
24 elif shape == "Rectangle":
25 draw.rectangle(
26 (begin_x, begin_y, end_x, end_y),
27 fill=fill_color,
28 width=width,
29 outline=outline_color,
30 )
31 image.save(tmp_file)
32
33 bio = io.BytesIO()
34 image.save(bio, format="PNG")
35 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))

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:

• The image filename


• The shape combobox
• The beginning coordinates (-BEGIN_X-, -BEGIN_Y-)
• The ending coordinates (-END_X-, -END_Y-)
Chapter 8 - Drawing Shapes 203

• The width of the outline


• The fill color to be used (uses color names)
• The outline color (uses color names)

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():

1 def create_coords_elements(label, begin_x, begin_y, key1, key2):


2 return [
3 sg.Text(label),
4 sg.Input(begin_x, size=(5, 1), key=key1, enable_events=True),
5 sg.Input(begin_y, size=(5, 1), key=key2, enable_events=True),
6 ]

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:

1 >>> def get_elements():


2 ... return ["element_one", "element_two"]
3 ...
4 >>> [*get_elements()]
5 ['element_one', 'element_two']

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:

1 def text(xy, text, fill=None, font=None, anchor=None, spacing=4, align='left', direc\


2 tion=None,
3 features=None, language=None, stroke_width=0, stroke_fill=None, embedded_co\
4 lor=False)

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

• text - The string of text that you wish to draw.


• fill - The color of the text (can be a tuple, an integer (0-255), or one of the supported color
names).
• font - An ImageFont instance.
• anchor - The text anchor alignment. Determines the relative location of the anchor to the text.
The default alignment is top left.
• spacing - If the text is passed on to multiline_text(), this controls the number of pixels
between lines.
• align - If the text contains multiple lines, determines the relative alignment of those lines –
can be "left", "center", or "right". Use the anchor parameter to specify the alignment to xy.
• direction - Direction of the text. It can be "rtl" (right to left), "ltr" (left to right) or "ttb"
(top to bottom). Requires libraqm.
• features - A list of OpenType font features to be used during text layout. Requires libraqm.
• language - The language of the text. Different languages may use different glyph shapes or
ligatures. This parameter tells the font which language the text is in, and to apply the correct
substitutions as appropriate, if available. It should be a BCP 47 language code. Requires libraqm.
• stroke_width - The width of the text stroke
• stroke_fill - The color of the text stroke. If you don’t set this, it defaults to the fill
parameter’s value.
• embedded_color - Whether to use font embedded color glyphs (COLR or CBDT).

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:

Fig. 9-1: Pillow’s Default Font

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!

Loading TrueType Fonts


Pillow supports loading TrueType and OpenType fonts. So if you have a favorite font or a company-
mandated one, Pillow can probably load it. There are many open-source TrueType fonts that you
can download. One popular option is Gidole, which you can get on GitHub²⁰.
The Pillow package also comes with several fonts in its test folder. You can download Pillow’s source
on GitHub²¹ too.
This book’s code repository²² on GitHub includes the Gidole font as well as a handful of the fonts
from the Pillow tests folder that you can use for the examples in this chapter.
To see how you can load up a TrueType font, create a new file and name it draw_truetype.py. Then
enter the following:

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

Fig. 9-2: Chihuly Exhibit at the Dallas Arboretum

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

Fig. 9-3: Different Font Sizes

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

13 font = ImageFont.truetype(ttf_file, size=44)


14 draw.text((10, y), f"{ttf_file} (font_size=44)", font=font)
15 y += 55
16 image.save(output_path)
17
18 if __name__ == "__main__":
19 truetype("chihuly_exhibit.jpg", "truetype_fonts.jpg")

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:

Fig. 9-4: Drawing Text in Different Fonts

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

Changing Text Color


Pillow allows you to change the color of your text by using the fill parameter. You can set this
color using an RGB tuple, an integer, or a supported color name.
Go ahead and create a new file and name it text_colors.py. Then enter the following code into it:

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

Fig. 9-5: Drawing Text in Different Colors

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!

Drawing Multiple Lines of Text


Pillow also supports drawing multiple lines of text at once. In this section, you will learn two different
methods of drawing multiple lines. The first is by using Python’s newline character: \n.
To see how that works, create a file and name it draw_multiline_text.py. Then add the following
code:

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:

Fig. 9-6: Drawing Multiple Lines of Text

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

13 draw.multiline_text((10, 25), text, font=font)


14 image.save(output_path)
15
16 if __name__ == "__main__":
17 text("chihuly_exhibit.jpg", "multiline_text_2.jpg")

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:

Fig. 9-7: Drawing Multiple Lines of Text using multiline_text()

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

Fig. 9-8: Aligned Text Examples

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!

Fig. 9-9: Centered Text

Now let’s find out how to change your text’s opacity!

Changing Text Opacity


Pillow supports changing the text’s opacity as well. What that means is that you can make the text
transparent, opaque, or somewhere in between. This only works with images that have an alpha
channel.
Chapter 9 - Drawing Text 221

For this example, you will use this flower image:

Fig. 9-10: Pink Flowers

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

17 draw.text((10,60), "Rocks!", font=font, fill=(255,255,255,255))


18
19 composite = Image.alpha_composite(base_image, txt_img)
20 composite.save(output_path)
21
22 if __name__ == "__main__":
23 change_opacity("flowers_dallas.png", "flowers_opacity.png")

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:

Fig. 9-11: Text Opacity Example


Chapter 9 - Drawing Text 223

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.

Learning About Text Anchors


You can use the anchor parameter to determine the alignment of your text relative to the xy
coordinates you give. The default is top-left, which is the la anchor. According to the documentation,
la means left-ascender aligned text. The first letter in an anchor specifies its horizontal alignment
while the second letter specifies its vertical alignment. In the next two sub-sections, you will learn
what each of the anchor names mean.

Horizontal Anchor Alignment


There are four horizontal anchors. The following is an adaptation from the documentation on
horizontal anchors:
Preferred Horizontal Anchors for Horizontal Text

• 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.

Preferred Horizontal Anchors for Vertical Text

• 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.

Vertical Anchor Alignment


There are six vertical anchors. The following is an adaptation from the documentation on vertical
anchors:
Preferred Vertical Anchors for Horizontal 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

Preferred Vertical Anchors for Vertical Text

• 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

Fig. 9-12: Anchor Examples

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!

Creating a Text Drawing GUI


In this section, you will create a user interface that allows your users to draw text on an image of
their choice. You will also allow them to modify the following aspects of their text:

• Font type
• Font color
• Font-size
• Text position

When your GUI is finished, it will look like this:


Chapter 9 - Drawing Text 226

Fig. 9-13: Text GUI

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

130 [sg.Button("Save Image")],


131 ]
132
133 window = sg.Window("Draw Text GUI", layout)
134
135 while True:
136 event, values = window.read()
137 if event == "Exit" or event == sg.WIN_CLOSED:
138 break
139 if event in ["Load Image", "-COLORS-", "-TTF-", "-FONT_SIZE-",
140 "-TEXT-X-", "-TEXT-Y-", "-TEXT-"]:
141 apply_text(values, window)
142 if event == "Save Image" and values["-FILENAME-"]:
143 save_image(values)
144 if event == "Open Font Directory":
145 update_ttf_values(window)
146
147 window.close()
148
149 if __name__ == "__main__":
150 main()

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:

1 def get_value(key, values):


2 value = values[key]
3 if value.isdigit():
4 return int(value)
5 return 0

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():

1 def apply_text(values, window):


2 global ttf_files
3 image_file = values["-FILENAME-"]
4 font_name = values["-TTF-"]
5 font_size = get_value("-FONT_SIZE-", values)
6 color = values["-COLORS-"]
7 x, y = get_value("-TEXT-X-", values), get_value("-TEXT-Y-", values)
8 text = values["-TEXT-"]
9
10 if image_file and os.path.exists(image_file):
11 shutil.copy(image_file, tmp_file)
12 image = Image.open(tmp_file)
13 image.thumbnail((400, 400))
14
15 if text:
16 draw = ImageDraw.Draw(image)
17 if font_name == "Default Font":
18 font = None
19 else:
20 font = ImageFont.truetype(ttf_files[font_name], size=font_size)
21 draw.text((x, y), text=text, font=font, fill=color)
22 image.save(tmp_file)
23
24 bio = io.BytesIO()
25 image.save(bio, format="PNG")
26 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))
Chapter 9 - Drawing Text 232

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():

1 def create_row(label, key, file_types):


2 return [
3 sg.Text(label),
4 sg.Input(size=(25, 1), key=key),
5 sg.FileBrowse(file_types=file_types),
6 ]

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:

• ImageChops.blend() is an alias for Image.blend()


• ImageChops.composite() is an alias for Image.composite()
• ImageChops.duplicate() is an alias for Image.copy()

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:

1 def add(image1, image2, scale=1.0, offset=0):


2 """
3 Adds two images, dividing the result by scale and adding the offset. If omitted,\
4 scale defaults to 1.0, and offset to 0.0.
5
6 out = ((image1 + image2) / scale + offset)
7 """

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

Fig. 10-1: Murex Shell

You will also be using this skyline photo:


Chapter 10 - Channel Operations 240

Fig. 10-2: Skyline

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

16 add_images("shell.png", "skyline.png", "added_images.jpg")

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:

Fig. 10-3: Skyline and Shell Added Together

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

1 def darker(image1, image2):


2 """
3 Compares the two images, pixel by pixel, and returns a new image containing
4 the darker values.
5
6 out = min(image1, image2)
7 """

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

Fig. 10-4: Using ImageChops.darker()

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

1 def lighter(image1, image2):


2 """
3 Compares the two images, pixel by pixel, and returns a new image containing
4 the lighter values.
5
6 out = max(image1, image2)
7 """

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

Fig. 10-5: Using ImageChops.lighter()

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.

Finding Differences in Images


There are times when you might want or need to know if an image has changed. For example, you
might need to write a test that checks if your application draws something the same way every time.
One way to check that would be to take a known good output and verify that the output of your
application doesn’t change when new features are added or the code is refactored.
One way to do that would be to use Pillow’s ImageChops.difference function.
You can’t use the images you were using before because they are completely different, making this
comparison pointless. Instead, you will use this image of a butterfly:
Chapter 10 - Channel Operations 246

Fig. 10-6: Butterfly

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

Fig. 10-7: Butterfly with Watermark

These images are in the GitHub repository for this book.


Here is the code definition for the difference() function:

1 def difference(image1, image2):


2 """
3 Returns the absolute value of the pixel-by-pixel difference between the two
4 images.
5
6 out = abs(image1 - image2)
7 """

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:

Fig. 10-8: Image After Applying ImageChops.difference()


Chapter 10 - Channel Operations 249

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

Fig. 10-9: Butterfly

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

Fig. 10-10: Butterfly

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()!

Using Soft Light on Images


The documentation for the ImageChops.soft_light() function is pretty light. It’s pretty much
exactly what you see in the function definition.
Here it is so you can learn what it does too:

1 def soft_light(image1, image2):


2 """
3 Superimposes two images on top of each other using the Soft Light algorithm
4 """

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

Fig. 10-11: Applying Soft Light

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”.

Using Hard Light on Images


The hard light algorithm is a combination of Multiply and Screen. Multiply is a blend mode that
multiplies the RGB channel numbers for each pixel from the top layer with the values for the
corresponding pixel from the bottom layer. Screen, on the other hand, is a blend mode that takes the
two layers, inverts them, multiplies them, and then inverts them a second time. You can read more
about this on Wikipedia²⁹.
The Pillow definition doesn’t describe that at all:

²⁹https://en.wikipedia.org/wiki/Blend_modes#Multiply_and_Screen
Chapter 10 - Channel Operations 254

1 def hard_light(image1, image2):


2 """
3 Superimposes two images on top of each other using the Hard Light algorithm
4 """

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

Fig. 10-12: Applying Hard Light

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:

1 def overlay(image1, image2):


2 """
3 Superimposes two images on top of each other using the Overlay algorithm
4 """

Overlay is a combination of Multiply and Screen, although it is a slightly different combination


than the “hard light” algorithm. Even Wikipedia³⁰ doesn’t explain the difference though.
³⁰https://en.wikipedia.org/wiki/Blend_modes#Overlay
Chapter 10 - Channel Operations 256

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

Fig. 10-13: Overlaying Images

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.

Creating a Blending GUI


Blending images is fun, but it can be hard to imagine what the output will look like. So in this section,
you will take what you have learned from this chapter and wrap a graphical user interface (GUI)
around the modules you created. This will let you experiment with different images and blend them
in real-time so you can see what the output will look like.
When you are done, your user interface will look like this:
Chapter 10 - Channel Operations 258

Fig. 10-14: ImageChops GUI

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

87 create_row("Image File 1:", "-FILENAME_ONE-", file_types),


88 [sg.Button("Load Image")],
89 create_row("Image File 2:", "-FILENAME_TWO-", file_types),
90 [
91 sg.Text("Effect"),
92 sg.Combo(
93 effect_names, default_value="Normal", key="-EFFECTS-",
94 enable_events=True, readonly=True,
95 ),
96 ],
97 [sg.Button("Save")],
98 ]
99
100 window = sg.Window("ImageChops GUI", layout, size=(450, 600))
101
102 events = ["Load Image", "-FILENAME_TWO-", "-EFFECTS-"]
103 while True:
104 event, values = window.read()
105 if event == "Exit" or event == sg.WIN_CLOSED:
106 break
107 if event in events:
108 apply_effect(values, window)
109 filename_one = values["-FILENAME_ONE-"]
110 filename_two = values["-FILENAME_TWO-"]
111 if event == "Save" and filename_one:
112 save_image(filename_one, filename_two)
113
114 window.close()
115
116
117 if __name__ == "__main__":
118 main()

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:

• file_types - the file types to load or save


• tmp_file - the temporary file to save interim images to
• effects - a dictionary of channel operations functions to apply to the image

Now you are ready to proceed to the apply_effect() function:


Chapter 10 - Channel Operations 263

1 def apply_effect(values, window):


2 selected_effect = values["-EFFECTS-"]
3 image_file_one = values["-FILENAME_ONE-"]
4 image_file_two = values["-FILENAME_TWO-"]
5 if os.path.exists(image_file_one):
6 shutil.copy(image_file_one, tmp_file)
7 if selected_effect in ["Normal", "Negative"]:
8 effects[selected_effect](image_file_one, tmp_file)
9 elif os.path.exists(image_file_two):
10 effects[selected_effect](
11 image_file_one, image_file_two, tmp_file,
12 )
13 elif selected_effect not in ["Normal", "Negative"]:
14 sg.popup(
15 "You need both images selected to apply this effect!",
16 )
17 return
18
19 image = Image.open(tmp_file)
20 image.thumbnail((400, 400))
21 bio = io.BytesIO()
22 image.save(bio, format="PNG")
23 window["-IMAGE-"].update(data=bio.getvalue(), size=(400,400))

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

1 def create_row(label, key, file_types):


2 return [
3 sg.Text(label),
4 sg.Input(size=(25, 1), key=key),
5 sg.FileBrowse(file_types=file_types),
6 ]

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():

1 events = ["Load Image", "-FILENAME_TWO-", "-EFFECTS-"]


2 while True:
3 event, values = window.read()
4 if event == "Exit" or event == sg.WIN_CLOSED:
5 break
6 if event in events:
7 apply_effect(values, window)
8 filename_one = values["-FILENAME_ONE-"]
9 filename_two = values["-FILENAME_TWO-"]
10 if event == "Save" and filename_one:
11 save_image(filename_one, filename_two)
12
13 window.close()
14
15
16 if __name__ == "__main__":
17 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:

• Applying Automatic Contrast


• Colorizing Photos
• Padding an Image
• Adding a Border
• Removing a Border
• Scaling an Image
• Equalizing the Histogram
• Sizing and Cropping an Image
• Flipping an Image
• Mirroring an Image
• Inverting an Image
• Posterize an Image
• Solarize an Image
• Transpose Image Using Exif Orientation
• Creating an ImageOps GUI

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.

Applying Automatic Contrast


The autocontrast() function’s purpose is to maximize or normalize your image’s contrast. It does
that by taking the image’s histogram and removing the cutoff percent of the lightest and darkest
pixels. Then it remaps the image so that the darkest pixel becomes black (0) and the lightest pixel
becomes white (255).
Here is the official function definition:
³¹https://pillow.readthedocs.io/en/stable/reference/ImageOps.html
Chapter 11 - The ImageOps Module 268

1 def autocontrast(image, cutoff=0, ignore=None, mask=None):


2 """
3 Maximize (normalize) image contrast. This function calculates a
4 histogram of the input image (or mask region), removes ``cutoff`` percent of the
5 lightest and darkest pixels from the histogram, and remaps the image
6 so that the darkest pixel becomes black (0), and the lightest
7 becomes white (255).
8
9 :param image: The image to process.
10 :param cutoff: The percent to cut off from the histogram on the low and
11 high ends. Either a tuple of (low, high), or a single
12 number for both.
13 :param ignore: The background pixel value (use None for no background).
14 :param mask: Histogram used in contrast operation is computed using pixels
15 within the mask. If no mask is given the entire image is used
16 for histogram computation.
17 :return: An image.
18 """

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

Fig. 11-1: Ducklings

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:

Fig. 11-2: Ducklings with Auto Contrast

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

1 def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=1\


2 27):
3 """
4 Colorize grayscale image.
5 This function calculates a color wedge which maps all black pixels in
6 the source image to the first color and all white pixels to the
7 second color. If ``mid`` is specified, it uses three-color mapping.
8 The ``black`` and ``white`` arguments should be RGB tuples or color names;
9 optionally you can use three-color mapping by also specifying ``mid``.
10 Mapping positions for any of the colors can be specified
11 (e.g. ``blackpoint``), where these parameters are the integer
12 value corresponding to where the corresponding color should be mapped.
13 These parameters must have logical order, such that
14 ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
15
16 :param image: The image to colorize.
17 :param black: The color to use for black input pixels.
18 :param white: The color to use for white input pixels.
19 :param mid: The color to use for midtone input pixels.
20 :param blackpoint: an int value [0, 255] for the black mapping.
21 :param whitepoint: an int value [0, 255] for the white mapping.
22 :param midpoint: an int value [0, 255] for the midtone mapping.
23 :return: An image.
24 """

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

Fig. 11-3: Grayscale Monarch Caterpillar

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:

Fig. 11-4: Colorized Monarch Caterpillar

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

1 def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)):


2 """
3 Returns a sized and padded version of the image, expanded to fill the
4 requested aspect ratio and size.
5
6 :param image: The image to size and crop.
7 :param size: The requested output size in pixels, given as a
8 (width, height) tuple.
9 :param method: What resampling method to use. Default is
10 :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
11 :param color: The background color of the padded image.
12 :param centering: Control the position of the original image within the
13 padded version.
14
15 (0.5, 0.5) will keep the image centered
16 (0, 0) will keep the image aligned to the top left
17 (1, 1) will keep the image aligned to the bottom
18 right
19 :return: An image.
20 """

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

Fig. 11-5: Flowers

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:

Fig. 11-6: Flowers with Padding

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:

1 def expand(image, border=0, fill=0):


2 """
3 Add border to the image
4
5 :param image: The image to expand.
6 :param border: Border width, in pixels.
7 :param fill: Pixel fill value (a color value). Default is 0 (black).
8 :return: An image.
9 """

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

Fig. 11-7: Flowers with a Border

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

1 def crop(image, border=0):


2 """
3 Remove border from image. The same amount of pixels are removed
4 from all four sides. This function works on all image modes.
5
6 .. seealso:: :py:meth:`~PIL.Image.Image.crop`
7
8 :param image: The image to crop.
9 :param border: The number of pixels to remove.
10 :return: An image.
11 """

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

1 def scale(image, factor, resample=Image.BICUBIC):


2 """
3 Returns a rescaled image by a specific factor given in parameter.
4 A factor greater than 1 expands the image, between 0 and 1 contracts the
5 image.
6
7 :param image: The image to rescale.
8 :param factor: The expansion factor, as a float.
9 :param resample: What resampling method to use. Default is
10 :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
11 :returns: An :py:class:`~PIL.Image.Image` object.
12 """

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!

Equalizing the Histogram


Pillow has an interesting function in the ImageOps module that it calls equalize(). This function
will take in an image and an optional mask. It will use the image’s histogram to equalize the image.
What that means is that it will use the histogram to create a uniform distribution of the grayscale
values in the output.
Here is the function’s definition so you can see how to call it:

1 def equalize(image, mask=None):


2 """
3 Equalize the image histogram. This function applies a non-linear
4 mapping to the input image, to create a uniform
5 distribution of grayscale values in the output image.
6
7 :param image: The image to equalize.
8 :param mask: An optional mask. If given, only the pixels selected by
9 the mask are included in the analysis.
10 :return: An image.
11 """

For this example, you will re-use the flower image from earlier. Here it is again for reference:
Chapter 11 - The ImageOps Module 282

Fig. 11-8: Flowers

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

Fig. 11-9: Flowers with Equalize Applied

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!

Sizing and Cropping an Image


The fit() function is kind of a combination of crop() and scale(). It will resize the passed in image
object using the size you pass in. You can use bleed to control border removal. The centering
parameter tells Pillow where the center of the image is. If you change the “center” of the image, it
will crop differently.
Here is the function’s signature:
Chapter 11 - The ImageOps Module 284

1 def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):


2 """
3 Returns a sized and cropped version of the image, cropped to the
4 requested aspect ratio and size.
5
6 This function was contributed by Kevin Cazabon.
7
8 :param image: The image to size and crop.
9 :param size: The requested output size in pixels, given as a
10 (width, height) tuple.
11 :param method: What resampling method to use. Default is
12 :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
13 :param bleed: Remove a border around the outside of the image from all
14 four edges. The value is a decimal percentage (use 0.01 for
15 one percent). The default value is 0 (no border).
16 Cannot be greater than or equal to 0.5.
17 :param centering: Control the cropping position. Use (0.5, 0.5) for
18 center cropping (e.g. if cropping the width, take 50% off
19 of the left side, and therefore 50% off the right side).
20 (0.0, 0.0) will crop from the top left corner (i.e. if
21 cropping the width, take all of the crop off of the right
22 side, and if cropping the height, take all of it off the
23 bottom). (1.0, 0.0) will crop from the bottom left
24 corner, etc. (i.e. if cropping the width, take all of the
25 crop off the left side, and if cropping the height take
26 none from the top, and therefore all off the bottom).
27 :return: An image.
28 """

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:

Fig. 11-10: Flowers with fit() applied

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

Fig. 11-11: Flowers with fit() and centering=(1.0, 0.0) applied

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:

Fig. 11-12: Ducklings

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:

Fig. 11-13: Flipped Ducklings

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.

Here is the official definition signature:

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

Fig. 11-14: Mirrored Ducklings

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

Fig. 11-15: Inverted Ducklings

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

1 def posterize(image, bits):


2 """
3 Reduce the number of bits for each color channel.
4
5 :param image: The image to posterize.
6 :param bits: The number of bits to keep for each channel (1-8).
7 :return: An image.
8 """

For this example, you will use this new image of a jellyfish:

Fig. 11-16: 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:

Fig. 11-17: Jellyfish with Posterize Applied


Chapter 11 - The ImageOps Module 295

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:

1 def solarize(image, threshold=128):


2 """
3 Invert all pixel values above a threshold.
4
5 :param image: The image to solarize.
6 :param threshold: All pixels above this greyscale level are inverted.
7 :return: An image.
8 """

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:

Fig. 11-18: Jellyfish with Solarize Applied

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!

Transpose Image Using Exif Orientation


The last function that you will learn about from the ImageOps module is called exif_transpose().
It is a very interesting function. It takes in an image and looks up the image’s orientation using the
image’s Exif data.
If the orientation is one (or less), then a copy of the image is returned. An orientation of one means
that the image was taken with the camera in the upright orientation. If the orientation is of another
type, then exif_transpose() will look to see if it has a method to handle that orientation. If it does
have a matching method, exif_transpose() will transpose (or turn) the image so that it is now in
the correct orientation.
Chapter 11 - The ImageOps Module 297

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

Fig. 11-19: Paper Snowman

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.

Creating an ImageOps GUI


The ImageOps module has lots of great functions built into it. You could write a graphical user
interface that wraps them all, but to keep things simple, you’ll only be wrapping the functions that
take in an image and return an image.
When you are finished, your GUI will look like this:
Chapter 11 - The ImageOps Module 300

Fig. 11-20: ImageOps 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

included in this example.


To get started, you will need to create a new file and name it imageops_gui.py. Then add the
following code to it:

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:

1 def apply_effect(image_file_one, effect, image_obj):


2 if os.path.exists(image_file_one):
3 effects[effect](image_file_one, tmp_file)
4 image = Image.open(tmp_file)
5 image.thumbnail((400, 400))
6 bio = io.BytesIO()
7 image.save(bio, format="PNG")
8 image_obj.update(data=bio.getvalue(), size=(400,400))

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

1 def create_row(label, key, file_types):


2 return [
3 sg.Text(label),
4 sg.Input(size=(25, 1), key=key),
5 sg.FileBrowse(file_types=file_types),
6 ]

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:

• Applying Automatic Contrast


• Colorizing Photos
• Padding an Image
• Adding a Border
• Removing a Border
• Scaling an Image
• Equalizing the Histogram
• Sizing and Cropping an Image
• Flipping an Image
• Mirroring an Image
• Inverting an Image
• Posterize an Image
• Solarize an Image
• Transpose Image Using Exif Orientation
• Creating an ImageOps GUI

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

Fig. 12-1: Pink Flowers

The first GUI toolkit you will learn about is Kivy!

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:

Fig. 12-2: Kivy Image Viewer

That looks good!


If you know that Kivy’s Image class will suffice for the types of images that you want to load, you
can get rid of all that complexity in the code above.
Here is a new version of the code without Python’s io module or Pillow:
Chapter 12 - Pillow Integration with GUI Toolkits 312

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

Fig. 12-3: PySimpleGUI Image Viewer

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

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())
37
38 window.close()
39
40
41 if __name__ == "__main__":
42 main()

In this case, you use Python’s io module to convert the PIL image object into something that
sg.Image can understand.

Here is the key chunk of code from this example:

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 python -m pip install PyQt5

You will be focusing on PyQt5 in this section.


The question now is, what if you want to use Pillow to edit or enhance photos in a Qt-based GUI?
How would you display a Pillow image object in PyQt? Pillow has built-in support for converting a
Pillow image into something that PyQt can understand using the ImageQt module.
To find out how to use Pillow with PyQt, create a new file and name it pyqt_pillow_demo.py. Then
enter this code in the file:

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

Fig. 12-4: PyQt Image Viewer

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

Fig. 12-5: Tkinter Image Viewer

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:

Fig. 12-6: wxPython Image Viewer

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:

1 python3 -m pip install numpy

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:

Fig. 13-1: Mike Driscoll

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:

Fig. 13-2: Concatenating RGB Demo

That’s a neat effect!


NumPy can do other things that Pillow doesn’t do easily, like Binarization or denoising. You can
Chapter 13 - Alternatives to Pillow 328

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:

1 python3 -m pip install opencv-python

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.

Face Detection with OpenCV


Now it’s time to see an example of using OpenCV with Python. You will use OpenCV to find the
face in the photo you used earlier this chapter.
Create a new file named face_finder.py and enter the following code:

³⁸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

Fig. 13-3: Finding a Face with OpenCV

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

Fig. 13-4: Finding Eyes with OpenCV

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!

Wand (ImageMagick Bindings)


ImageMagick is an open-source tool that you can use to create, edit, compose, or convert digital
images. It supports over 200 image formats. According to its website, ImageMagick can resize, flip,
mirror, rotate, distort, shear, and transform images, adjust image colors, apply various special effects,
or draw text, lines, polygons, ellipses, and Bézier curves.
Chapter 13 - Alternatives to Pillow 335

For more information about ImageMagick, you should go to their website⁴².


Wand is a Python wrapper around ImageMagick. Wand has many similar features to Pillow and is
the closest thing you could label as an alternative to Pillow. Wand is easy to install with pip:

1 python3 -m pip install 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!

Applying Image Effects with Wand


Wand has several different image effects that are built-in. Here is a full listing:

• 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

Fig. 13-5: Ducklings

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:

Fig. 13-6: Ducklings with Edge Effect

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

Fig. 13-7: Author with the Vignette Effect Applied

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.

Cropping with Wand


Cropping with Wand is similar to how Pillow crops. You can pass in four coordinates (left, top, right,
bottom) or (left, top, width, and height). You will use the duckling’s photo and find out how to crop
the photo down to only the birds.
Create a new file and name it wand_crop.py. Then add the following code:
Chapter 13 - Alternatives to Pillow 340

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:

Fig. 13-8: Cropped Ducklings

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:

• Creating a Batch CLI Application


• Running the Batch Application with Threads
• Modularizing Your Code
• Creating a Batch GUI

You will start by learning how to make your application work via the command line!

Creating a Batch CLI Application


Python provides a built-in library called argparse that you can use to create a command-line
application. You could add an interactive Text User Interface (TUI) using the curses library, but
it’s much more complex. You should check it out if you are feeling adventurous though.
For this application, argparse will do nicely. Your goal is to create an application that can resize
images. You will want to be able to pass the following information to your program:

• The input image folder


• The output folder
• The new width, height or both
• Whether or not to search sub-folders

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

44 print(f"{image_path} converted to {output}")


45 images_converted += 1
46 else:
47 # do not convert image, and inform user
48 print(f"{image_path} size is smaller than new size. Skipping file.")
49 # save (converted) image to destination directory
50 pil_image.save(output)
51 end = time.time()
52 print(f"Converted {images_converted} image(s) in {end-start} seconds.")
53 print(f"Output folder is: {output_dir}")
54
55
56 def validate_directory(path):
57 if not os.path.isdir(path):
58 print(f"{path} is not a directory")
59 return False
60 return True
61
62
63 def main():
64 parser = argparse.ArgumentParser("Image Resizer")
65 parser.add_argument("-i", "--infolder", help="Input folder",
66 required=True, dest="input_dir")
67 parser.add_argument("-r", "--recursive",
68 help="Search sub-folders recursively",
69 dest="recursive", default=False,
70 action="store_true")
71 parser.add_argument("--height",
72 help="The new height the image should be",
73 dest="height", type=int)
74 parser.add_argument("--width",
75 help="The new width the image should be",
76 dest="width", type=int)
77 parser.add_argument("-o", "--out", help="Output folder",
78 required=True, dest="output_dir")
79 args = parser.parse_args()
80
81 if args.width is None and args.height is None:
82 print("You need to specify width or height or both")
83 return
84
85 input_dir = args.input_dir
86 output_dir = args.output_dir
Chapter 14 - Batch Processing 345

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

1 def get_image_paths(search_path, recursive):


2 if "/" != search_path[-1]:
3 search_path += "/"
4 if recursive:
5 search_path += "**/"
6 search_path += "*.png"
7 image_paths = glob.glob(search_path, recursive=recursive)
8 return image_paths

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:

1 def resize_images(image_paths, width, height, output_dir):


2 start = time.time()
3 if width is None:
4 width = height
5 if height is None:
6 height = width
7
8 if not os.path.exists(output_dir):
9 try:
10 os.makedirs(output_dir)
11 except OSError:
12 print(f"Error creating {output_dir}")
13 return
14
15 images_converted = 0
16 for image_path in image_paths:
17 pil_image = Image.open(image_path)
18 im_w, im_h = pil_image.size
19 image_name = os.path.basename(image_path)
20 output = os.path.join(output_dir, image_name)
21 if height < im_h or width < im_w:
Chapter 14 - Batch Processing 347

22 # convert image and inform user


23 pil_image.thumbnail((width, height), Image.ANTIALIAS)
24 print(f"{image_path} converted to {output}")
25 images_converted += 1
26 else:
27 # do not convert image, and inform user
28 print(f"{image_path} size is smaller than new size. Skipping file.")
29 # save (converted) image to destination directory
30 pil_image.save(output)
31 end = time.time()
32 print(f"Converted {images_converted} image(s) in {end-start} seconds.")
33 print(f"Output folder is: {output_dir}")

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

1 def valid te_directory(path):


2 return os.path.isdir(path)

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

Here is an example of how you would run the application:

1 python3 image_resizer_cli.py --infolder /path/to/images --out /path/to/output --widt\


2 h 400

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!

Running the Batch Application with Threads


Python threads are really good for I/O bound tasks, such as reading and writing files. That makes
them a perfect fit for batch processing image files. In this section, you will use concurrent.futures
to create a thread pool to process multiple images at once.
Chapter 14 - Batch Processing 350

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

78 parser = argparse.ArgumentParser("Image Resizer")


79 parser.add_argument("-i", "--infolder", help="Input folder",
80 required=True, dest="input_dir")
81 parser.add_argument("-r", "--recursive",
82 help="Search sub-folders recursively",
83 dest="recursive", default=False,
84 action="store_true")
85 parser.add_argument("--height",
86 help="The new height the image should be",
87 dest="height", type=int)
88 parser.add_argument("--width",
89 help="The new width the image should be",
90 dest="width", type=int)
91 parser.add_argument("-o", "--out", help="Output folder",
92 required=True, dest="output_dir")
93 args = parser.parse_args()
94
95 if args.width is None and args.height is None:
96 print("You need to specify width or height or both")
97 return
98
99 input_dir = args.input_dir
100 output_dir = args.output_dir
101
102 if input_dir == output_dir:
103 print("The output folder cannot be the same as the input")
104 return
105 else:
106 if validate_directory(input_dir):
107 image_paths = get_image_paths(input_dir, args.recursive)
108 resize_images(image_paths, args.width, args.height,
109 output_dir)
110
111
112 if __name__ == "__main__":
113 main()

This code is only 13 lines longer than the previous example.


Let’s find out what has changed:
Chapter 14 - Batch Processing 353

1 from concurrent.futures import ThreadPoolExecutor


2 from concurrent.futures import as_completed

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:

1 def resize_image(image_path, width, height, output_dir):


2 pil_image = Image.open(image_path)
3 im_w, im_h = pil_image.size
4 image_name = os.path.basename(image_path)
5 output = os.path.join(output_dir, image_name)
6 if height < im_h or width < im_w:
7 pil_image.thumbnail((width, height), Image.ANTIALIAS)
8 pil_image.save(output)
9 return f"{image_path} converted to {output}"
10 else:
11 pil_image.save(output)
12 return f"{image_path} copied to {output}."

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:

1 def resize_images(image_paths, width, height, output_dir):


2 start = time.time()
3 if width is None:
4 width = height
5 if height is None:
6 height = width
7
8 if not os.path.exists(output_dir):
9 try:
10 os.makedirs(output_dir)
11 except OSError:
12 print(f"Error creating {output_dir}")
13 return
14
15 images_converted = 0
16 with ThreadPoolExecutor(max_workers=5) as executor:
Chapter 14 - Batch Processing 354

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:

1 with ThreadPoolExecutor(max_workers=5) as executor:


2 futures = [
3 executor.submit(
4 resize_image, image_path, width, height, output_dir,
5 )
6 for image_path in image_paths
7 ]
8 for future in as_completed(futures):
9 result = future.result()
10 if "converted" in result:
11 images_converted += 1
12 print(result)

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

Modularizing Your Code


You might be wondering why you would want to modularize your code? One of the best reasons to
step back and do this is that it allows you to reuse your code. For example, if you take out all the
threading and resizing code and put it into a separate module, then that module can be used by a
GUI. In other words, most of your “worker” code can be used by both the CLI and GUI versions of
your code.
One of the most popular ways to split up code that has a front-end and a back-end is known as
Model-View-Controller. The Model usually refers to the database or other state saving machinery,
which you don’t have or need for this application. The View refers to the CLI or GUI interface. The
Controller is the code that does all the actual work, which in this case is resizing images.
To see how this works, create a file named controller.py and add the following:

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

Fig. 14-1: Image Resizer GUI

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

63 create_row("Output Image Folder", "-OUTPUT_FOLDER-"),


64 [
65 sg.Text("Width"),
66 sg.Input(key="-WIDTH-", enable_events=True, size=(10, 5)),
67 sg.Text("Height"),
68 sg.Input(key="-HEIGHT-", enable_events=True, size=(10, 5))
69 ],
70 [sg.Output(size=(80, 3))],
71 [sg.Button("Resize")],
72 ]
73
74 window = sg.Window("Image Resizer", layout, size=(450, 250))
75
76 while True:
77 event, values = window.read()
78 if event == "Exit" or event == sg.WIN_CLOSED:
79 break
80 if event == "-WIDTH-" and values["-WIDTH-"]:
81 if not values["-WIDTH-"][-1].isdigit():
82 window["-WIDTH-"].update(values["-WIDTH-"][:-1])
83 elif event == "-HEIGHT-" and values["-HEIGHT-"]:
84 if not values["-HEIGHT-"][-1].isdigit():
85 window["-HEIGHT-"].update(values["-HEIGHT-"][:-1])
86 elif event == "Resize":
87 resize(values)
88
89 window.close()
90
91
92 if __name__ == "__main__":
93 main()

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

1 def create_row(label, key):


2 return [
3 sg.Text(label),
4 sg.Input(size=(25, 1), key=key, readonly=True),
5 sg.FolderBrowse(),
6 ]

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

Now you’re ready to learn about verify():

1 def verify(input_folder, output_folder, width, height):


2 if not width and not height:
3 sg.popup("Width or height has to be set")
4 return False
5 if not input_folder:
6 sg.popup("Input folder not set")
7 return False
8 if not output_folder:
9 sg.popup("Output folder not set")
10 return False
11 if input_folder == output_folder:
12 sg.popup("input folder cannot be the same as output")
13 return False
14 return True

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:

• Creating a Batch CLI Application


• Running the Batch Application with Threads
• Modularizing Your Code
• Creating a Batch GUI

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

You might also like