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, limit_shape=False):
images = [cv2.imread(name) for name in image_files]
size_hint = [exponent**i for i in range(len(images))]
if resize_images:
resized_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))
if limit_shape:
max_side = int((sum([w*h for w, h in sizes])*2)**.5)
packed = rpack.pack(sizes, max_width=max_side, max_height=max_side)
else:
packed = rpack.pack(sizes)
shapes = [(x, y, w, h) for (x, y), (w, h) in zip(packed, 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')
0.9 exponent:
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.
Update:
I added some logic that constraints the maximum side the bounding area can have, so that the output will roughly be square-like.
Basically it takes the sum of the areas, multiply the sum by two, and takes the square root as maximum width and height.
It is still experimental so it is disabled by default, but so far I have not encountered any problems.
Below is a selection of my photos edited by me, arranged using the new method, I have resized and compressed the image to make it fit here, but I did not edit the contents.