Making a collage in PIL

Question:

I. Am. Stuck.

I have been working on this for over a week now, and I cannot seem to get my code to run correctly. I am fairly new to PIL and Python as a whole. I am trying to make a 2×3 collage of some pictures. I have my code listed below. I am trying to get my photos to fit without any access black space in the newly created collage, however when I run my code I can only get 2 pictures to be placed into the collage, instead of the 6 I want. Any suggestions would be helpful.

*CODE EDITED

from PIL import Image
im= Image.open('Tulips.jpg')




out=im.convert("RGB", (
    0.412453, 0.357580, 0.180423, 0,
    0.212671, 0.715160, 0.072169, 0,
    0.019334, 0.119193, 0.950227, 0 ))
out.save("Image2" + ".jpg")

out2=im.convert("RGB", (
    0.9756324, 0.154789, 0.180423, 0,
    0.212671, 0.715160, 0.254783, 0,
    0.123456, 0.119193, 0.950227, 0 ))
out2.save("Image3" + ".jpg")



out3= im.convert("1")
out3.save("Image4"+".jpg")


out4=im.convert("RGB", (
    0.986542, 0.154789, 0.756231, 0,
    0.212671, 0.715160, 0.254783, 0,
    0.123456, 0.119193, 0.112348, 0 ))
out4.save("Image5" + ".jpg")


out5=Image.blend(im, out4, 0.5)
out5.save("Image6" + ".jpg")

listofimages=['Tulips.jpg', 'Image2.jpg', 'Image3.jpg', 'Image4.jpg', 'Image5.jpg', 'Image6.jpg']

def create_collage(width, height, listofimages):
    Picturewidth=width//3
    Pictureheight=height//2
    size=Picturewidth, Pictureheight
    new_im=Image.new('RGB', (450, 300))
    for p in listofimages:
        Image.open(p)
    for col in range(0,width):
        for row in range(0, height):
                image=Image.eval(p, lambda x: x+(col+row)/30)
                new_im.paste(p, (col,row))
                new_im.save("Collage"+".jpg")

create_collage(450,300,listofimages)
Asked By: python_newbie

||

Answers:

Here’s some working code.

  1. When you call Image.open(p), that returns an Image object, so you need to store than in a variable: im = Image.open(p).

  2. I’m not sure what image=Image.eval(p, lambda x: x+(col+row)/30) is meant to do so I removed it.

  3. size is the size of the thumbnails, but you’re not using that variable. After opening the image, it should be resized to size.

  4. I renamed Picturewidth and Pictureheight to thumbnail_width and thumbnail_height to make it clear what they are and follow Python naming conventions.

  5. I also moved the number of cols and rows to variables so they can be reused without magic numbers.

  6. The first loop opens each image into an im, thumbnails it and puts it in a list of ims.

  7. Before the next loops we initialise i,x, andy` variables to keep track of which image we’re looking at, and the x and y coordinates to paste the thumbnails into the larger canvas. They’ll be updated in the next loops.

  8. The first loop is for columns (cols), not pixels (width). (Also range(0, thing) does the same as range(thing).)

  9. Similarly the second loop is for rows instead of pixels. Inside this loop we paste the current image at ims[i] into the big new_im at x, y. These are pixel positions, not row/cols positions.

  10. At the end of the inner loop, increment the i counter, and add thumbnail_height to y.

  11. Similarly, at the end of the outer loop, and add thumnnail_width to x and reset y to zero.

  12. You only need to save new_im once, after these loops have finished.

  13. There’s no need for concatenating "Image2" + ".jpg" etc., just do “Image2.jpg”.

This results in something like this:

Collage.jpg

This code could be improved. For example, if you don’t need them for anything else, there’s no need to save the intermediate ImageX.jpg files, and rather than putting those filenames in listofimages, put the images directly there: listofimages = [im, out1, out2, etc...], and then replace for p in listofimages: with for im in listofimages: and remove im = Image.open(p).

You could also calculate some padding for the images so the blackspace is even.

from PIL import Image
im= Image.open('Tulips.jpg')

out=im.convert("RGB", (
    0.412453, 0.357580, 0.180423, 0,
    0.212671, 0.715160, 0.072169, 0,
    0.019334, 0.119193, 0.950227, 0 ))
out.save("Image2.jpg")

out2=im.convert("RGB", (
    0.9756324, 0.154789, 0.180423, 0,
    0.212671, 0.715160, 0.254783, 0,
    0.123456, 0.119193, 0.950227, 0 ))
out2.save("Image3.jpg")

out3= im.convert("1")
out3.save("Image4.jpg")

out4=im.convert("RGB", (
    0.986542, 0.154789, 0.756231, 0,
    0.212671, 0.715160, 0.254783, 0,
    0.123456, 0.119193, 0.112348, 0 ))
out4.save("Image5.jpg")

out5=Image.blend(im, out4, 0.5)
out5.save("Image6.jpg")

listofimages=['Tulips.jpg', 'Image2.jpg', 'Image3.jpg', 'Image4.jpg', 'Image5.jpg', 'Image6.jpg']

def create_collage(width, height, listofimages):
    cols = 3
    rows = 2
    thumbnail_width = width//cols
    thumbnail_height = height//rows
    size = thumbnail_width, thumbnail_height
    new_im = Image.new('RGB', (width, height))
    ims = []
    for p in listofimages:
        im = Image.open(p)
        im.thumbnail(size)
        ims.append(im)
    i = 0
    x = 0
    y = 0
    for col in range(cols):
        for row in range(rows):
            print(i, x, y)
            new_im.paste(ims[i], (x, y))
            i += 1
            y += thumbnail_height
        x += thumbnail_width
        y = 0

    new_im.save("Collage.jpg")

create_collage(450, 300, listofimages)
Answered By: Hugo

I made a solution inspired by @Hugo’s answer which only requires the input list of images. The function automatically creates a grid based on the number of images input.


def find_multiples(number : int):
    multiples = set()
    for i in range(number - 1, 1, -1):
        mod = number % i
        if mod == 0:
            tup = (i, int(number / i))
            if tup not in multiples and (tup[1], tup[0]) not in multiples:
                multiples.add(tup)
                
    if len(multiples) == 0:
        mod == number % 2
        div = number // 2
        multiples.add((2, div + mod))
        
    return list(multiples)

def get_smallest_multiples(number : int, smallest_first = True) -> Tuple[int, int]:
    multiples = find_multiples(number)
    smallest_sum = number
    index = 0
    for i, m in enumerate(multiples):
        sum = m[0] + m[1]
        if sum < smallest_sum:
            smallest_sum = sum
            index = i
            
    result = list(multiples[i])
    if smallest_first:
        result.sort()
        
    return result[0], result[1]
    

def create_collage(listofimages : List[str], n_cols : int = 0, n_rows: int = 0, 
                   thumbnail_scale : float = 1.0, thumbnail_width : int = 0, thumbnail_height : int = 0):
    
    n_cols = n_cols if n_cols >= 0 else abs(n_cols)
    n_rows = n_rows if n_rows >= 0 else abs(n_rows)
    
    if n_cols == 0 and n_rows != 0:
        n_cols = len(listofimages) // n_rows
        
    if n_rows == 0 and n_cols != 0:
        n_rows = len(listofimages) // n_cols
        
    if n_rows == 0 and n_cols == 0:
        n_cols, n_rows = get_smallest_multiples(len(listofimages))
    
    thumbnail_width = 0 if thumbnail_width == 0 or n_cols == 0 else round(thumbnail_width / n_cols)
    thumbnail_height = 0 if thumbnail_height == 0 or n_rows == 0 else round(thumbnail_height/n_rows)
    
    all_thumbnails : List[Image.Image] = []
    for p in listofimages:
        thumbnail = Image.open(p)
        if thumbnail_width * thumbnail_scale < thumbnail.width:
            thumbnail_width = round(thumbnail.width * thumbnail_scale)
        if thumbnail_height * thumbnail_scale < thumbnail.height:
            thumbnail_height = round(thumbnail.height * thumbnail_scale)
        
        thumbnail.thumbnail((thumbnail_width, thumbnail_height))
        all_thumbnails.append(thumbnail)

    new_im = Image.new('RGB', (thumbnail_width * n_cols, thumbnail_height * n_rows), 'white')
    
    i, x, y = 0, 0, 0
    for col in range(n_cols):
        for row in range(n_rows):
            if i > len(all_thumbnails) - 1:
                continue
            
            print(i, x, y)
            new_im.paste(all_thumbnails[i], (x, y))
            i += 1
            y += thumbnail_height
        x += thumbnail_width
        y = 0

    extension = os.path.splitext(listofimages[0])[1]
    if extension == "":
        extension = ".jpg"
    destination_file = os.path.join(os.path.dirname(listofimages[0]), f"Collage{extension}")
    new_im.save(destination_file)

Example usage:

listofimages=['Tulips.jpg', 'Image2.jpg', 'Image3.jpg', 'Image4.jpg', 'Image5.jpg', 'Image6.jpg']
create_collage(listofimages)

In this case, because the input images are 6, the function returns a 3×2 (3 rows, 2 columns) collage of the images.

To do so, the function finds the two smallest integer multiples of the length of the input list of graphs (e.g. for 12, it returns 3 and 4 rather than 2 and 6) and creates a grid, where the first number is always the smallest of the multiples and it is taken to be the number of columns (i.e. by default the grid gets fewer columns than rows; for 12 images, you get a 4×3 matrix: 4 rows, 3 columns). This it can be customized via the smallest_first argument (only exposed in get_smallest_multiples()).
Optional arguments also allow to force a number of rows/columns.

The final image size is the sum of the sizes of the single images, but an optional thumbnail_scale argument allows to specify a percentage of scaling for all the thumbnails (defaults to 1.0, i.e. 100%, no scaling).

This function works well when the size of the images are all roughly the same. I have not covered more complex scenarios.

Answered By: alelom