Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

Python PIL/Pillow

A comprehensive image processing library for Python enabling creation, manipulation, and analysis of images.

Python PIL/Pillow Cheatsheet

A comprehensive image processing library for Python enabling creation, manipulation, and analysis of images.

Overview

Pillow is the friendly fork of PIL (Python Imaging Library), providing extensive file format support, efficient internal representation, and powerful image processing capabilities. It's the go-to library for image manipulation tasks in Python.

Output FormatsPillow ProcessingImage SourcesFileURLBytesNumPy ArrayImage ObjectDrawingFiltersTransformsColour OpsJPEGPNGGIFWebPPDFOutput FormatsPillow ProcessingImage SourcesFileURLBytesNumPy ArrayImage ObjectDrawingFiltersTransformsColour OpsJPEGPNGGIFWebPPDF

Installation

pip install Pillow

Image Creation and Manipulation

Key Concepts

  • Image object is the central class representing an image
  • Modes define pixel format (L=greyscale, RGB, RGBA, CMYK, etc.)
  • Size is a tuple of (width, height) in pixels
  • Coordinate system starts at (0, 0) in top-left corner
  • Operations return new images unless specified otherwise

Common Patterns

from PIL import Image

# Open an existing image
img = Image.open('photo.jpg')

# Create a new blank image
img = Image.new('RGB', (800, 600), color='white')

# Get image properties
width, height = img.size
mode = img.mode
format = img.format

# Save image
img.save('output.png')

# Close image (free resources)
img.close()

Examples

Creating Images from Scratch

from PIL import Image

# Create blank images with different modes
rgb_img = Image.new('RGB', (400, 300), color=(255, 128, 0))  # Orange
rgba_img = Image.new('RGBA', (400, 300), color=(255, 0, 0, 128))  # Semi-transparent red
grey_img = Image.new('L', (400, 300), color=128)  # Medium grey

# Create image from pixel data
pixels = [(r, g, b) for r in range(256) for g in range(256) for b in [128]]
img = Image.new('RGB', (256, 256))
img.putdata(pixels)

# Create from bytes
import struct
raw_data = struct.pack('3B' * 4, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0)
img = Image.frombytes('RGB', (2, 2), raw_data)

Basic Transformations

from PIL import Image

img = Image.open('photo.jpg')

# Resize image
resized = img.resize((800, 600))

# Resize maintaining aspect ratio
img.thumbnail((800, 800))  # Modifies in place

# Rotate image
rotated = img.rotate(45)  # 45 degrees counter-clockwise
rotated_expand = img.rotate(45, expand=True)  # Expand canvas to fit

# Flip image
flipped_h = img.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
flipped_v = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)

# Crop image (left, upper, right, lower)
cropped = img.crop((100, 100, 500, 400))

# Paste one image onto another
background = Image.new('RGB', (1000, 1000), 'white')
background.paste(img, (100, 100))

# Paste with transparency mask
foreground = Image.open('overlay.png')
background.paste(foreground, (50, 50), foreground)  # Use alpha as mask

Working with Pixel Data

from PIL import Image

img = Image.open('photo.jpg')

# Get single pixel
pixel = img.getpixel((100, 100))  # Returns (R, G, B)

# Set single pixel
img.putpixel((100, 100), (255, 0, 0))  # Set to red

# Get all pixels as list
pixels = list(img.getdata())

# Modify all pixels
new_pixels = [(r, g, 0) for r, g, b in pixels]  # Remove blue channel
img.putdata(new_pixels)

# Access pixel data efficiently with load()
pixel_access = img.load()
for y in range(img.height):
    for x in range(img.width):
        r, g, b = pixel_access[x, y]
        pixel_access[x, y] = (r, g, b // 2)  # Reduce blue

NumPy Integration

from PIL import Image
import numpy as np

# PIL to NumPy
img = Image.open('photo.jpg')
arr = np.array(img)

# Manipulate with NumPy
arr = arr * 0.5  # Darken
arr = np.clip(arr, 0, 255).astype(np.uint8)

# NumPy to PIL
img_from_array = Image.fromarray(arr)

# Create gradient with NumPy
gradient = np.linspace(0, 255, 256).astype(np.uint8)
gradient = np.tile(gradient, (256, 1))
img = Image.fromarray(gradient, mode='L')

File Format Support

Key Concepts

  • Format detection is automatic when opening files
  • Save format is inferred from filename extension
  • Quality settings vary by format (JPEG uses 1-100, PNG uses compression level)
  • Metadata (EXIF, ICC profiles) can be preserved or stripped
Special FormatsPDFICOEPSAnimated FormatsGIFWebPAPNGLossless FormatsPNGBMPTIFFLossy FormatsJPEGWebPSpecial FormatsPDFICOEPSAnimated FormatsGIFWebPAPNGLossless FormatsPNGBMPTIFFLossy FormatsJPEGWebP

Common Patterns

from PIL import Image

img = Image.open('photo.jpg')

# Save as different formats
img.save('output.png')  # Format from extension
img.save('output.webp', format='WEBP')  # Explicit format

# Save with quality settings
img.save('output.jpg', quality=85, optimise=True)
img.save('output.png', compress_level=6)
img.save('output.webp', quality=80, method=6)

# Get supported formats
print(Image.registered_extensions())

Examples

JPEG Handling

from PIL import Image

img = Image.open('photo.jpg')

# Save with quality control
img.save('high_quality.jpg', quality=95, optimise=True)
img.save('low_quality.jpg', quality=30)

# Save with progressive encoding (better for web)
img.save('progressive.jpg', quality=85, progressive=True)

# Preserve EXIF data
exif_data = img.info.get('exif')
if exif_data:
    img.save('with_exif.jpg', exif=exif_data)

# Save with subsampling control
img.save('output.jpg', quality=85, subsampling=0)  # 4:4:4 (best quality)

PNG Handling

from PIL import Image

# Create with transparency
img = Image.new('RGBA', (400, 300), (255, 0, 0, 128))

# Save with compression
img.save('output.png', compress_level=9)  # Max compression

# Save with optimised palette
rgb_img = Image.open('photo.jpg')
palette_img = rgb_img.quantize(colors=256)
palette_img.save('indexed.png')

# Add metadata
from PIL import PngImagePlugin
meta = PngImagePlugin.PngInfo()
meta.add_text('Author', 'Your Name')
meta.add_text('Description', 'Image description')
img.save('with_metadata.png', pnginfo=meta)

GIF and Animation

from PIL import Image

# Save animated GIF
frames = []
for i in range(10):
    frame = Image.new('RGB', (100, 100), (i * 25, 0, 255 - i * 25))
    frames.append(frame)

frames[0].save(
    'animation.gif',
    save_all=True,
    append_images=frames[1:],
    duration=100,  # ms per frame
    loop=0  # 0 = infinite loop
)

# Read animated GIF
gif = Image.open('animation.gif')
print(f"Frames: {gif.n_frames}")

# Extract all frames
extracted_frames = []
for frame_num in range(gif.n_frames):
    gif.seek(frame_num)
    extracted_frames.append(gif.copy())

# Convert to palette mode for smaller GIF
rgb_img = Image.open('photo.jpg')
gif_img = rgb_img.convert('P', palette=Image.Palette.ADAPTIVE, colors=256)
gif_img.save('converted.gif')

WebP Format

from PIL import Image

img = Image.open('photo.jpg')

# Lossy WebP
img.save('lossy.webp', quality=80)

# Lossless WebP
img.save('lossless.webp', lossless=True)

# Animated WebP
frames = [Image.open(f'frame_{i}.png') for i in range(5)]
frames[0].save(
    'animated.webp',
    save_all=True,
    append_images=frames[1:],
    duration=100,
    loop=0
)

Format Conversion

from PIL import Image

def convert_format(input_path, output_path):
    """Convert image between formats with appropriate settings."""
    img = Image.open(input_path)

    # Handle RGBA to RGB for JPEG
    if output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg'):
        if img.mode == 'RGBA':
            # Create white background
            background = Image.new('RGB', img.size, (255, 255, 255))
            background.paste(img, mask=img.split()[3])
            img = background
        elif img.mode != 'RGB':
            img = img.convert('RGB')

    img.save(output_path)

# Batch conversion
import os
def batch_convert(input_dir, output_dir, output_format):
    os.makedirs(output_dir, exist_ok=True)
    for filename in os.listdir(input_dir):
        if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp')):
            input_path = os.path.join(input_dir, filename)
            output_name = os.path.splitext(filename)[0] + output_format
            output_path = os.path.join(output_dir, output_name)
            convert_format(input_path, output_path)

Drawing and Text Rendering

Key Concepts

  • ImageDraw module provides drawing primitives
  • Coordinates are (x, y) tuples or bounding boxes
  • Colours can be names, hex codes, or RGB/RGBA tuples
  • Fonts require TrueType/OpenType font files for custom text
  • Anti-aliasing is automatic for most operations
ImageDraw ObjectShapesTextLinesRectangleEllipsePolygonArcSimple TextMultilineCustom FontLinePolylineImageDraw ObjectShapesTextLinesRectangleEllipsePolygonArcSimple TextMultilineCustom FontLinePolyline

Common Patterns

from PIL import Image, ImageDraw, ImageFont

# Create drawing context
img = Image.new('RGB', (800, 600), 'white')
draw = ImageDraw.Draw(img)

# Draw shapes
draw.rectangle([10, 10, 100, 100], fill='blue', outline='black')
draw.ellipse([120, 10, 220, 100], fill='red')
draw.line([(0, 0), (800, 600)], fill='green', width=3)

# Draw text
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 36)
draw.text((100, 200), 'Hello World', fill='black', font=font)

Examples

Drawing Shapes

from PIL import Image, ImageDraw

img = Image.new('RGB', (600, 400), 'white')
draw = ImageDraw.Draw(img)

# Rectangle (filled and outline)
draw.rectangle([20, 20, 120, 80], fill='blue', outline='black', width=2)

# Rounded rectangle
draw.rounded_rectangle([140, 20, 260, 80], radius=10, fill='green')

# Ellipse
draw.ellipse([280, 20, 380, 100], fill='red', outline='darkred', width=2)

# Circle (square bounding box)
draw.ellipse([400, 30, 470, 100], fill='orange')

# Polygon
points = [(50, 150), (100, 200), (80, 250), (20, 250), (0, 200)]
draw.polygon(points, fill='purple', outline='black')

# Arc (portion of ellipse)
draw.arc([120, 130, 220, 230], start=0, end=270, fill='blue', width=3)

# Pie slice (filled arc)
draw.pieslice([240, 130, 340, 230], start=45, end=315, fill='cyan', outline='black')

# Chord (arc with straight line connecting ends)
draw.chord([360, 130, 460, 230], start=0, end=180, fill='yellow', outline='black')

img.save('shapes.png')

Drawing Lines and Paths

from PIL import Image, ImageDraw

img = Image.new('RGB', (600, 400), 'white')
draw = ImageDraw.Draw(img)

# Simple line
draw.line([(10, 10), (200, 100)], fill='black', width=2)

# Polyline (connected lines)
points = [(250, 50), (300, 100), (350, 50), (400, 100), (450, 50)]
draw.line(points, fill='blue', width=3)

# Dashed line (manual implementation)
def draw_dashed_line(draw, start, end, dash_length=10, gap_length=5, fill='black', width=1):
    import math
    x1, y1 = start
    x2, y2 = end
    dx = x2 - x1
    dy = y2 - y1
    length = math.sqrt(dx * dx + dy * dy)

    dash_count = int(length / (dash_length + gap_length))

    for i in range(dash_count + 1):
        start_ratio = i * (dash_length + gap_length) / length
        end_ratio = min((i * (dash_length + gap_length) + dash_length) / length, 1)

        sx = x1 + dx * start_ratio
        sy = y1 + dy * start_ratio
        ex = x1 + dx * end_ratio
        ey = y1 + dy * end_ratio

        draw.line([(sx, sy), (ex, ey)], fill=fill, width=width)

draw_dashed_line(draw, (10, 200), (590, 200), fill='red', width=2)

img.save('lines.png')

Text Rendering

from PIL import Image, ImageDraw, ImageFont

img = Image.new('RGB', (800, 600), 'white')
draw = ImageDraw.Draw(img)

# Default font (limited)
draw.text((20, 20), 'Default Font', fill='black')

# TrueType font
try:
    font_path = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'
    font_small = ImageFont.truetype(font_path, 24)
    font_large = ImageFont.truetype(font_path, 48)
    font_bold = ImageFont.truetype(font_path.replace('.ttf', '-Bold.ttf'), 36)
except OSError:
    # Fallback
    font_small = ImageFont.load_default()
    font_large = font_small
    font_bold = font_small

# Draw text with different fonts
draw.text((20, 60), 'Small Text', fill='blue', font=font_small)
draw.text((20, 100), 'Large Text', fill='red', font=font_large)
draw.text((20, 170), 'Bold Text', fill='green', font=font_bold)

# Multiline text
multiline = "Line 1\nLine 2\nLine 3"
draw.multiline_text((20, 250), multiline, fill='black', font=font_small, spacing=10)

# Get text bounding box for positioning
bbox = draw.textbbox((0, 0), 'Centred Text', font=font_large)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]

# Centre text on image
x = (img.width - text_width) // 2
y = 400
draw.text((x, y), 'Centred Text', fill='purple', font=font_large)

# Text with outline/stroke
def draw_text_with_outline(draw, position, text, font, fill, outline, outline_width=2):
    x, y = position
    # Draw outline
    for dx in range(-outline_width, outline_width + 1):
        for dy in range(-outline_width, outline_width + 1):
            if dx != 0 or dy != 0:
                draw.text((x + dx, y + dy), text, font=font, fill=outline)
    # Draw text
    draw.text(position, text, font=font, fill=fill)

draw_text_with_outline(draw, (20, 500), 'Outlined Text', font_large, 'white', 'black', 2)

img.save('text.png')

Creating Charts and Diagrams

from PIL import Image, ImageDraw, ImageFont

def draw_bar_chart(data, title, output_path):
    """Draw a simple bar chart."""
    # Dimensions
    width = 600
    height = 400
    margin = 60
    bar_width = 40
    spacing = 20

    img = Image.new('RGB', (width, height), 'white')
    draw = ImageDraw.Draw(img)

    # Calculate scales
    max_value = max(data.values())
    chart_height = height - 2 * margin
    scale = chart_height / max_value

    # Draw bars
    x = margin
    colours = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6']

    for i, (label, value) in enumerate(data.items()):
        bar_height = value * scale
        y = height - margin - bar_height

        # Draw bar
        colour = colours[i % len(colours)]
        draw.rectangle([x, y, x + bar_width, height - margin], fill=colour)

        # Draw label
        try:
            font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 12)
        except OSError:
            font = ImageFont.load_default()

        bbox = draw.textbbox((0, 0), label, font=font)
        text_width = bbox[2] - bbox[0]
        draw.text((x + (bar_width - text_width) // 2, height - margin + 5),
                  label, fill='black', font=font)

        # Draw value
        draw.text((x + 5, y - 20), str(value), fill='black', font=font)

        x += bar_width + spacing

    # Draw title
    try:
        title_font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 18)
    except OSError:
        title_font = ImageFont.load_default()

    bbox = draw.textbbox((0, 0), title, font=title_font)
    title_width = bbox[2] - bbox[0]
    draw.text(((width - title_width) // 2, 15), title, fill='black', font=title_font)

    img.save(output_path)

# Usage
data = {'Jan': 45, 'Feb': 62, 'Mar': 38, 'Apr': 71, 'May': 55}
draw_bar_chart(data, 'Monthly Sales', 'bar_chart.png')

Image Filtering and Enhancements

Key Concepts

  • ImageFilter module provides predefined and custom filters
  • ImageEnhance module adjusts brightness, contrast, colour, sharpness
  • Filters are applied using img.filter() method
  • Convolution kernels enable custom filter effects
  • Multiple filters can be chained together
ImageEnhanceBrightnessContrastColorSharpnessImageFilterBLURSHARPENEDGE_ENHANCEEMBOSSCONTOUROriginal ImageProcessed ImageImageEnhanceBrightnessContrastColorSharpnessImageFilterBLURSHARPENEDGE_ENHANCEEMBOSSCONTOUROriginal ImageProcessed Image

Common Patterns

from PIL import Image, ImageFilter, ImageEnhance

img = Image.open('photo.jpg')

# Apply predefined filters
blurred = img.filter(ImageFilter.BLUR)
sharpened = img.filter(ImageFilter.SHARPEN)
edges = img.filter(ImageFilter.FIND_EDGES)

# Apply enhancements
enhancer = ImageEnhance.Brightness(img)
brightened = enhancer.enhance(1.5)  # 1.0 = original, >1 = brighter

enhancer = ImageEnhance.Contrast(img)
contrasted = enhancer.enhance(1.3)

Examples

Built-in Filters

from PIL import Image, ImageFilter

img = Image.open('photo.jpg')

# Blur filters
blur = img.filter(ImageFilter.BLUR)
gaussian = img.filter(ImageFilter.GaussianBlur(radius=5))
box_blur = img.filter(ImageFilter.BoxBlur(radius=5))

# Sharpen filters
sharpen = img.filter(ImageFilter.SHARPEN)
unsharp = img.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))

# Edge detection
edges = img.filter(ImageFilter.FIND_EDGES)
contour = img.filter(ImageFilter.CONTOUR)
edge_enhance = img.filter(ImageFilter.EDGE_ENHANCE)
edge_enhance_more = img.filter(ImageFilter.EDGE_ENHANCE_MORE)

# Emboss and detail
emboss = img.filter(ImageFilter.EMBOSS)
detail = img.filter(ImageFilter.DETAIL)
smooth = img.filter(ImageFilter.SMOOTH)
smooth_more = img.filter(ImageFilter.SMOOTH_MORE)

# Median filter (good for noise reduction)
median = img.filter(ImageFilter.MedianFilter(size=5))

# Min/Max filters
min_filter = img.filter(ImageFilter.MinFilter(size=3))
max_filter = img.filter(ImageFilter.MaxFilter(size=3))
mode_filter = img.filter(ImageFilter.ModeFilter(size=3))

Custom Convolution Kernels

from PIL import Image, ImageFilter

img = Image.open('photo.jpg')

# Custom sharpen kernel
sharpen_kernel = ImageFilter.Kernel(
    size=(3, 3),
    kernel=[
        0, -1, 0,
        -1, 5, -1,
        0, -1, 0
    ],
    scale=1,
    offset=0
)
custom_sharpen = img.filter(sharpen_kernel)

# Custom emboss kernel
emboss_kernel = ImageFilter.Kernel(
    size=(3, 3),
    kernel=[
        -2, -1, 0,
        -1, 1, 1,
        0, 1, 2
    ],
    scale=1,
    offset=128
)
custom_emboss = img.filter(emboss_kernel)

# Custom edge detection (Sobel)
sobel_x = ImageFilter.Kernel(
    size=(3, 3),
    kernel=[
        -1, 0, 1,
        -2, 0, 2,
        -1, 0, 1
    ],
    scale=1,
    offset=128
)

# High-pass filter
high_pass = ImageFilter.Kernel(
    size=(3, 3),
    kernel=[
        -1, -1, -1,
        -1, 9, -1,
        -1, -1, -1
    ],
    scale=1,
    offset=0
)

Image Enhancements

from PIL import Image, ImageEnhance

img = Image.open('photo.jpg')

# Brightness adjustment
enhancer = ImageEnhance.Brightness(img)
darker = enhancer.enhance(0.5)    # 50% brightness
brighter = enhancer.enhance(1.5)  # 150% brightness

# Contrast adjustment
enhancer = ImageEnhance.Contrast(img)
low_contrast = enhancer.enhance(0.5)
high_contrast = enhancer.enhance(2.0)

# Colour saturation
enhancer = ImageEnhance.Color(img)
desaturated = enhancer.enhance(0.0)  # Greyscale
saturated = enhancer.enhance(2.0)    # Double saturation

# Sharpness
enhancer = ImageEnhance.Sharpness(img)
soft = enhancer.enhance(0.0)    # Blurred
sharp = enhancer.enhance(2.0)   # Sharpened

# Combine multiple enhancements
def enhance_photo(img, brightness=1.0, contrast=1.0, colour=1.0, sharpness=1.0):
    """Apply multiple enhancements to an image."""
    if brightness != 1.0:
        img = ImageEnhance.Brightness(img).enhance(brightness)
    if contrast != 1.0:
        img = ImageEnhance.Contrast(img).enhance(contrast)
    if colour != 1.0:
        img = ImageEnhance.Color(img).enhance(colour)
    if sharpness != 1.0:
        img = ImageEnhance.Sharpness(img).enhance(sharpness)
    return img

enhanced = enhance_photo(img, brightness=1.1, contrast=1.2, sharpness=1.3)

Advanced Processing

from PIL import Image, ImageFilter, ImageOps

img = Image.open('photo.jpg')

# Auto-enhance operations
equalized = ImageOps.equalize(img)  # Histogram equalisation
auto_contrast = ImageOps.autocontrast(img, cutoff=1)

# Invert colours
inverted = ImageOps.invert(img)

# Posterize (reduce colours)
posterized = ImageOps.posterize(img, bits=3)

# Solarize
solarized = ImageOps.solarize(img, threshold=128)

# Greyscale
greyscale = ImageOps.grayscale(img)

# Apply multiple operations
def create_vintage_effect(img):
    """Create a vintage photo effect."""
    # Reduce saturation
    img = ImageEnhance.Color(img).enhance(0.6)

    # Add slight yellow tint
    r, g, b = img.split()
    r = ImageEnhance.Brightness(r.convert('RGB')).enhance(1.1).split()[0]
    img = Image.merge('RGB', (r, g, b))

    # Reduce contrast
    img = ImageEnhance.Contrast(img).enhance(0.9)

    # Add vignette
    img = add_vignette(img)

    return img

def add_vignette(img, intensity=0.3):
    """Add vignette effect to image."""
    from PIL import ImageDraw

    width, height = img.size
    mask = Image.new('L', (width, height), 255)
    draw = ImageDraw.Draw(mask)

    # Create radial gradient
    for i in range(min(width, height) // 2):
        alpha = int(255 * (1 - (i / (min(width, height) / 2)) ** 2 * intensity))
        draw.ellipse([i, i, width - i, height - i], fill=alpha)

    img = img.copy()
    img.putalpha(255)
    black = Image.new('RGBA', (width, height), (0, 0, 0, 255))

    return Image.composite(img, black, mask).convert('RGB')

Colour Management

Key Concepts

  • Colour modes define how pixels store colour information
  • Channels are individual colour components (R, G, B, A, etc.)
  • Palettes map indices to colours for P mode images
  • ICC profiles enable colour-accurate conversions
  • Alpha channel stores transparency information

Common Patterns

from PIL import Image

img = Image.open('photo.jpg')

# Convert between modes
rgb = img.convert('RGB')
rgba = img.convert('RGBA')
grey = img.convert('L')
palette = img.convert('P')

# Split into channels
r, g, b = rgb.split()

# Merge channels
merged = Image.merge('RGB', (r, g, b))

# Get histogram
histogram = img.histogram()

Examples

Mode Conversions

from PIL import Image

img = Image.open('photo.jpg')

# Common mode conversions
rgb_img = img.convert('RGB')       # RGB colour
rgba_img = img.convert('RGBA')     # RGB with alpha
grey_img = img.convert('L')        # Greyscale
binary_img = img.convert('1')      # Black and white
cmyk_img = img.convert('CMYK')     # Print colours
palette_img = img.convert('P')     # 256-colour palette

# Convert with dithering for binary
binary_dithered = img.convert('1', dither=Image.Dither.FLOYDSTEINBERG)

# Convert to palette with custom colours
palette_img = img.quantize(colors=16, method=Image.Quantize.MEDIANCUT)

# Mode information
print(f"Mode: {img.mode}")
print(f"Bands: {img.getbands()}")  # e.g., ('R', 'G', 'B')

Working with Channels

from PIL import Image, ImageOps

img = Image.open('photo.jpg').convert('RGB')

# Split channels
r, g, b = img.split()

# Save individual channels
r.save('red_channel.png')
g.save('green_channel.png')
b.save('blue_channel.png')

# Swap channels
bgr = Image.merge('RGB', (b, g, r))

# Create channel from scratch
alpha = Image.new('L', img.size, 128)  # Semi-transparent
rgba = Image.merge('RGBA', (r, g, b, alpha))

# Manipulate single channel
r = r.point(lambda x: min(255, x + 50))  # Increase red
modified = Image.merge('RGB', (r, g, b))

# Channel operations
blended = Image.blend(r, g, alpha=0.5)  # Blend two channels

# Create mask from channel
mask = r.point(lambda x: 255 if x > 128 else 0)

Colour Adjustments

from PIL import Image

img = Image.open('photo.jpg').convert('RGB')

# Adjust RGB levels using point()
def adjust_levels(img, r_adjust=0, g_adjust=0, b_adjust=0):
    """Adjust RGB levels individually."""
    r, g, b = img.split()

    r = r.point(lambda x: max(0, min(255, x + r_adjust)))
    g = g.point(lambda x: max(0, min(255, x + g_adjust)))
    b = b.point(lambda x: max(0, min(255, x + b_adjust)))

    return Image.merge('RGB', (r, g, b))

# Warm tone (increase red, decrease blue)
warm = adjust_levels(img, r_adjust=20, b_adjust=-20)

# Cool tone (increase blue, decrease red)
cool = adjust_levels(img, r_adjust=-20, b_adjust=20)

# Gamma correction
def gamma_correct(img, gamma):
    """Apply gamma correction."""
    inv_gamma = 1.0 / gamma
    table = [int((i / 255) ** inv_gamma * 255) for i in range(256)]

    if img.mode == 'RGB':
        return img.point(table * 3)
    elif img.mode == 'L':
        return img.point(table)

gamma_corrected = gamma_correct(img, 2.2)

# Colour temperature adjustment
def adjust_temperature(img, temperature):
    """Adjust colour temperature. Positive = warmer, negative = cooler."""
    r, g, b = img.split()

    if temperature > 0:
        r = r.point(lambda x: min(255, x + temperature))
        b = b.point(lambda x: max(0, x - temperature))
    else:
        r = r.point(lambda x: max(0, x + temperature))
        b = b.point(lambda x: min(255, x - temperature))

    return Image.merge('RGB', (r, g, b))

warmer = adjust_temperature(img, 30)

Working with Palettes and Indexed Colour

from PIL import Image

# Create palette image
img = Image.open('photo.jpg')

# Quantize to limited palette
quantized = img.quantize(colors=256)

# Get palette data
palette = quantized.getpalette()  # Returns list of R, G, B values

# Custom palette
custom_palette = []
for i in range(256):
    custom_palette.extend([i, i, i])  # Greyscale palette

palette_img = Image.new('P', (256, 256))
palette_img.putpalette(custom_palette)

# Apply palette to image
indexed = img.quantize(palette=palette_img)

# Web-safe palette
web_safe = img.quantize(colors=216, method=Image.Quantize.FASTOCTREE)

Transparency and Alpha

from PIL import Image

# Add alpha channel
img = Image.open('photo.jpg').convert('RGBA')

# Create circular mask
width, height = img.size
mask = Image.new('L', (width, height), 0)
from PIL import ImageDraw
draw = ImageDraw.Draw(mask)
draw.ellipse([0, 0, width, height], fill=255)

# Apply mask as alpha
img.putalpha(mask)

# Modify existing alpha
r, g, b, a = img.split()
a = a.point(lambda x: x // 2)  # Make 50% transparent
img = Image.merge('RGBA', (r, g, b, a))

# Remove transparency (composite onto background)
background = Image.new('RGB', img.size, 'white')
background.paste(img, mask=img.split()[3])

# Create gradient alpha
def create_gradient_alpha(size, direction='horizontal'):
    """Create gradient transparency."""
    width, height = size
    alpha = Image.new('L', size)

    for x in range(width):
        for y in range(height):
            if direction == 'horizontal':
                value = int(255 * x / width)
            else:
                value = int(255 * y / height)
            alpha.putpixel((x, y), value)

    return alpha

gradient = create_gradient_alpha(img.size, 'horizontal')
img.putalpha(gradient)

Common Use Cases

Thumbnails

from PIL import Image
import os

def create_thumbnail(input_path, output_path, size=(200, 200)):
    """Create a thumbnail preserving aspect ratio."""
    with Image.open(input_path) as img:
        img.thumbnail(size, Image.Resampling.LANCZOS)
        img.save(output_path)

def create_square_thumbnail(input_path, output_path, size=200):
    """Create a square thumbnail with centre crop."""
    with Image.open(input_path) as img:
        # Calculate crop dimensions
        width, height = img.size
        min_dim = min(width, height)

        left = (width - min_dim) // 2
        top = (height - min_dim) // 2
        right = left + min_dim
        bottom = top + min_dim

        # Crop to square and resize
        img = img.crop((left, top, right, bottom))
        img = img.resize((size, size), Image.Resampling.LANCZOS)
        img.save(output_path)

def batch_thumbnails(input_dir, output_dir, size=(200, 200)):
    """Create thumbnails for all images in a directory."""
    os.makedirs(output_dir, exist_ok=True)

    for filename in os.listdir(input_dir):
        if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
            input_path = os.path.join(input_dir, filename)
            output_path = os.path.join(output_dir, f'thumb_{filename}')
            create_thumbnail(input_path, output_path, size)
            print(f'Created thumbnail: {output_path}')

Watermarks

from PIL import Image, ImageDraw, ImageFont, ImageEnhance

def add_text_watermark(img_path, text, output_path, opacity=0.5):
    """Add text watermark to image."""
    img = Image.open(img_path).convert('RGBA')

    # Create watermark layer
    watermark = Image.new('RGBA', img.size, (255, 255, 255, 0))
    draw = ImageDraw.Draw(watermark)

    # Get font
    try:
        font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
                                  img.width // 20)
    except OSError:
        font = ImageFont.load_default()

    # Calculate text position (bottom right)
    bbox = draw.textbbox((0, 0), text, font=font)
    text_width = bbox[2] - bbox[0]
    text_height = bbox[3] - bbox[1]

    x = img.width - text_width - 20
    y = img.height - text_height - 20

    # Draw text with opacity
    alpha = int(255 * opacity)
    draw.text((x, y), text, fill=(255, 255, 255, alpha), font=font)

    # Composite
    result = Image.alpha_composite(img, watermark)
    result.convert('RGB').save(output_path)

def add_image_watermark(img_path, watermark_path, output_path, position='bottom-right', opacity=0.5, scale=0.2):
    """Add image watermark to photo."""
    img = Image.open(img_path).convert('RGBA')
    watermark = Image.open(watermark_path).convert('RGBA')

    # Scale watermark
    new_width = int(img.width * scale)
    ratio = new_width / watermark.width
    new_height = int(watermark.height * ratio)
    watermark = watermark.resize((new_width, new_height), Image.Resampling.LANCZOS)

    # Adjust opacity
    r, g, b, a = watermark.split()
    a = a.point(lambda x: int(x * opacity))
    watermark = Image.merge('RGBA', (r, g, b, a))

    # Calculate position
    margin = 20
    positions = {
        'top-left': (margin, margin),
        'top-right': (img.width - new_width - margin, margin),
        'bottom-left': (margin, img.height - new_height - margin),
        'bottom-right': (img.width - new_width - margin, img.height - new_height - margin),
        'centre': ((img.width - new_width) // 2, (img.height - new_height) // 2)
    }

    x, y = positions.get(position, positions['bottom-right'])

    # Paste watermark
    img.paste(watermark, (x, y), watermark)
    img.convert('RGB').save(output_path)

def add_tiled_watermark(img_path, text, output_path, opacity=0.1):
    """Add tiled (repeated) text watermark."""
    img = Image.open(img_path).convert('RGBA')

    # Create watermark layer
    watermark = Image.new('RGBA', img.size, (255, 255, 255, 0))
    draw = ImageDraw.Draw(watermark)

    try:
        font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 30)
    except OSError:
        font = ImageFont.load_default()

    bbox = draw.textbbox((0, 0), text, font=font)
    text_width = bbox[2] - bbox[0]
    text_height = bbox[3] - bbox[1]

    alpha = int(255 * opacity)

    # Tile the watermark
    y = 0
    row = 0
    while y < img.height:
        x = -text_width // 2 if row % 2 else 0
        while x < img.width:
            draw.text((x, y), text, fill=(128, 128, 128, alpha), font=font)
            x += text_width + 50
        y += text_height + 50
        row += 1

    result = Image.alpha_composite(img, watermark)
    result.convert('RGB').save(output_path)

Image Comparison and Analysis

from PIL import Image, ImageChops, ImageStat
import math

def compare_images(img1_path, img2_path):
    """Compare two images and return similarity score."""
    img1 = Image.open(img1_path).convert('RGB')
    img2 = Image.open(img2_path).convert('RGB')

    # Resize if different sizes
    if img1.size != img2.size:
        img2 = img2.resize(img1.size)

    # Calculate difference
    diff = ImageChops.difference(img1, img2)

    # Calculate RMS difference
    stat = ImageStat.Stat(diff)
    rms = math.sqrt(sum([s ** 2 for s in stat.mean]) / 3)

    # Convert to similarity (0-100%)
    similarity = max(0, 100 - rms)

    return similarity

def get_image_stats(img_path):
    """Get statistical information about an image."""
    img = Image.open(img_path)
    stat = ImageStat.Stat(img)

    return {
        'size': img.size,
        'mode': img.mode,
        'mean': stat.mean,
        'median': stat.median,
        'stddev': stat.stddev,
        'extrema': stat.extrema
    }

def find_dominant_colours(img_path, num_colours=5):
    """Find dominant colours in an image."""
    img = Image.open(img_path).convert('RGB')

    # Reduce to palette
    small = img.resize((100, 100))
    result = small.quantize(colors=num_colours)

    # Get palette
    palette = result.getpalette()
    colours = []

    for i in range(num_colours):
        r = palette[i * 3]
        g = palette[i * 3 + 1]
        b = palette[i * 3 + 2]
        colours.append((r, g, b))

    return colours

Batch Processing

from PIL import Image
import os
from concurrent.futures import ThreadPoolExecutor

def process_image(args):
    """Process a single image."""
    input_path, output_path, operations = args

    try:
        img = Image.open(input_path)

        for op, params in operations:
            if op == 'resize':
                img.thumbnail(params, Image.Resampling.LANCZOS)
            elif op == 'convert':
                img = img.convert(params)
            elif op == 'rotate':
                img = img.rotate(params, expand=True)

        img.save(output_path)
        return True, input_path
    except Exception as e:
        return False, f"{input_path}: {e}"

def batch_process(input_dir, output_dir, operations, max_workers=4):
    """Batch process images in parallel."""
    os.makedirs(output_dir, exist_ok=True)

    tasks = []
    for filename in os.listdir(input_dir):
        if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
            input_path = os.path.join(input_dir, filename)
            output_path = os.path.join(output_dir, filename)
            tasks.append((input_path, output_path, operations))

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        results = list(executor.map(process_image, tasks))

    success = sum(1 for r in results if r[0])
    print(f"Processed {success}/{len(tasks)} images")

    for success, msg in results:
        if not success:
            print(f"Error: {msg}")

# Usage
operations = [
    ('resize', (800, 800)),
    ('convert', 'RGB')
]
batch_process('input_images', 'output_images', operations)

Performance Considerations

Memory Management

from PIL import Image

# Use context manager to ensure cleanup
with Image.open('large_image.jpg') as img:
    # Process image
    thumbnail = img.copy()
    thumbnail.thumbnail((200, 200))
    thumbnail.save('thumbnail.jpg')
# Image automatically closed

# Manually close when not using context manager
img = Image.open('large_image.jpg')
# ... process ...
img.close()

# Load image without keeping file handle open
img = Image.open('image.jpg')
img.load()  # Load pixel data into memory
# File can now be closed/moved

# For very large images, use draft mode
img = Image.open('huge_image.jpg')
img.draft('RGB', (1000, 1000))  # Load at reduced resolution

Processing Speed Optimisation

from PIL import Image

# Use fastest resampling for previews
preview = img.resize((100, 100), Image.Resampling.NEAREST)  # Fastest

# Use LANCZOS for quality output
final = img.resize((800, 600), Image.Resampling.LANCZOS)  # Highest quality

# Resampling methods (fastest to slowest):
# NEAREST < BOX < BILINEAR < HAMMING < BICUBIC < LANCZOS

# For batch operations, reuse objects
from PIL import ImageDraw, ImageFont

font = ImageFont.truetype('font.ttf', 24)  # Load once

def add_watermark(img_path, output_path):
    img = Image.open(img_path)
    draw = ImageDraw.Draw(img)
    draw.text((10, 10), 'Watermark', font=font)  # Reuse font
    img.save(output_path)
    img.close()

# Use NumPy for pixel-level operations (much faster)
import numpy as np

img = Image.open('image.jpg')
arr = np.array(img)

# NumPy operations are vectorised and fast
arr = arr * 0.5  # Darken all pixels
arr = np.clip(arr, 0, 255).astype(np.uint8)

img = Image.fromarray(arr)

Efficient I/O

from PIL import Image
import io

# Save to bytes buffer (for web/network)
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=85)
image_bytes = buffer.getvalue()

# Load from bytes
img = Image.open(io.BytesIO(image_bytes))

# Progressive JPEG for faster perceived loading
img.save('progressive.jpg', progressive=True, quality=85)

# Optimise PNG
img.save('optimised.png', optimise=True)

# Use appropriate quality settings
# JPEG: 85 is good balance of size/quality
# WebP: 80-85 for photos, use lossless for graphics
img.save('output.jpg', quality=85, optimise=True)
img.save('output.webp', quality=80, method=4)  # method 0-6 (slower = smaller)

Lazy Loading and Deferred Operations

from PIL import Image

# Pillow uses lazy loading - image data loaded on first access
img = Image.open('large.jpg')  # Only reads header
print(img.size)  # Still no pixel data loaded
pixels = img.load()  # NOW pixel data is loaded

# Operations can be chained efficiently
img = Image.open('photo.jpg')
img = img.crop((0, 0, 500, 500))     # Deferred
img = img.transpose(Image.Transpose.FLIP_LEFT_RIGHT)  # Deferred
img = img.resize((100, 100))         # May trigger evaluation
img.save('result.jpg')               # Final evaluation

Quick Reference

Operation Code
Open image img = Image.open('file.jpg')
Create blank img = Image.new('RGB', (800, 600), 'white')
Save image img.save('output.png')
Resize img.resize((800, 600), Image.Resampling.LANCZOS)
Thumbnail img.thumbnail((200, 200))
Crop img.crop((left, top, right, bottom))
Rotate img.rotate(45, expand=True)
Flip horizontal img.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
Convert mode img.convert('RGB')
Get size width, height = img.size
Get pixel pixel = img.getpixel((x, y))
Set pixel img.putpixel((x, y), (r, g, b))
Paste image img.paste(overlay, (x, y), mask)
Apply filter img.filter(ImageFilter.BLUR)
Adjust brightness ImageEnhance.Brightness(img).enhance(1.5)
Draw rectangle draw.rectangle([x1, y1, x2, y2], fill='blue')
Draw text draw.text((x, y), 'text', fill='black', font=font)
Split channels r, g, b = img.split()
Merge channels img = Image.merge('RGB', (r, g, b))
NumPy array arr = np.array(img)
From NumPy img = Image.fromarray(arr)
Save JPEG quality img.save('out.jpg', quality=85)
Animated GIF frames[0].save('anim.gif', save_all=True, append_images=frames[1:])

Common Issues and Solutions

Issue: Cannot Write Mode RGBA as JPEG

Problem: OSError: cannot write mode RGBA as JPEG

Solution:

from PIL import Image

img = Image.open('image.png')  # RGBA image

# Convert to RGB with white background
if img.mode == 'RGBA':
    background = Image.new('RGB', img.size, (255, 255, 255))
    background.paste(img, mask=img.split()[3])
    img = background

img.save('output.jpg')

Issue: Image File is Truncated

Problem: OSError: image file is truncated

Solution:

from PIL import Image, ImageFile

# Allow loading truncated images
ImageFile.LOAD_TRUNCATED_IMAGES = True

img = Image.open('truncated.jpg')

Issue: DecompressionBombError

Problem: PIL.Image.DecompressionBombError: Image size exceeds limit

Solution:

from PIL import Image

# Increase or disable pixel limit
Image.MAX_IMAGE_PIXELS = 500000000  # 500 million pixels

# Or disable completely (use with caution)
Image.MAX_IMAGE_PIXELS = None

img = Image.open('huge_image.jpg')

Issue: Cannot Identify Image File

Problem: PIL.UnidentifiedImageError: cannot identify image file

Solution:

from PIL import Image
import io

# Verify file exists and is readable
import os
if not os.path.exists('image.jpg'):
    print("File not found")

# Check file format manually
with open('image.jpg', 'rb') as f:
    header = f.read(10)
    print(header)  # Check magic bytes

# Try opening from bytes
with open('image.jpg', 'rb') as f:
    img = Image.open(io.BytesIO(f.read()))

# Verify image format
print(Image.registered_extensions())

Issue: Poor Quality After Resize

Problem: Resized images look pixelated or blurry

Solution:

from PIL import Image

img = Image.open('photo.jpg')

# Use high-quality resampling
resized = img.resize((800, 600), Image.Resampling.LANCZOS)

# For thumbnails, preserve sharpness
img.thumbnail((200, 200), Image.Resampling.LANCZOS)

# Apply sharpening after resize
from PIL import ImageFilter
resized = resized.filter(ImageFilter.UnsharpMask(radius=1, percent=50, threshold=3))

Issue: Colours Look Wrong After Save

Problem: Colours appear different after saving

Solution:

from PIL import Image

img = Image.open('photo.jpg')

# Preserve ICC profile
icc_profile = img.info.get('icc_profile')
if icc_profile:
    img.save('output.jpg', icc_profile=icc_profile)

# For CMYK to RGB conversion
if img.mode == 'CMYK':
    img = img.convert('RGB')

# Check colour mode
print(f"Mode: {img.mode}")  # Should be RGB for JPEG

Issue: Memory Error with Large Images

Problem: MemoryError when processing large images

Solution:

from PIL import Image

# Process in tiles
def process_large_image(input_path, output_path, tile_size=1024):
    img = Image.open(input_path)
    width, height = img.size

    result = Image.new(img.mode, img.size)

    for y in range(0, height, tile_size):
        for x in range(0, width, tile_size):
            box = (x, y, min(x + tile_size, width), min(y + tile_size, height))
            tile = img.crop(box)
            # Process tile
            processed = tile.filter(ImageFilter.BLUR)
            result.paste(processed, box)

    result.save(output_path)

# Use draft mode to load at reduced resolution
img = Image.open('huge.jpg')
img.draft('RGB', (2000, 2000))
img.load()

Issue: Font Not Found

Problem: OSError: cannot open resource

Solution:

from PIL import ImageFont

# Use system fonts
font_paths = [
    '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',  # Linux
    '/System/Library/Fonts/Helvetica.ttc',               # macOS
    'C:/Windows/Fonts/arial.ttf',                        # Windows
]

font = None
for path in font_paths:
    try:
        font = ImageFont.truetype(path, 24)
        break
    except OSError:
        continue

if font is None:
    font = ImageFont.load_default()  # Fallback

# Or use pillow-fonts package
# pip install pillow-fonts
from PIL import ImageFont
font = ImageFont.truetype('DejaVuSans.ttf', 24)

Issue: Transparency Lost When Saving

Problem: Transparent areas become black or white

Solution:

from PIL import Image

img = Image.open('transparent.png')

# Save as PNG to preserve transparency
img.save('output.png')

# Convert RGBA to RGB with custom background
if img.mode == 'RGBA':
    background = Image.new('RGB', img.size, (200, 200, 200))  # Grey background
    background.paste(img, mask=img.split()[3])
    background.save('output.jpg')

Related Topics

  • NumPy - Efficient numerical operations on image arrays
  • OpenCV - Advanced computer vision and image processing
  • scikit-image - Scientific image analysis and processing
  • ImageMagick - Command-line image manipulation tool
  • Cairo - Vector graphics and PDF generation
  • Matplotlib - Data visualisation and figure generation