Skip to content

Pillow

The standard image library for Python. Pillow (PIL fork) gives you straightforward image I/O, resizing, cropping, color conversion, and drawing — simpler and more Pythonic than OpenCV for most non-CV tasks. Every ML training pipeline starts with Pillow to load and resize images.

Table of Contents

Installation / Setup

pip install Pillow                    # or: uv add Pillow
from PIL import Image, ImageFilter, ImageDraw, ImageFont, ImageEnhance
import numpy as np

Pillow images are RGB by default (unlike OpenCV's BGR).

Core Syntax

Image loading and saving

from PIL import Image

img = Image.open("photo.jpg")           # RGB (or RGBA for PNG)
img_gray = Image.open("photo.jpg").convert("L")
img.save("output.png")
img.save("output.jpg", quality=95)
Mode Description
"L" Grayscale (8-bit)
"RGB" 3-channel color
"RGBA" RGB + alpha transparency
"CMYK" Print color space
"1" Binary (black/white)

Image info

w, h = img.size        # (width, height)
mode = img.mode
fmt = img.format

Resizing

# fixed size
resized = img.resize((640, 480), Image.LANCZOS)

# keep aspect ratio
w, h = img.size
ratio = 512 / max(w, h)
resized = img.resize((int(w * ratio), int(h * ratio)), Image.LANCZOS)

# thumbnail modifies in place (max dimension)
img_copy = img.copy()
img_copy.thumbnail((224, 224), Image.LANCZOS)
Resampling filter Use
Image.NEAREST Fast, pixelated
Image.BILINEAR General purpose
Image.BICUBIC Better quality for enlarging
Image.LANCZOS Best quality (default for resizing)

Cropping

cropped = img.crop((left, top, right, bottom))   # tuple: (left, upper, right, lower)
center = img.crop((w//4, h//4, 3*w//4, 3*h//4))

Rotating and flipping

rotated = img.rotate(45, expand=True, fillcolor=(255, 255, 255))
flipped_h = img.transpose(Image.FLIP_LEFT_RIGHT)
flipped_v = img.transpose(Image.FLIP_TOP_BOTTOM)

Color spaces

gray = img.convert("L")
rgb = img.convert("RGB")
rgba = img.convert("RGBA")

# to NumPy array (RGB)
arr = np.array(img)

# back to PIL from NumPy
img = Image.fromarray(arr)

Common Operations

Filtering

blurred = img.filter(ImageFilter.GaussianBlur(radius=3))
sharpened = img.filter(ImageFilter.SHARPEN)
edges = img.filter(ImageFilter.FIND_EDGES)
median = img.filter(ImageFilter.MedianFilter(size=3))
Filter Effect
ImageFilter.GaussianBlur(radius) Smooth / denoise
ImageFilter.SHARPEN Enhance edges
ImageFilter.FIND_EDGES Edge detection
ImageFilter.EDGE_ENHANCE Subtle edge enhancement
ImageFilter.SMOOTH Gentle smoothing
ImageFilter.MedianFilter(size) Salt-and-pepper noise removal

Enhancement

from PIL import ImageEnhance

enhancer = ImageEnhance.Brightness(img)
bright = enhancer.enhance(1.3)       # 1.0 = original, >1 brighter

enhancer = ImageEnhance.Contrast(img)
contrast = enhancer.enhance(1.5)

enhancer = ImageEnhance.Color(img)
saturated = enhancer.enhance(1.2)

enhancer = ImageEnhance.Sharpness(img)
sharp = enhancer.enhance(2.0)

Drawing and annotations

from PIL import ImageDraw, ImageFont

draw = ImageDraw.Draw(img)

# shapes
draw.rectangle([x1, y1, x2, y2], outline="red", width=2)
draw.ellipse([x1, y1, x2, y2], fill="blue")
draw.line([(x1, y1), (x2, y2)], fill="green", width=3)

# text
try:
    font = ImageFont.truetype("arial.ttf", size=20)
except IOError:
    font = ImageFont.load_default()
draw.text((50, 50), "Hello World", fill="white", font=font)

Compositing

# paste one image onto another
background.paste(foreground, (x, y), mask=foreground)   # mask for transparency

# resize and paste
fg = foreground.resize((100, 100))
background.paste(fg, (10, 10))

Exif and metadata

exif = img.getexif()
print(dict(exif))

# strip metadata for smaller files
img.save("clean.jpg", exif=b"")

Practical Examples

Example 1 — Data augmentation pipeline for training

from PIL import Image, ImageEnhance, ImageFilter
import random

def augment(img, seed=None):
    if seed: random.seed(seed)
    img = img.transpose(random.choice([Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM]))
    img = img.rotate(random.uniform(-15, 15), fillcolor=(128, 128, 128))
    img = ImageEnhance.Brightness(img).enhance(random.uniform(0.8, 1.2))
    img = ImageEnhance.Contrast(img).enhance(random.uniform(0.8, 1.2))
    if random.random() > 0.5:
        img = img.filter(ImageFilter.GaussianBlur(radius=random.uniform(0, 1)))
    return img.resize((224, 224), Image.LANCZOS)

Example 2 — Create a contact sheet / thumbnail grid

from PIL import Image
import glob, math

def contact_sheet(paths, cols=4, thumb_size=(200, 200)):
    imgs = [Image.open(p).resize(thumb_size, Image.LANCZOS) for p in paths[:cols * 6]]
    rows = math.ceil(len(imgs) / cols)
    sheet = Image.new("RGB", (cols * thumb_size[0], rows * thumb_size[1]), "white")
    for i, img in enumerate(imgs):
        x = (i % cols) * thumb_size[0]
        y = (i // cols) * thumb_size[1]
        sheet.paste(img, (x, y))
    return sheet

paths = glob.glob("photos/*.jpg")
sheet = contact_sheet(paths)
sheet.save("contact_sheet.jpg")

Example 3 — Convert images to a standard size and format

from PIL import Image
import glob, os

for path in glob.glob("raw_images/*.jpg"):
    img = Image.open(path).convert("RGB")
    img.thumbnail((800, 800), Image.LANCZOS)
    out = os.path.join("processed", os.path.splitext(os.path.basename(path))[0] + ".png")
    img.save(out, "PNG")

Common Patterns

Pattern: convert to NumPy, process, convert back

import numpy as np
from PIL import Image

img = Image.open("photo.jpg")
arr = np.array(img)                     # (H, W, 3) uint8 RGB

# custom processing
arr[arr > 200] = 255                    # threshold

result = Image.fromarray(arr)
result.save("result.jpg")

Pattern: ensure consistent input size for ML

def load_and_resize(path, size=(224, 224)):
    return Image.open(path).convert("RGB").resize(size, Image.LANCZOS)

Pattern: tile images into a montage

from PIL import Image
import glob

imgs = [Image.open(p).resize((128, 128)) for p in glob.glob("data/*.png")[:16]]
cols = 4
rows = (len(imgs) + cols - 1) // cols
montage = Image.new("RGB", (cols * 128, rows * 128), "gray")
for i, img in enumerate(imgs):
    montage.paste(img, ((i % cols) * 128, (i // cols) * 128))
montage.save("montage.jpg")

Best Practices

  • Use Image.LANCZOS for resizing — best quality; Image.NEAREST only for masks/labels.
  • Pillow uses RGB (unlike OpenCV's BGR) — match the convention of your pipeline.
  • Always convert("RGB") before saving as JPEG — RGBA JPEGs fail silently.
  • Image.open() is lazy — it reads header only; actual pixel data loaded on access.
  • For large batch processing, keep images in a list then save one-by-one to manage memory.
  • Use thumbnail() for in-place resizing (modifies the image object), resize() returns a new image.
  • Strip EXIF orientation before training — ImageOps.exif_transpose() fixes rotated phones photos.

Common Mistakes

Mistake Fix
RGBA image fails to save as JPEG convert("RGB") before saving
Image.open() is lazy — file handle stays open Call .load() or keep a reference until save
Forgetting expand=True on rotate — corners get cropped rotate(angle, expand=True) keeps full image
JPEG quality too low — artifacts save(path, quality=95) (default is 75)
Mixing PIL and OpenCV arrays — RGB/BGR confusion PIL is RGB; OpenCV is BGR — convert before passing
thumbnail modifies in place Don't reassign: call img.thumbnail(...) not img = img.thumbnail(...)
Memory blow-up with huge images Resize or load() before heavy processing

Troubleshooting

Symptom Cause / Fix
OSError: cannot identify image file File is corrupted, not an image, or wrong extension
PIL.UnidentifiedImageError Try Image.open(path).verify() or install libjpeg/libpng
Transposed/oriented JPEG from phone Use ImageOps.exif_transpose(img)
Memory errors on large images img.load() forces loading; process in tiles
FileNotFoundError for font Install the font file or use ImageFont.load_default()
  • opencv.md — advanced CV: contours, video, webcam
  • torchvision — ML transforms and pretrained models
  • yolo — object detection
  • NumPy — array operations
  • matplotlib — displaying images in notebooks
  • Python — loops, functions, file I/O

Official Documentation

Last Updated

2026-09-06 — Initial version.