Home / Library / PIL / Handbook / Tutorial

A short tutorial

The same ground the old handbook tutorial covered, written for Pillow and Python 3.

Opening an image

from PIL import Image

im = Image.open("photo.jpg")
print(im.format, im.size, im.mode)

That prints something like JPEG (1600, 1200) RGB. The file has been opened and its header read, but the pixels have not been loaded yet. Pillow waits until something needs them.

Making a thumbnail

im = Image.open("photo.jpg")
im.thumbnail((200, 200))
im.save("thumb.png")

thumbnail keeps the aspect ratio, never enlarges, and changes the image in place rather than returning a new one. If you want an exact size regardless of shape, use resize instead.

Cropping and pasting

box = (100, 100, 400, 400)
piece = im.crop(box)

piece = piece.transpose(Image.Transpose.ROTATE_180)
im.paste(piece, box)

The box is (left, upper, right, lower), with the origin at the top left corner. The right and lower edges are not included, so that box is exactly 300 by 300 pixels.

Bands

r, g, b = im.split()
swapped = Image.merge("RGB", (b, g, r))

Each band is an image in its own right, in mode L.

Converting between modes

grey = im.convert("L")

Mode is worth understanding early, because most confusing errors come from it: saving RGBA as JPEG, filtering a palette image, or comparing images that do not match. The mode viewer shows what each one does.

Resizing and rotating

small = im.resize((320, 240), Image.Resampling.LANCZOS)
turned = im.rotate(45, expand=True)

Without expand=True the corners are cut off. Which resampling filter you pass matters, and the comparison tool shows why.

Filters and enhancement

from PIL import ImageFilter, ImageEnhance

soft = im.filter(ImageFilter.GaussianBlur(2))
sharp = im.filter(ImageFilter.UnsharpMask(radius=2, percent=120))
brighter = ImageEnhance.Brightness(im).enhance(1.3)

Working through a folder

import pathlib
from PIL import Image, ImageOps

for path in pathlib.Path("photos").glob("*.jpg"):
    im = Image.open(path)
    im = ImageOps.exif_transpose(im)
    im.thumbnail((1200, 1200), Image.Resampling.LANCZOS)
    im.save(pathlib.Path("web") / path.name, quality=85, optimize=True)

The exif_transpose call is the one people leave out and regret. Phone cameras record orientation as a tag rather than rotating the pixels, so without it a proportion of your output is sideways.

Reading pixels

print(im.getpixel((10, 10)))

px = im.load()
px[10, 10] = (255, 0, 0)

getpixel is fine for a handful of values. For anything more, load() is much faster, and point() or numpy faster still.

Next