Skip to main content
1 of 2

Python 3 script to make photo collages

I have written a Python script that takes a list of filenames of pictures and generates a photo collage of them. This is my most complex project yet, and I have written it completely by myself (like I always do, but this is way harder than most of my other projects).

It can take images with different aspect ratios and it can take any number of images. It does not crop the images, and it automatically generates a layout so that all of the images can be put into the layout without any of the images overlapping.

It has two modes, with resizing and without resizing.

With resizing, it automatically resizes the image to a certain ratio to a unit area, while maintaining the original aspect ratio. It scales the images according to the position of the image inside the list, namely the lower the index the larger the image will be, it scales the images exponentially.

It also rotates the images randomly, you can control whether it rotates the images or not, the maximum degree of rotation, and a bunch of other parameters as well.

Every time the code is run, the output is completely randomized.

Here is the script:

import cv2
import numpy as np
import random
import rpack
from fractions import Fraction
from math import prod

def resize_guide(image_size, unit_shape, target_ratio):
    aspect_ratio = Fraction(*image_size).limit_denominator()
    horizontal = aspect_ratio.numerator
    vertical = aspect_ratio.denominator
    target_area = prod(unit_shape) * target_ratio
    unit_length = (target_area/(horizontal*vertical))**.5
    return (int(horizontal*unit_length), int(vertical*unit_length))

def make_border(image, value, border=16):
    return cv2.copyMakeBorder(
        image,
        top=border,
        bottom=border,
        left=border,
        right=border,
        borderType=cv2.BORDER_CONSTANT,
        value=value
    )

def rotate_image(image, angle):
    h, w = image.shape[:2]
    cX, cY = (w // 2, h // 2)
    M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
    cos = np.abs(M[0, 0])
    sin = np.abs(M[0, 1])
    nW = int((h * sin) + (w * cos))
    nH = int((h * cos) + (w * sin))
    M[0, 2] += (nW / 2) - cX
    M[1, 2] += (nH / 2) - cY
    return cv2.warpAffine(image, M, (nW, nH))

def make_collage(image_files, output_file,
        exponent=0.8, border=16, max_degree=15, unit_shape=(1280,720),
        resize_images=True, image_border=True, rotate_images=True):
    images = [cv2.imread(name) for name in image_files]
    size_hint = [exponent**i for i in range(len(images))]
    resized_images = []
    
    if resize_images:
        for image, hint in zip(images, size_hint):
            height, width = image.shape[:2]
            guide = resize_guide((width, height), unit_shape, hint)
            resized = cv2.resize(image, guide, interpolation = cv2.INTER_AREA)
            if image_border:
                resized = make_border(resized, (255, 255, 255), border)
            resized_images.append(resized)
        images = resized_images
    else:
        sorted_images = []
        for image in sorted(images, key=lambda x: -prod(x.shape[:2])):
            if image_border:
                image = make_border(image, (255, 255, 255), border)
            sorted_images.append(image)
        images = sorted_images
    
    sizes = []
    processed_images = []
    for image in images:
        if rotate_images:
            image = rotate_image(image, random.randrange(-max_degree, max_degree+1))
        processed = make_border(image, (0,0,0), border)
        processed_images.append(processed)
        height, width = processed.shape[:2]
        sizes.append((width, height))
    
    shapes = [(x, y, w, h) for (x, y), (w, h) in zip(rpack.pack(sizes), sizes)]
    rightmost = sorted(shapes, key=lambda x: -x[0] - x[2])[0]
    bound_width = rightmost[0] + rightmost[2]
    downmost = sorted(shapes, key=lambda x: -x[1] - x[3])[0]
    bound_height = downmost[1] + downmost[3]
    
    collage = np.zeros([bound_height, bound_width, 3],dtype=np.uint8)
    for image, (x, y, w, h) in zip(processed_images, shapes):
        collage[y:y+h, x:x+w] = image
    
    collage = cv2.GaussianBlur(collage, (3,3), cv2.BORDER_DEFAULT)
    cv2.imwrite(output_file, collage)

Example:


                                                                          
from pathlib import Path                                                  
                                                                          
files = [str(i) for i in Path('D:/Selected').glob('*')]                   
                                                                          
make_collage(files[::-1], 'D:/collages/'+random.randbytes(6).hex()+'.png')

enter image description here

0.9 exponent:

enter image description here

The images are a selection of the pictures I have created. They are ordered chronologically.

I don't completely understand how the libraries work, but I know how to put them together, to make them work in concert.

This is the first time I have done something like this, and I really want to know how it can be improved.