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))]
resized_images
= [] if resize_images:
ifresized_images 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))
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(rpack.pack(sizes)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)
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.


