image processing
Python
image concatenation
horizontal merge
PIL library

Combine several images horizontally with Python

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Combining images horizontally is a common task for reporting dashboards, before and after comparisons, and dataset previews. Python with Pillow makes this straightforward, but robust results require decisions about height normalization, background color, and output format. A clean pipeline loads images, aligns dimensions, pastes in sequence, and saves with explicit settings.

Core Sections

Basic horizontal merge with Pillow

The basic algorithm is: load images, compute total width and max height, create a destination canvas, and paste each image at the correct x offset.

python
1from PIL import Image
2
3paths = ["a.png", "b.png", "c.png"]
4images = [Image.open(p) for p in paths]
5
6total_width = sum(img.width for img in images)
7max_height = max(img.height for img in images)
8
9result = Image.new("RGB", (total_width, max_height), color="white")
10
11x = 0
12for img in images:
13    result.paste(img, (x, 0))
14    x += img.width
15
16result.save("combined.png")

This works when mixed heights are acceptable and top alignment is fine.

Normalize heights for cleaner visual output

If source images have very different heights, resize them to a shared height before combining.

python
1from PIL import Image
2
3
4def resize_to_height(img: Image.Image, target_h: int) -> Image.Image:
5    ratio = target_h / img.height
6    new_w = int(img.width * ratio)
7    return img.resize((new_w, target_h), Image.Resampling.LANCZOS)
8
9paths = ["a.jpg", "b.jpg", "c.jpg"]
10originals = [Image.open(p).convert("RGB") for p in paths]
11normalized = [resize_to_height(img, 400) for img in originals]
12
13total_w = sum(img.width for img in normalized)
14canvas = Image.new("RGB", (total_w, 400), "black")
15
16x = 0
17for img in normalized:
18    canvas.paste(img, (x, 0))
19    x += img.width
20
21canvas.save("strip.jpg", quality=95)

This produces consistent row height for presentations and reports.

Add spacing and labels

For comparison boards, add margins between images and optional text labels.

python
1from PIL import Image, ImageDraw
2
3gap = 12
4width = sum(img.width for img in normalized) + gap * (len(normalized) - 1)
5out = Image.new("RGB", (width, 440), "white")
6draw = ImageDraw.Draw(out)
7
8x = 0
9for i, img in enumerate(normalized):
10    out.paste(img, (x, 20))
11    draw.text((x, 0), f"Image {i+1}", fill="black")
12    x += img.width + gap
13
14out.save("labeled.png")

Simple spacing and labels improve readability for non-technical stakeholders.

Memory and batch processing

Large image sets can consume significant memory. Use context managers to close files, process in batches, and avoid keeping many full-resolution images in memory at once.

python
1from PIL import Image
2
3with Image.open("a.png") as img:
4    thumb = img.copy()

For very large workflows, generate intermediate strips and merge strips into a final image. This staged approach reduces peak memory usage.

Output format and color considerations

Use PNG for lossless output and transparency support. Use JPEG for smaller files when slight compression is acceptable. If input color profiles vary, convert consistently to RGB or RGBA before paste operations to avoid unexpected color shifts.

Set output intent based on use case. For web previews, compressed JPEG may be enough. For scientific comparison or design review, lossless PNG is usually better.

Build a reusable merge function for pipelines

For repeated jobs, wrap the merge behavior in a reusable function with explicit options. This makes batch operations consistent and testable.

python
1from PIL import Image
2
3
4def merge_horizontal(paths, output_path, height=None, gap=0, bg="white"):
5    imgs = [Image.open(p).convert("RGB") for p in paths]
6    if height is not None:
7        resized = []
8        for img in imgs:
9            ratio = height / img.height
10            resized.append(img.resize((int(img.width * ratio), height), Image.Resampling.LANCZOS))
11        imgs = resized
12
13    width = sum(i.width for i in imgs) + gap * (len(imgs) - 1)
14    h = max(i.height for i in imgs)
15    canvas = Image.new("RGB", (width, h), bg)
16
17    x = 0
18    for img in imgs:
19        canvas.paste(img, (x, 0))
20        x += img.width + gap
21
22    canvas.save(output_path)

A small utility like this reduces copy and paste errors and keeps style choices consistent across teams.

Common Pitfalls

  • Pasting images with mismatched modes without explicit conversion.
  • Ignoring differing heights and getting uneven or clipped composites.
  • Holding too many high-resolution images in memory at once.
  • Using JPEG when transparency is required.
  • Forgetting to close image resources in long-running scripts.

Summary

  • Pillow can merge images horizontally with a simple paste loop.
  • Normalize heights when visual consistency matters.
  • Add spacing and labels for clear side-by-side comparisons.
  • Manage memory carefully for large image sets.
  • Choose output format based on quality and transparency requirements.

Course illustration
Course illustration

All Rights Reserved.