Skip to content

Pillow (PIL)

Pillow is the modern, friendly fork of the Python Imaging Library (PIL). It provides extensive file format support, efficient internal representation, and powerful image processing capabilities. Pillow is the de facto standard for image manipulation in Python.

Installation

# Basic installation
pip install Pillow

# With optional dependencies
pip install Pillow[complete]  # Includes all optional dependencies

# With specific extras
pip install "Pillow[extra]"   # WebP, PDF, and other format support

# Development version
pip install git+https://github.com/python-pillow/Pillow.git

# System dependencies (Ubuntu/Debian)
sudo apt-get install libjpeg-dev zlib1g-dev libtiff-dev libfreetype6-dev

Basic Setup

from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
from PIL import ImageOps, ImageChops, ImageStat, ImageColor
import os
import io
import numpy as np

# Check Pillow version and features
print(f"Pillow version: {Image.__version__}")

# Check supported formats
print("Supported formats:")
print("Read:", ", ".join(sorted(Image.registered_extensions())))
print("Write:", ", ".join(sorted(Image.SAVE.keys())))

Core Functionality

Loading and Saving Images

# Load images
img = Image.open("path/to/image.jpg")
img = Image.open("path/to/image.png")

# Load from URL
import requests
from io import BytesIO
response = requests.get("https://example.com/image.jpg")
img = Image.open(BytesIO(response.content))

# Load from bytes
with open("image.jpg", "rb") as f:
    img = Image.open(BytesIO(f.read()))

# Basic image information
print(f"Format: {img.format}")        # JPEG, PNG, etc.
print(f"Mode: {img.mode}")           # RGB, RGBA, L, etc.
print(f"Size: {img.size}")           # (width, height)
print(f"Info: {img.info}")           # Metadata dictionary

# Save images
img.save("output.jpg")                # Auto-detect format from extension
img.save("output.png", "PNG")         # Explicit format
img.save("output.jpg", quality=95)    # JPEG with quality setting

# Save with optimization
img.save("optimized.jpg", optimize=True, quality=85)
img.save("progressive.jpg", progressive=True, quality=90)

# Save to bytes
buffer = io.BytesIO()
img.save(buffer, format='JPEG')
image_bytes = buffer.getvalue()

Image Modes and Conversions

# Common image modes
# L: Grayscale (8-bit)
# RGB: Color (3x8-bit)
# RGBA: Color with transparency (4x8-bit)
# CMYK: Color for printing (4x8-bit)
# P: Palette mode (8-bit mapped)

# Mode conversions
rgb_img = img.convert('RGB')          # Convert to RGB
gray_img = img.convert('L')           # Convert to grayscale
rgba_img = img.convert('RGBA')        # Add alpha channel

# Grayscale with custom weights
def rgb_to_gray_custom(img, weights=(0.299, 0.587, 0.114)):
    """Convert RGB to grayscale with custom weights"""
    r, g, b = img.split()
    gray = Image.eval(r, lambda x: int(x * weights[0])) 
    gray.paste(Image.eval(g, lambda x: int(x * weights[1])), mask=None)
    gray.paste(Image.eval(b, lambda x: int(x * weights[2])), mask=None)
    return gray

# Color quantization (reduce colors)
quantized = img.quantize(colors=256)   # Reduce to 256 colors
quantized = img.quantize(colors=8)     # Reduce to 8 colors

Basic Image Operations

# Resize images
resized = img.resize((800, 600))              # Resize to specific size
resized = img.resize((400, 300), Image.LANCZOS)  # High-quality resampling

# Maintain aspect ratio
def resize_with_aspect(img, max_size=(800, 600)):
    img.thumbnail(max_size, Image.LANCZOS)    # In-place resize maintaining aspect
    return img

# Crop images
cropped = img.crop((100, 100, 400, 400))      # (left, top, right, bottom)

# Rotate images
rotated = img.rotate(45)                      # Rotate 45 degrees
rotated = img.rotate(30, expand=True)         # Expand canvas to fit
rotated = img.rotate(-90, fillcolor='white')  # Fill empty areas with white

# Flip and transpose
flipped_h = img.transpose(Image.FLIP_LEFT_RIGHT)   # Horizontal flip
flipped_v = img.transpose(Image.FLIP_TOP_BOTTOM)   # Vertical flip
rotated_90 = img.transpose(Image.ROTATE_90)        # 90-degree rotation
rotated_180 = img.transpose(Image.ROTATE_180)      # 180-degree rotation
rotated_270 = img.transpose(Image.ROTATE_270)      # 270-degree rotation

# Paste one image onto another
background = Image.new('RGB', (800, 600), 'white')
background.paste(img, (100, 100))            # Paste at position
background.paste(img, (200, 200), img)       # Use img as mask (if RGBA)

Common Use Cases

Image Resizing and Thumbnails

def create_thumbnail(input_path, output_path, size=(150, 150)):
    """Create a thumbnail maintaining aspect ratio"""
    with Image.open(input_path) as img:
        # Convert to RGB if necessary (for JPEG output)
        if img.mode in ('RGBA', 'LA', 'P'):
            img = img.convert('RGB')

        img.thumbnail(size, Image.LANCZOS)
        img.save(output_path, 'JPEG', quality=85, optimize=True)

def resize_to_fit(img, target_size, background_color='white'):
    """Resize image to fit within target size, adding padding if needed"""
    img.thumbnail(target_size, Image.LANCZOS)

    # Create new image with target size
    new_img = Image.new('RGB', target_size, background_color)

    # Calculate position to center the image
    x = (target_size[0] - img.width) // 2
    y = (target_size[1] - img.height) // 2

    new_img.paste(img, (x, y))
    return new_img

def resize_to_cover(img, target_size):
    """Resize and crop image to cover target size"""
    img_ratio = img.width / img.height
    target_ratio = target_size[0] / target_size[1]

    if img_ratio > target_ratio:
        # Image is wider, resize by height
        new_height = target_size[1]
        new_width = int(new_height * img_ratio)
        img = img.resize((new_width, new_height), Image.LANCZOS)

        # Crop horizontally
        left = (new_width - target_size[0]) // 2
        img = img.crop((left, 0, left + target_size[0], target_size[1]))
    else:
        # Image is taller, resize by width
        new_width = target_size[0]
        new_height = int(new_width / img_ratio)
        img = img.resize((new_width, new_height), Image.LANCZOS)

        # Crop vertically
        top = (new_height - target_size[1]) // 2
        img = img.crop((0, top, target_size[0], top + target_size[1]))

    return img

# Usage examples
create_thumbnail('large_image.jpg', 'thumbnail.jpg', (200, 200))

img = Image.open('original.jpg')
fitted = resize_to_fit(img, (800, 600), 'black')
covered = resize_to_cover(img, (800, 600))

Image Filters and Enhancement

# Built-in filters
blurred = img.filter(ImageFilter.BLUR)
sharp = img.filter(ImageFilter.SHARPEN)
smooth = img.filter(ImageFilter.SMOOTH)
detail = img.filter(ImageFilter.DETAIL)
edge_enhance = img.filter(ImageFilter.EDGE_ENHANCE)
emboss = img.filter(ImageFilter.EMBOSS)
contour = img.filter(ImageFilter.CONTOUR)

# Gaussian blur with radius
gaussian = img.filter(ImageFilter.GaussianBlur(radius=2))

# Box blur
box_blur = img.filter(ImageFilter.BoxBlur(radius=1))

# Unsharp mask for sharpening
unsharp = img.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))

# Kernel-based filters
kernel_sharpen = ImageFilter.Kernel(
    size=(3, 3),
    kernel=[-1, -1, -1, -1, 9, -1, -1, -1, -1],
    scale=1
)
sharpened = img.filter(kernel_sharpen)

# Edge detection kernel
kernel_edge = ImageFilter.Kernel(
    size=(3, 3),
    kernel=[-1, -1, -1, -1, 8, -1, -1, -1, -1],
    scale=1
)
edges = img.filter(kernel_edge)

# Image enhancement
enhancer = ImageEnhance.Brightness(img)
brighter = enhancer.enhance(1.3)      # 30% brighter
darker = enhancer.enhance(0.7)        # 30% darker

enhancer = ImageEnhance.Contrast(img)
high_contrast = enhancer.enhance(1.5) # Increase contrast

enhancer = ImageEnhance.Color(img)
saturated = enhancer.enhance(1.4)     # More saturated
desaturated = enhancer.enhance(0.6)   # Less saturated

enhancer = ImageEnhance.Sharpness(img)
sharp = enhancer.enhance(2.0)         # Sharper
soft = enhancer.enhance(0.5)          # Softer

Drawing on Images

def add_watermark(img, text, position='bottom-right', font_size=36, opacity=128):
    """Add text watermark to image"""
    # Create a transparent overlay
    overlay = Image.new('RGBA', img.size, (255, 255, 255, 0))
    draw = ImageDraw.Draw(overlay)

    # Try to load a font
    try:
        font = ImageFont.truetype("arial.ttf", font_size)
    except:
        font = ImageFont.load_default()

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

    # Calculate position
    margin = 20
    if position == 'bottom-right':
        x = img.width - text_width - margin
        y = img.height - text_height - margin
    elif position == 'bottom-left':
        x = margin
        y = img.height - text_height - margin
    elif position == 'top-right':
        x = img.width - text_width - margin
        y = margin
    else:  # top-left
        x = margin
        y = margin

    # Draw text
    draw.text((x, y), text, font=font, fill=(255, 255, 255, opacity))

    # Composite with original image
    if img.mode != 'RGBA':
        img = img.convert('RGBA')

    watermarked = Image.alpha_composite(img, overlay)
    return watermarked.convert('RGB')

def draw_shapes_and_text(img):
    """Draw various shapes and text on image"""
    draw = ImageDraw.Draw(img)

    # Rectangle
    draw.rectangle([50, 50, 150, 100], fill='red', outline='black', width=2)

    # Circle (ellipse with equal width and height)
    draw.ellipse([200, 50, 300, 150], fill='blue', outline='white', width=3)

    # Line
    draw.line([50, 150, 300, 200], fill='green', width=5)

    # Polygon
    draw.polygon([(400, 50), (450, 100), (400, 150), (350, 100)], 
                fill='yellow', outline='purple')

    # Text
    try:
        font = ImageFont.truetype("arial.ttf", 24)
    except:
        font = ImageFont.load_default()

    draw.text((50, 220), "Hello, Pillow!", font=font, fill='black')

    return img

def add_border(img, border_size=10, color='black'):
    """Add border around image"""
    return ImageOps.expand(img, border=border_size, fill=color)

def create_rounded_corners(img, radius=20):
    """Create image with rounded corners"""
    # Create mask
    mask = Image.new('L', img.size, 0)
    draw = ImageDraw.Draw(mask)
    draw.rounded_rectangle([0, 0, img.width, img.height], radius, fill=255)

    # Apply mask
    img = img.convert('RGBA')
    img.putalpha(mask)

    return img

# Usage examples
img = Image.open('photo.jpg')
watermarked = add_watermark(img, "© 2025 My Company", opacity=100)
bordered = add_border(img, 15, 'white')
rounded = create_rounded_corners(img, 30)

Batch Processing

import os
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import multiprocessing

def process_image(input_path, output_path, operations):
    """Process a single image with specified operations"""
    try:
        with Image.open(input_path) as img:
            # Convert to RGB if needed
            if img.mode in ('RGBA', 'LA', 'P'):
                img = img.convert('RGB')

            # Apply operations
            for operation, params in operations.items():
                if operation == 'resize':
                    img = img.resize(params['size'], Image.LANCZOS)
                elif operation == 'thumbnail':
                    img.thumbnail(params['size'], Image.LANCZOS)
                elif operation == 'rotate':
                    img = img.rotate(params['angle'])
                elif operation == 'enhance_brightness':
                    enhancer = ImageEnhance.Brightness(img)
                    img = enhancer.enhance(params['factor'])
                elif operation == 'enhance_contrast':
                    enhancer = ImageEnhance.Contrast(img)
                    img = enhancer.enhance(params['factor'])
                elif operation == 'filter':
                    img = img.filter(params['filter'])
                elif operation == 'grayscale':
                    img = img.convert('L')

            # Save processed image
            os.makedirs(os.path.dirname(output_path), exist_ok=True)
            img.save(output_path, quality=params.get('quality', 90), optimize=True)

        return f"Processed: {input_path} -> {output_path}"

    except Exception as e:
        return f"Error processing {input_path}: {str(e)}"

def batch_process_images(input_dir, output_dir, operations, file_extensions=None):
    """Batch process images in a directory"""
    if file_extensions is None:
        file_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}

    input_path = Path(input_dir)
    output_path = Path(output_dir)

    # Find all image files
    image_files = []
    for ext in file_extensions:
        image_files.extend(input_path.glob(f"**/*{ext}"))
        image_files.extend(input_path.glob(f"**/*{ext.upper()}"))

    # Prepare tasks
    tasks = []
    for img_file in image_files:
        relative_path = img_file.relative_to(input_path)
        output_file = output_path / relative_path
        tasks.append((str(img_file), str(output_file), operations))

    # Process with multiple threads
    max_workers = min(32, multiprocessing.cpu_count() * 2)
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        results = executor.map(lambda args: process_image(*args), tasks)

    # Print results
    for result in results:
        print(result)

# Usage example
operations = {
    'thumbnail': {'size': (800, 600)},
    'enhance_contrast': {'factor': 1.1},
    'filter': {'filter': ImageFilter.SHARPEN},
    'quality': 85
}

batch_process_images('./input_photos', './output_photos', operations)

# Specific batch operations
def create_thumbnails_batch(input_dir, output_dir, size=(200, 200)):
    """Create thumbnails for all images in directory"""
    operations = {
        'thumbnail': {'size': size},
        'quality': 85
    }
    batch_process_images(input_dir, output_dir, operations)

def optimize_images_batch(input_dir, output_dir, quality=85):
    """Optimize images for web (reduce file size)"""
    operations = {
        'resize': {'size': (1920, 1080)},  # Max size for web
        'quality': quality
    }
    batch_process_images(input_dir, output_dir, operations)

# Usage
create_thumbnails_batch('./photos', './thumbnails', (150, 150))
optimize_images_batch('./large_photos', './optimized', quality=75)

Advanced Features

Working with Image Metadata

from PIL.ExifTags import TAGS, GPSTAGS
import json

def extract_exif_data(img_path):
    """Extract EXIF data from image"""
    with Image.open(img_path) as img:
        exif_data = {}

        # Get raw EXIF data
        exif = img._getexif()

        if exif:
            for tag_id, value in exif.items():
                tag = TAGS.get(tag_id, tag_id)

                # Handle GPS data specially
                if tag == "GPSInfo":
                    gps_data = {}
                    for gps_tag_id, gps_value in value.items():
                        gps_tag = GPSTAGS.get(gps_tag_id, gps_tag_id)
                        gps_data[gps_tag] = gps_value
                    exif_data[tag] = gps_data
                else:
                    exif_data[tag] = value

        return exif_data

def get_image_info(img_path):
    """Get comprehensive image information"""
    with Image.open(img_path) as img:
        info = {
            'filename': os.path.basename(img_path),
            'format': img.format,
            'mode': img.mode,
            'size': img.size,
            'width': img.width,
            'height': img.height,
            'aspect_ratio': round(img.width / img.height, 2),
            'file_size': os.path.getsize(img_path),
            'has_transparency': img.mode in ('RGBA', 'LA') or 'transparency' in img.info
        }

        # Add EXIF data if available
        exif = extract_exif_data(img_path)
        if exif:
            info['exif'] = {
                'make': exif.get('Make', 'Unknown'),
                'model': exif.get('Model', 'Unknown'),
                'datetime': exif.get('DateTime', 'Unknown'),
                'orientation': exif.get('Orientation', 1),
                'iso': exif.get('ISOSpeedRatings', 'Unknown'),
                'focal_length': exif.get('FocalLength', 'Unknown')
            }

        return info

def auto_rotate_image(img_path, output_path=None):
    """Auto-rotate image based on EXIF orientation"""
    with Image.open(img_path) as img:
        # Get orientation from EXIF
        exif = img._getexif()
        orientation = 1

        if exif and 'Orientation' in [TAGS.get(k) for k in exif.keys()]:
            for tag_id, value in exif.items():
                if TAGS.get(tag_id) == 'Orientation':
                    orientation = value
                    break

        # Apply rotation based on orientation
        if orientation == 3:
            img = img.rotate(180, expand=True)
        elif orientation == 6:
            img = img.rotate(270, expand=True)
        elif orientation == 8:
            img = img.rotate(90, expand=True)

        # Save rotated image
        if output_path:
            img.save(output_path)
        else:
            img.save(img_path)

        return img

# Usage
info = get_image_info('photo.jpg')
print(json.dumps(info, indent=2, default=str))

auto_rotate_image('rotated_photo.jpg', 'corrected_photo.jpg')

Color Operations

def adjust_color_balance(img, cyan_red=0, magenta_green=0, yellow_blue=0):
    """Adjust color balance similar to Photoshop"""
    # Split into RGB channels
    r, g, b = img.split()

    # Apply adjustments
    r = r.point(lambda x: max(0, min(255, x + cyan_red)))
    g = g.point(lambda x: max(0, min(255, x + magenta_green)))
    b = b.point(lambda x: max(0, min(255, x + yellow_blue)))

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

def apply_color_curves(img, curve_points):
    """Apply color curves adjustment"""
    # Create lookup table
    curve = list(range(256))

    # Interpolate curve points
    for i in range(len(curve_points) - 1):
        x1, y1 = curve_points[i]
        x2, y2 = curve_points[i + 1]

        for x in range(x1, x2 + 1):
            if x2 > x1:
                ratio = (x - x1) / (x2 - x1)
                curve[x] = int(y1 + ratio * (y2 - y1))

    return img.point(curve)

def create_sepia_effect(img):
    """Create sepia tone effect"""
    # Convert to RGB if necessary
    if img.mode != 'RGB':
        img = img.convert('RGB')

    # Apply sepia transformation matrix
    pixels = img.load()
    width, height = img.size

    for y in range(height):
        for x in range(width):
            r, g, b = pixels[x, y]

            # Sepia transformation
            tr = int(0.393 * r + 0.769 * g + 0.189 * b)
            tg = int(0.349 * r + 0.686 * g + 0.168 * b)
            tb = int(0.272 * r + 0.534 * g + 0.131 * b)

            # Ensure values are within range
            pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb))

    return img

def create_vintage_effect(img):
    """Create vintage photo effect"""
    # Apply sepia
    img = create_sepia_effect(img)

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

    # Add slight blur
    img = img.filter(ImageFilter.GaussianBlur(0.5))

    # Add vignette effect
    width, height = img.size
    vignette = Image.new('L', (width, height), 255)
    draw = ImageDraw.Draw(vignette)

    # Create radial gradient for vignette
    center_x, center_y = width // 2, height // 2
    max_distance = min(width, height) // 2

    for y in range(height):
        for x in range(width):
            distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
            if distance < max_distance:
                alpha = int(255 * (1 - (distance / max_distance) ** 2))
                vignette.putpixel((x, y), alpha)
            else:
                vignette.putpixel((x, y), 0)

    # Apply vignette
    img = img.convert('RGBA')
    img.putalpha(vignette)

    return img

def extract_dominant_colors(img, num_colors=5):
    """Extract dominant colors from image"""
    # Convert to RGB and resize for performance
    img = img.convert('RGB')
    img = img.resize((150, 150))  # Smaller size for faster processing

    # Quantize to reduce colors
    quantized = img.quantize(colors=num_colors)

    # Get palette colors
    palette = quantized.getpalette()
    colors = []

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

    # Count occurrences of each color
    quantized = quantized.convert('RGB')
    pixels = list(quantized.getdata())

    color_counts = {}
    for pixel in pixels:
        color_counts[pixel] = color_counts.get(pixel, 0) + 1

    # Sort by frequency
    sorted_colors = sorted(color_counts.items(), key=lambda x: x[1], reverse=True)

    return [color for color, count in sorted_colors[:num_colors]]

# Usage examples
img = Image.open('photo.jpg')

# Color adjustments
balanced = adjust_color_balance(img, cyan_red=10, yellow_blue=-5)

# Curves adjustment (darken shadows, brighten highlights)
curves = apply_color_curves(img, [(0, 0), (64, 50), (128, 128), (192, 200), (255, 255)])

# Effects
sepia = create_sepia_effect(img)
vintage = create_vintage_effect(img)

# Extract colors
dominant_colors = extract_dominant_colors(img)
print("Dominant colors:", dominant_colors)

Advanced Compositing

def blend_images(img1, img2, mode='normal', opacity=0.5):
    """Blend two images with different blend modes"""
    # Ensure images are same size
    if img1.size != img2.size:
        img2 = img2.resize(img1.size, Image.LANCZOS)

    # Convert to same mode
    if img1.mode != img2.mode:
        img2 = img2.convert(img1.mode)

    if mode == 'normal':
        return Image.blend(img1, img2, opacity)
    elif mode == 'multiply':
        return ImageChops.multiply(img1, img2)
    elif mode == 'screen':
        return ImageChops.screen(img1, img2)
    elif mode == 'overlay':
        return ImageChops.overlay(img1, img2)
    elif mode == 'difference':
        return ImageChops.difference(img1, img2)
    elif mode == 'add':
        return ImageChops.add(img1, img2)
    elif mode == 'subtract':
        return ImageChops.subtract(img1, img2)
    elif mode == 'darker':
        return ImageChops.darker(img1, img2)
    elif mode == 'lighter':
        return ImageChops.lighter(img1, img2)
    else:
        return Image.blend(img1, img2, opacity)

def create_photo_collage(images, layout=(2, 2), spacing=10, background_color='white'):
    """Create a photo collage from multiple images"""
    rows, cols = layout

    # Calculate dimensions for each cell
    total_images = len(images)
    images = images[:rows * cols]  # Limit to layout size

    # Find maximum dimensions
    max_width = max(img.width for img in images)
    max_height = max(img.height for img in images)

    # Calculate canvas size
    canvas_width = cols * max_width + (cols + 1) * spacing
    canvas_height = rows * max_height + (rows + 1) * spacing

    # Create canvas
    canvas = Image.new('RGB', (canvas_width, canvas_height), background_color)

    # Place images
    for i, img in enumerate(images):
        row = i // cols
        col = i % cols

        # Resize image to fit cell
        img_resized = img.copy()
        img_resized.thumbnail((max_width, max_height), Image.LANCZOS)

        # Calculate position
        x = spacing + col * (max_width + spacing) + (max_width - img_resized.width) // 2
        y = spacing + row * (max_height + spacing) + (max_height - img_resized.height) // 2

        canvas.paste(img_resized, (x, y))

    return canvas

def apply_gradient_mask(img, direction='horizontal', start_alpha=255, end_alpha=0):
    """Apply gradient mask to image"""
    # Create gradient mask
    width, height = img.size
    mask = Image.new('L', (width, height))

    for y in range(height):
        for x in range(width):
            if direction == 'horizontal':
                alpha = int(start_alpha + (end_alpha - start_alpha) * x / width)
            elif direction == 'vertical':
                alpha = int(start_alpha + (end_alpha - start_alpha) * y / height)
            elif direction == 'radial':
                center_x, center_y = width // 2, height // 2
                distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
                max_distance = min(width, height) // 2
                alpha = int(start_alpha + (end_alpha - start_alpha) * min(distance / max_distance, 1))

            mask.putpixel((x, y), max(0, min(255, alpha)))

    # Apply mask
    if img.mode != 'RGBA':
        img = img.convert('RGBA')

    img.putalpha(mask)
    return img

def create_panorama(images):
    """Simple panorama creation (basic horizontal stitching)"""
    if not images:
        return None

    # Resize all images to same height
    min_height = min(img.height for img in images)
    resized_images = []

    for img in images:
        aspect_ratio = img.width / img.height
        new_width = int(min_height * aspect_ratio)
        resized_img = img.resize((new_width, min_height), Image.LANCZOS)
        resized_images.append(resized_img)

    # Calculate total width
    total_width = sum(img.width for img in resized_images)

    # Create panorama canvas
    panorama = Image.new('RGB', (total_width, min_height))

    # Paste images horizontally
    x_offset = 0
    for img in resized_images:
        panorama.paste(img, (x_offset, 0))
        x_offset += img.width

    return panorama

# Usage examples
img1 = Image.open('photo1.jpg')
img2 = Image.open('photo2.jpg')

# Blend images
blended = blend_images(img1, img2, 'overlay', 0.7)

# Create collage
images = [Image.open(f'photo{i}.jpg') for i in range(1, 5)]
collage = create_photo_collage(images, (2, 2), spacing=15)

# Apply gradient
gradient_img = apply_gradient_mask(img1, 'radial')

# Create panorama
pano_images = [Image.open(f'pano{i}.jpg') for i in range(1, 4)]
panorama = create_panorama(pano_images)

Integration with Other Libraries

With NumPy

import numpy as np

# Convert PIL Image to NumPy array
img_array = np.array(img)
print(f"Array shape: {img_array.shape}")  # (height, width, channels)
print(f"Array dtype: {img_array.dtype}")  # uint8

# Convert NumPy array to PIL Image
img_from_array = Image.fromarray(img_array)

# Advanced NumPy operations
def adjust_gamma(img, gamma=1.0):
    """Apply gamma correction"""
    img_array = np.array(img, dtype=np.float64)
    img_array = img_array / 255.0  # Normalize to [0, 1]
    img_array = np.power(img_array, gamma)
    img_array = (img_array * 255).astype(np.uint8)
    return Image.fromarray(img_array)

def apply_histogram_equalization(img):
    """Apply histogram equalization using NumPy"""
    if img.mode != 'L':
        img = img.convert('L')

    img_array = np.array(img)

    # Calculate histogram
    hist, bins = np.histogram(img_array.flatten(), 256, [0, 256])

    # Calculate cumulative distribution
    cdf = hist.cumsum()
    cdf_normalized = cdf * 255 / cdf[-1]

    # Apply equalization
    img_equalized = np.interp(img_array.flatten(), bins[:-1], cdf_normalized)
    img_equalized = img_equalized.reshape(img_array.shape).astype(np.uint8)

    return Image.fromarray(img_equalized)

def create_noise(size, noise_type='gaussian'):
    """Create noise using NumPy"""
    if noise_type == 'gaussian':
        noise = np.random.normal(128, 30, size).astype(np.uint8)
    elif noise_type == 'uniform':
        noise = np.random.uniform(0, 255, size).astype(np.uint8)
    elif noise_type == 'salt_pepper':
        noise = np.random.choice([0, 255], size=size, p=[0.5, 0.5]).astype(np.uint8)

    return Image.fromarray(noise)

# Usage
gamma_corrected = adjust_gamma(img, gamma=1.5)
equalized = apply_histogram_equalization(img)
noise_img = create_noise((200, 200, 3), 'gaussian')

With Matplotlib

import matplotlib.pyplot as plt
import matplotlib.patches as patches

def plot_image_analysis(img):
    """Create comprehensive image analysis plot"""
    fig, axes = plt.subplots(2, 3, figsize=(15, 10))

    # Original image
    axes[0, 0].imshow(img)
    axes[0, 0].set_title('Original Image')
    axes[0, 0].axis('off')

    # Grayscale
    gray_img = img.convert('L')
    axes[0, 1].imshow(gray_img, cmap='gray')
    axes[0, 1].set_title('Grayscale')
    axes[0, 1].axis('off')

    # Edge detection
    edges = gray_img.filter(ImageFilter.FIND_EDGES)
    axes[0, 2].imshow(edges, cmap='gray')
    axes[0, 2].set_title('Edge Detection')
    axes[0, 2].axis('off')

    # RGB Histogram
    if img.mode == 'RGB':
        r_hist = np.array(img.split()[0]).flatten()
        g_hist = np.array(img.split()[1]).flatten()
        b_hist = np.array(img.split()[2]).flatten()

        axes[1, 0].hist(r_hist, bins=256, color='red', alpha=0.7, density=True)
        axes[1, 0].hist(g_hist, bins=256, color='green', alpha=0.7, density=True)
        axes[1, 0].hist(b_hist, bins=256, color='blue', alpha=0.7, density=True)
        axes[1, 0].set_title('RGB Histogram')
        axes[1, 0].set_xlabel('Pixel Value')
        axes[1, 0].set_ylabel('Density')

    # Grayscale histogram
    gray_hist = np.array(gray_img).flatten()
    axes[1, 1].hist(gray_hist, bins=256, color='gray', alpha=0.7)
    axes[1, 1].set_title('Grayscale Histogram')
    axes[1, 1].set_xlabel('Pixel Value')
    axes[1, 1].set_ylabel('Frequency')

    # Image statistics
    stats = ImageStat.Stat(img)
    axes[1, 2].axis('off')
    stats_text = f"""
    Size: {img.size}
    Mode: {img.mode}
    Mean: {[round(m, 1) for m in stats.mean]}
    Median: {[round(m, 1) for m in stats.median]}
    StdDev: {[round(s, 1) for s in stats.stddev]}
    """
    axes[1, 2].text(0.1, 0.5, stats_text, fontsize=12, verticalalignment='center')
    axes[1, 2].set_title('Image Statistics')

    plt.tight_layout()
    plt.show()

def create_before_after_plot(original, processed, title="Before / After"):
    """Create side-by-side comparison"""
    fig, axes = plt.subplots(1, 2, figsize=(12, 6))

    axes[0].imshow(original)
    axes[0].set_title('Before')
    axes[0].axis('off')

    axes[1].imshow(processed)
    axes[1].set_title('After')
    axes[1].axis('off')

    fig.suptitle(title, fontsize=16)
    plt.tight_layout()
    plt.show()

# Usage
plot_image_analysis(img)
processed_img = img.filter(ImageFilter.SHARPEN)
create_before_after_plot(img, processed_img, "Sharpening Effect")

With OpenCV Integration

import cv2

def pil_to_opencv(pil_img):
    """Convert PIL Image to OpenCV format"""
    return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)

def opencv_to_pil(cv_img):
    """Convert OpenCV image to PIL format"""
    return Image.fromarray(cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB))

def advanced_edge_detection(img):
    """Advanced edge detection using OpenCV"""
    # Convert to OpenCV format
    cv_img = pil_to_opencv(img)
    gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)

    # Apply Canny edge detection
    edges = cv2.Canny(gray, 50, 150)

    # Convert back to PIL
    return Image.fromarray(edges, mode='L')

def detect_contours(img, min_area=1000):
    """Detect and draw contours"""
    cv_img = pil_to_opencv(img)
    gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)

    # Find contours
    contours, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # Filter by area and draw
    for contour in contours:
        area = cv2.contourArea(contour)
        if area > min_area:
            cv2.drawContours(cv_img, [contour], -1, (0, 255, 0), 2)

    return opencv_to_pil(cv_img)

# Usage
edges = advanced_edge_detection(img)
contours = detect_contours(img)

Best Practices

Performance Optimization

# 1. Use appropriate resampling filters
resizing_filters = {
    'fastest': Image.NEAREST,      # Fastest, lowest quality
    'balanced': Image.BILINEAR,    # Good balance
    'quality': Image.LANCZOS       # Best quality, slower
}

# 2. Work with smaller images when possible
def optimize_for_processing(img, max_size=1024):
    """Resize large images for faster processing"""
    if max(img.size) > max_size:
        img.thumbnail((max_size, max_size), Image.LANCZOS)
    return img

# 3. Use context managers for file handling
def process_multiple_images(file_paths, operations):
    """Efficiently process multiple images"""
    results = []
    for path in file_paths:
        with Image.open(path) as img:
            # Process without loading full image into memory
            for op in operations:
                img = op(img)
            results.append(img.copy())  # Make a copy before context ends
    return results

# 4. Batch operations when possible
def batch_resize(image_paths, size, output_dir):
    """Batch resize operation"""
    os.makedirs(output_dir, exist_ok=True)

    for path in image_paths:
        with Image.open(path) as img:
            img.thumbnail(size, Image.LANCZOS)
            filename = os.path.basename(path)
            img.save(os.path.join(output_dir, filename), optimize=True)

# 5. Use appropriate image formats
format_recommendations = {
    'photos': 'JPEG',           # Best for photos
    'graphics': 'PNG',          # Best for graphics with transparency
    'web_photos': 'WebP',       # Modern web format
    'icons': 'PNG',             # Best for icons
    'print': 'TIFF'            # Best for print
}

Memory Management

import psutil
import gc

def monitor_memory_usage():
    """Monitor memory usage"""
    process = psutil.Process()
    return process.memory_info().rss / 1024 / 1024  # MB

def process_large_image_safely(img_path, operations, chunk_size=None):
    """Process large images without loading everything into memory"""
    with Image.open(img_path) as img:
        print(f"Processing {img.size} image")
        print(f"Memory before: {monitor_memory_usage():.1f} MB")

        # Work with image
        for operation in operations:
            img = operation(img)
            gc.collect()  # Force garbage collection

        print(f"Memory after: {monitor_memory_usage():.1f} MB")
        return img

def create_image_pyramid(img, levels=3):
    """Create image pyramid to save memory"""
    pyramid = [img]
    current = img

    for level in range(1, levels):
        size = (current.width // 2, current.height // 2)
        current = current.resize(size, Image.LANCZOS)
        pyramid.append(current)

    return pyramid

Error Handling and Validation

def safe_image_operation(operation):
    """Decorator for safe image operations"""
    def wrapper(img_path, *args, **kwargs):
        try:
            with Image.open(img_path) as img:
                return operation(img, *args, **kwargs)
        except IOError:
            print(f"Cannot open image: {img_path}")
            return None
        except Exception as e:
            print(f"Error processing {img_path}: {str(e)}")
            return None
    return wrapper

@safe_image_operation
def safe_resize(img, size):
    """Safely resize image with error handling"""
    return img.resize(size, Image.LANCZOS)

def validate_image_format(img_path, allowed_formats=None):
    """Validate image format"""
    if allowed_formats is None:
        allowed_formats = {'JPEG', 'PNG', 'BMP', 'TIFF', 'WebP'}

    try:
        with Image.open(img_path) as img:
            if img.format in allowed_formats:
                return True, img.format
            else:
                return False, f"Format {img.format} not allowed"
    except:
        return False, "Cannot open file"

def robust_image_save(img, output_path, fallback_format='JPEG', **kwargs):
    """Save image with fallback options"""
    try:
        img.save(output_path, **kwargs)
        return True, "Saved successfully"
    except:
        try:
            # Try with fallback format
            base_name = os.path.splitext(output_path)[0]
            fallback_path = f"{base_name}.{fallback_format.lower()}"

            if img.mode in ('RGBA', 'LA'):
                img = img.convert('RGB')

            img.save(fallback_path, fallback_format, quality=90)
            return True, f"Saved as {fallback_format}"
        except Exception as e:
            return False, str(e)

# Usage
result = safe_resize('photo.jpg', (800, 600))
valid, msg = validate_image_format('image.xyz')
success, msg = robust_image_save(img, 'output.webp')

This comprehensive cheat sheet covers the essential aspects of Pillow for image processing in Python. The library's strength lies in its broad format support, ease of use, and extensive functionality for both simple and complex image operations. It's the go-to library for Python developers working with images in web development, data science, and desktop applications.