This lesson covers advanced image editing techniques with Pillow. You will learn to paste and composite images, work with alpha channels and masks, create image galleries, add borders and frames, and build automated image processing pipelines. These skills are essential for web development, social media automation, content creation, and data preparation.
🕯️ Magic Note
Pillow’s paste() and composite() functions are the foundation of image composition. You can layer images, create collages, add logos, and build complex visual layouts entirely in Python.
Python
from PIL import Image
# Load background and foreground
background = Image.new(“RGB”, (800, 600), (100, 150, 200))
logo = Image.open(“logo.png”)
# Paste at specific position (top-left corner)
background.paste(logo, (50, 50))
# Paste with transparency (using mask)
if logo.mode == “RGBA”:
background.paste(logo, (50, 50), logo) # Uses alpha channel as mask
# Paste using a separate mask image
mask = Image.new(“L”, logo.size, 128) # 50% transparent mask
background.paste(logo, (50, 50), mask)
# Paste in center
x = (background.width – logo.width) // 2
y = (background.height – logo.height) // 2
background.paste(logo, (x, y), logo if logo.mode == “RGBA” else None)
background.save(“pasted.jpg”)
Python
from PIL import Image
from pathlib import Path
import math
def create_collage(image_paths, output_path, cols=3, thumbnail_size=200, spacing=10):
“””
Create a collage from multiple images.
Args:
image_paths: List of paths to images
output_path: Where to save the collage
cols: Number of columns
thumbnail_size: Size to resize each image (square)
spacing: Spacing between images
“””
rows = math.ceil(len(image_paths) / cols)
# Calculate total collage size
width = cols * thumbnail_size + (cols – 1) * spacing
height = rows * thumbnail_size + (rows – 1) * spacing
# Create blank canvas
collage = Image.new(“RGB”, (width, height), (255, 255, 255))
for idx, img_path in enumerate(image_paths[:rows * cols]):
row = idx // cols
col = idx % cols
# Open and resize image
img = Image.open(img_path)
img.thumbnail((thumbnail_size, thumbnail_size))
# Create square canvas for this cell
cell = Image.new(“RGB”, (thumbnail_size, thumbnail_size), (255, 255, 255))
# Center the image in the cell
x = (thumbnail_size – img.width) // 2
y = (thumbnail_size – img.height) // 2
cell.paste(img, (x, y))
# Calculate position in collage
x_pos = col * (thumbnail_size + spacing)
y_pos = row * (thumbnail_size + spacing)
collage.paste(cell, (x_pos, y_pos))
collage.save(output_path)
print(f”Collage saved to {output_path}”)
# Usage
# create_collage([“img1.jpg”, “img2.jpg”, “img3.jpg”], “collage.jpg”, cols=2)
🕯️ Magic Note
The collage pattern is highly customizable. You can add borders, rounded corners, captions, or different layouts. This is how social media grid images and product catalogs are often generated automatically.
Python
from PIL import Image
# Load two images (must be same size)
img1 = Image.open(“image1.jpg”).convert(“RGBA”)
img2 = Image.open(“image2.jpg”).convert(“RGBA”)
# Resize to match if needed
img2 = img2.resize(img1.size)
# Blend with alpha (0.0 = first image, 1.0 = second image)
blended = Image.blend(img1, img2, alpha=0.5)
blended.save(“blended_50.jpg”)
# Composite using a mask (0-255 grayscale)
mask = Image.new(“L”, img1.size, 128) # Uniform 50% mask
composite = Image.composite(img1, img2, mask)
composite.save(“composite_uniform.jpg”)
# Gradient mask for smooth transition
gradient = Image.new(“L”, img1.size)
pixels = gradient.load()
for x in range(gradient.width):
for y in range(gradient.height):
pixels[x, y] = int(255 * x / gradient.width)
gradient_composite = Image.composite(img1, img2, gradient)
gradient_composite.save(“gradient_composite.jpg”)
Python
from PIL import Image, ImageDraw
def add_rounded_corners(img, radius=30):
“””Add rounded corners to an image.”””
# Create a mask for rounded corners
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 to create rounded corners
if img.mode != “RGBA”:
img = img.convert(“RGBA”)
output = Image.new(“RGBA”, img.size, (0, 0, 0, 0))
output.paste(img, (0, 0), mask)
return output
img = Image.open(“example.jpg”)
rounded = add_rounded_corners(img, radius=50)
rounded.save(“rounded_corners.png”)
🕯️ Magic Note
The rounded_rectangle() method draws a rectangle with rounded corners. When used as a mask, it creates the transparency effect. This is how profile pictures with circular or rounded corners are created.
Python
from PIL import Image, ImageDraw
def add_border(img, border_size=10, border_color=”black”):
“””Add a solid border around an image.”””
width, height = img.size
new_width = width + 2 * border_size
new_height = height + 2 * border_size
# Create new image with border color
bordered = Image.new(“RGB”, (new_width, new_height), border_color)
bordered.paste(img, (border_size, border_size))
return bordered
def add_polaroid_frame(img, border_size=20, bottom_height=60, text=””):
“””Create a Polaroid-style frame with space for text.”””
width, height = img.size
new_width = width + 2 * border_size
new_height = height + border_size + bottom_height
# Create white frame
frame = Image.new(“RGB”, (new_width, new_height), “white”)
# Paste image
frame.paste(img, (border_size, border_size))
# Add text if provided
if text:
draw = ImageDraw.Draw(frame)
try:
from PIL import ImageFont
font = ImageFont.truetype(“arial.ttf”, 16)
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] – bbox[0]
text_x = (new_width – text_width) // 2
text_y = height + border_size + 10
draw.text((text_x, text_y), text, fill=”black”, font=font)
return frame
img = Image.open(“example.jpg”)
bordered = add_border(img, 20, “#333333”)
bordered.save(“bordered.jpg”)
polaroid = add_polaroid_frame(img, 15, 50, “Summer 2025”)
polaroid.save(“polaroid.jpg”)
Python
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
import math
def create_contact_sheet(folder_path, output_path, cols=4, thumb_size=200, label=True):
“””Create a contact sheet of all images in a folder.”””
image_extensions = {“.jpg”, “.jpeg”, “.png”, “.gif”, “.bmp”}
image_paths = [p for p in Path(folder_path).iterdir() if p.suffix.lower() in image_extensions]
if not image_paths:
print(“No images found”)
return
rows = math.ceil(len(image_paths) / cols)
cell_width = thumb_size + 10
cell_height = thumb_size + (30 if label else 10)
width = cols * cell_width
height = rows * cell_height
sheet = Image.new(“RGB”, (width, height), “white”)
draw = ImageDraw.Draw(sheet)
try:
font = ImageFont.truetype(“arial.ttf”, 10)
except:
font = ImageFont.load_default()
for idx, img_path in enumerate(image_paths):
row = idx // cols
col = idx % cols
x = col * cell_width
y = row * cell_height
img = Image.open(img_path)
img.thumbnail((thumb_size, thumb_size))
paste_x = x + (thumb_size – img.width) // 2
paste_y = y + (thumb_size – img.height) // 2
sheet.paste(img, (paste_x, paste_y))
if label:
label_text = img_path.stem[:15]
bbox = draw.textbbox((0, 0), label_text, font=font)
text_width = bbox[2] – bbox[0]
text_x = x + (cell_width – text_width) // 2
text_y = y + thumb_size + 5
draw.text((text_x, text_y), label_text, fill=”gray”, font=font)
sheet.save(output_path)
print(f”Contact sheet saved to {output_path} ({len(image_paths)} images)”)
# create_contact_sheet(“photos”, “contact_sheet.jpg”, cols=5)
Python
from PIL import Image, ImageDraw, ImageEnhance
import math
def add_vignette(img, strength=0.5):
“””Add a dark vignette effect around the edges.”””
if img.mode != “RGBA”:
img = img.convert(“RGBA”)
# Create gradient mask
mask = Image.new(“L”, img.size, 255)
draw = ImageDraw.Draw(mask)
center_x, center_y = img.width / 2, img.height / 2
max_radius = math.sqrt(center_x ** 2 + center_y ** 2)
for x in range(img.width):
for y in range(img.height):
distance = math.sqrt((x – center_x) ** 2 + (y – center_y) ** 2)
darkness = min(255, int(255 * (distance / max_radius) * strength))
mask.putpixel((x, y), 255 – darkness)
# Apply mask as alpha
if img.mode != “RGBA”:
img = img.convert(“RGBA”)
img.putalpha(mask)
return img
def sepia(img):
“””Convert image to sepia tone.”””
if img.mode != “RGB”:
img = img.convert(“RGB”)
width, height = img.size
sepia_img = Image.new(“RGB”, (width, height))
pixels = img.load()
sepia_pixels = sepia_img.load()
for x in range(width):
for y in range(height):
r, g, b = pixels[x, y]
# Apply 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)
sepia_pixels[x, y] = (min(tr, 255), min(tg, 255), min(tb, 255))
return sepia_img
# Apply effects
img = Image.open(“example.jpg”)
vignette_img = add_vignette(img, strength=0.7)
vignette_img.save(“vignette.png”)
sepia_img = sepia(img)
sepia_img.save(“sepia.jpg”)
Python
from PIL import Image
from pathlib import Path
def batch_process_images(input_folder, output_folder, operations):
“””
Apply a series of operations to all images in a folder.
operations: list of tuples (operation_name, params)
“””
input_path = Path(input_folder)
output_path = Path(output_folder)
output_path.mkdir(exist_ok=True)
image_extensions = {“.jpg”, “.jpeg”, “.png”, “.gif”, “.bmp”}
image_files = [f for f in input_path.iterdir() if f.suffix.lower() in image_extensions]
for img_file in image_files:
img = Image.open(img_file)
for op_name, params in operations:
if op_name == “resize”:
img = img.resize(params[“size”])
elif op_name == “thumbnail”:
img.thumbnail(params[“size”])
elif op_name == “rotate”:
img = img.rotate(params[“angle”], expand=params.get(“expand”, True))
elif op_name == “convert”:
img = img.convert(params[“mode”])
elif op_name == “crop”:
img = img.crop(params[“box”])
elif op_name == “filter”:
from PIL import ImageFilter
filter_map = {
“blur”: ImageFilter.BLUR,
“contour”: ImageFilter.CONTOUR,
“sharpen”: ImageFilter.SHARPEN,
“edges”: ImageFilter.FIND_EDGES
}
img = img.filter(filter_map.get(params[“name”]))
output_file = output_path / img_file.name
img.save(output_file)
print(f”Processed: {img_file.name}”)
print(f”Batch processing complete. {len(image_files)} images processed.”)
# Usage example
# operations = [
# (“thumbnail”, {“size”: (800, 800)}),
# (“rotate”, {“angle”: 90, “expand”: False}),
# (“convert”, {“mode”: “L”}) # Convert to grayscale
# ]
# batch_process_images(“originals”, “processed”, operations)
Python
from PIL import Image
def create_animated_gif(image_paths, output_path, duration=100, loop=0):
“””Create an animated GIF from multiple images.”””
frames = []
for img_path in image_paths:
img = Image.open(img_path)
frames.append(img.copy())
if frames:
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
duration=duration,
loop=loop
)
print(f”Animated GIF saved to {output_path}”)
# Extract frames from GIF
def extract_gif_frames(gif_path, output_folder):
“””Extract all frames from an animated GIF.”””
with Image.open(gif_path) as gif:
frame_count = 0
while True:
try:
gif.seek(frame_count)
frame = gif.copy()
frame.save(f”{output_folder}/frame_{frame_count:03d}.png”)
frame_count += 1
except EOFError:
break
print(f”Extracted {frame_count} frames”)
# create_animated_gif([“img1.jpg”, “img2.jpg”, “img3.jpg”], “animation.gif”, duration=200)
- Forgetting to convert modes before compositing (ensure both images have same mode)
- Pasting images of different sizes without resizing (causes ValueError)
- Using JPEG for images with transparency (use PNG)
- Modifying the original image instead of working on a copy
- Not handling memory with large batches (use context managers or process images one by one)
- Assuming all images have the same dimensions when creating collages
- How do you paste one image onto another with transparency?
- Write a function that creates a 2×2 grid collage from four images.
- How do you add rounded corners to an image?
- What is the difference between Image.blend() and Image.composite()?
- Write code that creates a Polaroid-style frame with a caption.
- How do you create an animated GIF from multiple images?
⚡ Whisper
Editing images with Python is a superpower. You can process thousands of photos in seconds. Resize for web. Add watermarks. Create collages. Generate contact sheets. Convert formats. Apply effects. Batch process entire folders. The same operations that would take hours manually run in seconds automatically. This is not just convenience. This is scalability. A product catalog with 10,000 images? Process them in a loop. A photo gallery for a client? Generate thumbnails automatically. Social media posts? Create templates and fill them with images. The combination of Pillow’s capabilities and Python’s automation is transformative. Learn to paste, composite, blend. Master the mask. Understand alpha channels. Then build your pipelines. Images are data. Data can be processed. Process them well.