Create a Photo Collage with Python PIL

Published: December 30, 2023

Create a Photo Collage with Python PIL

Imagine you have a bunch of photos with differing size that you'd like to combine into a collage

Bunch of photos

Our task in this post to combine these pictures into a collage like below, while preserving their aspect ratios.

Photo collage

Below code converts image files contained in a folder into a collage:

# Import necessary libraries
import os
from PIL import Image, ImageDraw

# Define folder that has images
directory = "./images"

# Get list of images
images = [i for i in os.listdir(directory) if i.endswith(".jpg") or i.endswith(".jpeg") or i.endswith(".png")]

# Define the size of output collage and the size of 1 image in the collage as (width, height) tuple
# Below values will determine how many images will be put horizontally vs. vertically
# E.g. in below case -  3600/600 = 6  and 1600/4 = 4 - The collage will be 6x4
expected_size_collage = (3600, 1600)
expected_size_image = (600, 400)

# Create collage canvas to paste the images
collage = Image.new("RGBA", expected_size_collage, color=(255,255,255,255))

# Loop through image files
file_count = 0
for h in range(0, expected_size_collage[1], expected_size_image[1]):
    for w in range(0, expected_size_collage[0], expected_size_image[0]):
        # Read image
        file_name = images[file_count]
        path = directory + "/" + file_name
        image = Image.open(path).convert("RGBA")

        # Get the original image width and height
        image_width = image.size[0]
        image_height = image.size[1]

        # Get how the width and height should be
        width_factor = image_width / expected_size_image[0]
        height_factor = image_height / expected_size_image[1]

        # If width and height factors are same, no cropping is needed
        # If not, we need to crop image to the same ratio as expected_size_image
        if width_factor != height_factor:
            # Get the limiting factor
            factor = min(width_factor, height_factor)

            # Calculate the resulting image width and height
            expected_width = round(factor * expected_size_image[0])
            expected_height = round(factor * expected_size_image[1])

            # Get minx, miny, maxx, and maxy coordinates of new image
            start_width = round((image_width - expected_width) / 2)
            start_height = round((image_height - expected_height) / 2)
            end_width = expected_width + round((image_width - expected_width) / 2)
            end_height = expected_height + round((image_height - expected_height) / 2)

            # Crop the image
            image = image.crop((start_width, start_height, end_width, end_height))

        # Once the image is cropped, resize the image
        # Image should have the aspect ratio as the expected_size_image so resize won't disturb the image
        image = image.resize(expected_size_image)

        # Copy image to collage canvas
        collage.paste(image, (w, h))
        file_count += 1

# Save collage
collage.save("collage.png") 

Now let's go through the code step by step to see what it is doing.

1. Import libraries and define parameters

# Import necessary libraries
import os
from PIL import Image, ImageDraw

# Define folder that has images
directory = "./images"

# Get list of images
images = [i for i in os.listdir(directory) if i.endswith(".jpg") or i.endswith(".jpeg") or i.endswith(".png")]

# Define the size of output collage and the size of 1 image in the collage as (width, height) tuple
# Below values will determine how many images will be put horizontally vs. vertically
# E.g. in below case -  3600/600 = 6  and 1600/4 = 4 - The collage will be 6x4
expected_size_collage = (3600, 1600)
expected_size_image = (600, 400)

# Create collage canvas to paste the images
collage = Image.new("RGBA", expected_size_collage, color=(255,255,255,255))

In above code, we import the necessary libraries, including PIL which we'll use for cropping and resizing the images.

Images that end with .png .jpg .jpeg in a defined folder are collected and put into a list.

Then we define the size of the desired collage. Here we have 24 images in the folder and we'd like to have a 6x4 images in the collage.

The tiles (images) are assigned to have 600x400 size, which yields a collage size of 3600x1600. A blank image with this size is initialized. In the next section, we'll be pasting the image tiles into this blank canvas.

2. Process image tiles and create collage

# Loop through image files
file_count = 0
for h in range(0, expected_size_collage[1], expected_size_image[1]):
    for w in range(0, expected_size_collage[0], expected_size_image[0]):
        # Read image
        file_name = images[file_count]
        path = directory + "/" + file_name
        image = Image.open(path).convert("RGBA")

        # Get the original image width and height
        image_width = image.size[0]
        image_height = image.size[1]

        # Get how the width and height should be
        width_factor = image_width / expected_size_image[0]
        height_factor = image_height / expected_size_image[1]

        # If width and height factors are same, no cropping is needed
        # If not, we need to crop image to the same ratio as expected_size_image
        if width_factor != height_factor:
            # Get the limiting factor
            factor = min(width_factor, height_factor)

            # Calculate the resulting image width and height
            expected_width = round(factor * expected_size_image[0])
            expected_height = round(factor * expected_size_image[1])

            # Get minx, miny, maxx, and maxy coordinates of new image
            start_width = round((image_width - expected_width) / 2)
            start_height = round((image_height - expected_height) / 2)
            end_width = expected_width + round((image_width - expected_width) / 2)
            end_height = expected_height + round((image_height - expected_height) / 2)

            # Crop the image
            image = image.crop((start_width, start_height, end_width, end_height))

        # Once the image is cropped, resize the image
        # Image should have the aspect ratio as the expected_size_image so resize won't disturb the image
        image = image.resize(expected_size_image)

        # Copy image to collage canvas
        collage.paste(image, (w, h))
        file_count += 1

Here, we create 2 loops, where the 1st loop tiles images horizontally along a row, and second loop, goes through rows. E.g., at the end of the first loop, we have below image:

Result at the end of first loop

Within the loop 4 actions are conducted:

  • 1. Determine the expected size of the image
  • 2. Crop the image into the expected size
  • 3. Resize the image into the tile size defined in the previous code block (600, 400)
  • 4. Paste tile image into the collage
While actions 1,2, and 4 are straightforward, I'd like focus on action 3, which is not a trivial task when it comes to handling images with varying sizes. E.g., when we have 2 images as below with different sizes, and we want to convert them to a 600x400 image, what is the best way to do it?
Scenery Monkey

Here, the image needs to be converted into an aspect ratio of 600x400. In order to achieve this, we identify how large the actual side is by dividing the actual size with the expected size. Results are stored in width_factor and height_factor variables. Then we use the smaller of these 2 ratios, to identify the smallest multiple of 600x400 we can fit into this image.

E.g. an image with a size 2400x3000 can be fit into an aspect ratio of 600x400 as a 2400x1600 size. This means we need to crop out of 3000 - 1600 = 1400 pixels vertically. When removing pixels, we remain the middle portion of the image.

Once the image is cropped, it can be safely resized to 600x400 as it already has the same aspect ratio. Then the tile image is pasted onto the collage canvas.

3. Save resulting collage

# Save collage
collage.save("collage.png")

With this last bit of code, the collage is saved into the desired location. Note that, since the canvas was started as an RGBA image, it can't be saved as JPG.

Hope you enjoyed this blog post. Some further improvements to this algorithm can be made such as

  • Add gap between images
  • Smartly choose crop area, instead of always remaining the middle part of the image

Any questions? Let me know below in the comment section.

Leave comment

Comments

Check out other blog posts

Create A Simple and Dynamic Tooltip With Svelte and JavaScript

2024/06/19

Create A Simple and Dynamic Tooltip With Svelte and JavaScript

JavaScriptSvelteSimpleDynamicTooltipFront-end
Create an Interactive Map of Tokyo with JavaScript

2024/06/17

Create an Interactive Map of Tokyo with JavaScript

SvelteSVGJavaScriptTailwindInteractive MapTokyoJapanTokyo Metropolitan Area23 Wards
How to Easily Fix Japanese Character Issue in Matplotlib

2024/06/14

How to Easily Fix Japanese Character Issue in Matplotlib

MatplotlibGraphChartPythonJapanese charactersIssueBug
Book Review | Talking to Strangers: What We Should Know about the People We Don't Know by Malcolm Gladwell

2024/06/13

Book Review | Talking to Strangers: What We Should Know about the People We Don't Know by Malcolm Gladwell

Book ReviewTalking to StrangersWhat We Should Know about the People We Don't KnowMalcolm Gladwell
Most Commonly Used 3,000 Kanjis in Japanese

2024/06/07

Most Commonly Used 3,000 Kanjis in Japanese

Most CommonKanji3000ListUsage FrequencyJapaneseJLPTLanguageStudyingWordsKanji ImportanceWord Prevalence
Replace With Regex Using VSCode

2024/06/07

Replace With Regex Using VSCode

VSCodeRegexFindReplaceConditional Replace
Do Not Use Readable Store in Svelte

2024/06/06

Do Not Use Readable Store in Svelte

SvelteReadableWritableState ManagementStoreSpeedMemoryFile Size
Increase Website Load Speed by Compressing Data with Gzip and Pako

2024/06/05

Increase Website Load Speed by Compressing Data with Gzip and Pako

GzipCompressionPakoWebsite Load SpeedSvelteKit
Find the Word the Mouse is Pointing to on a Webpage with JavaScript

2024/05/31

Find the Word the Mouse is Pointing to on a Webpage with JavaScript

JavascriptMousePointerHoverWeb Development
Create an Interactive Map with Svelte using SVG

2024/05/29

Create an Interactive Map with Svelte using SVG

SvelteSVGInteractive MapFront-end
Book Review | Originals: How Non-Conformists Move the World by Adam Grant & Sheryl Sandberg

2024/05/28

Book Review | Originals: How Non-Conformists Move the World by Adam Grant & Sheryl Sandberg

Book ReviewOriginalsAdam Grant & Sheryl SandbergHow Non-Conformists Move the World
How to Algorithmically Solve Sudoku Using Javascript

2024/05/27

How to Algorithmically Solve Sudoku Using Javascript

Solve SudokuAlgorithmJavaScriptProgramming
How I Increased Traffic to my Website by 10x in a Month

2024/05/26

How I Increased Traffic to my Website by 10x in a Month

Increase Website TrafficClicksImpressionsGoogle Search Console
Life is Like Cycling

2024/05/24

Life is Like Cycling

CyclingLifePhilosophySuccess
Generate a Complete Sudoku Grid with Backtracking Algorithm in JavaScript

2024/05/19

Generate a Complete Sudoku Grid with Backtracking Algorithm in JavaScript

SudokuComplete GridBacktracking AlgorithmJavaScript
Why Tailwind is Amazing and How It Makes Web Dev a Breeze

2024/05/16

Why Tailwind is Amazing and How It Makes Web Dev a Breeze

TailwindAmazingFront-endWeb Development
Generate Sitemap Automatically with Git Hooks Using Python

2024/05/15

Generate Sitemap Automatically with Git Hooks Using Python

Git HooksPythonSitemapSvelteKit
Book Review | Range: Why Generalists Triumph in a Specialized World by David Epstein

2024/05/14

Book Review | Range: Why Generalists Triumph in a Specialized World by David Epstein

Book ReviewRangeDavid EpsteinWhy Generalists Triumph in a Specialized World
What is Svelte and SvelteKit?

2024/05/13

What is Svelte and SvelteKit?

SvelteSvelteKitFront-endVite
Internationalization with SvelteKit (Multiple Language Support)

2024/05/12

Internationalization with SvelteKit (Multiple Language Support)

InternationalizationI18NSvelteKitLanguage Support
Reduce Svelte Deploy Time With Caching

2024/05/11

Reduce Svelte Deploy Time With Caching

SvelteEnhanced ImageCachingDeploy Time
Lazy Load Content With Svelte and Intersection Oberver

2024/05/10

Lazy Load Content With Svelte and Intersection Oberver

Lazy LoadingWebsite Speed OptimizationSvelteIntersection Observer
Find the Optimal Stock Portfolio with a Genetic Algorithm

2024/05/10

Find the Optimal Stock Portfolio with a Genetic Algorithm

Stock marketPortfolio OptimizationGenetic AlgorithmPython
Convert ShapeFile To SVG With Python

2024/05/09

Convert ShapeFile To SVG With Python

ShapeFileSVGPythonGeoJSON
Reactivity In Svelte: Variables, Binding, and Key Function

2024/05/08

Reactivity In Svelte: Variables, Binding, and Key Function

SvelteReactivityBindingKey Function
Book Review | The Art Of War by Sun Tzu

2024/05/07

Book Review | The Art Of War by Sun Tzu

Book ReviewThe Art Of WarSun TzuThomas Cleary
Specialists Are Dead. Long Live Generalists!

2024/05/06

Specialists Are Dead. Long Live Generalists!

SpecialistGeneralistParadigm ShiftSoftware Engineering
Analyze Voter Behavior in Turkish Elections with Python

2024/05/03

Analyze Voter Behavior in Turkish Elections with Python

TurkeyAge Analysis2018 ElectionsVoter Behavior
Create Turkish Voter Profile Database With Web Scraping

2024/05/01

Create Turkish Voter Profile Database With Web Scraping

PythonSeleniumWeb ScrapingTurkish Elections
Make Infinite Scroll With Svelte and Tailwind

2024/04/30

Make Infinite Scroll With Svelte and Tailwind

SvelteTailwindInfinite ScrollFront-end
How I Reached Japanese Proficiency In Under A Year

2024/04/29

How I Reached Japanese Proficiency In Under A Year

JapaneseProficiencyJLPTBusiness
Use-ready Website Template With Svelte and Tailwind

2024/04/25

Use-ready Website Template With Svelte and Tailwind

Website TemplateFront-endSvelteTailwind
Lazy Engineers Make Lousy Products

2024/01/29

Lazy Engineers Make Lousy Products

Lazy engineerLousy productStarbucksSBI
On Greatness

2024/01/28

On Greatness

GreatnessMeaning of lifeSatisfactory lifePurpose
Converting PDF to PNG on a MacBook

2024/01/28

Converting PDF to PNG on a MacBook

PDFPNGMacBookAutomator
Recapping 2023: Compilation of 24 books read

2023/12/31

Recapping 2023: Compilation of 24 books read

BooksReading2023Reflections
Detect Device & Browser of Visitors to Your Website

2024/01/09

Detect Device & Browser of Visitors to Your Website

JavascriptDevice DetectionBrowser DetectionWebsite Analytics
Anatomy of a ChatGPT Response

2024/01/19

Anatomy of a ChatGPT Response

ChatGPTLarge Language ModelMachine LearningGenerative AI