-1

I'm learning how to use PIL and I want to concatenate 4 separate images into a grid (all 256x256 PNG). Using PIL (and optionally NumPy). With several examples I've found, I am already able to concatenate images by stacking them all horizontally/vertically or as a grid when I have exactly 4 images.

What I want to be able to do now is to combine up to 4 images into a grid, i.e. passing in anywhere between 1 and 4 images into a function.

img1 img2 img3 img4

Use cases:

1 image 2 images 3 images 4 images
or

My first thought was to split the image list into pairs, concatenate each horizontally, then concatenate the two results vertically, but that feels inefficient.

EDIT: I've gotten this working as I described above using OpenCV because it was easier to work with than PIL. Here's my code:

images = ["images/img1.png", "images/img2.png", "images/img3.png", "images/img4.png"]

def combine(images):
    if len(images) == 1:
        img1 = cv2.imread(images[0])
        return cv2.imwrite("Combined.png", img1)
    elif len(images) == 2:
        img1 = cv2.imread(images[0])
        img2 = cv2.imread(images[1])

        combined_image = cv2.hconcat([img1, img2])
        return cv2.imwrite("Combined.png", combined_image)
    elif len(images) == 3:
        img1 = cv2.imread(images[0])
        img2 = cv2.imread(images[1])
        img3 = cv2.imread(images[2])
        img4 = cv2.imread("images/Blank.png") # Just a transparent PNG

        image_row_1 = cv2.hconcat([img1, img2])
        image_row_2 = cv2.hconcat([img3, img4])
        combined_image = cv2.vconcat([image_row_1, image_row_2])

        return cv2.imwrite("Combined.png", combined_image)
    elif len(images) == 4:
        img1 = cv2.imread(images[0])
        img2 = cv2.imread(images[1])
        img3 = cv2.imread(images[2])
        img4 = cv2.imread(images[3])

        image_row_1 = cv2.hconcat([img1, img2])
        image_row_2 = cv2.hconcat([img3, img4])
        combined_image = cv2.vconcat([image_row_1, image_row_2])

        return cv2.imwrite("Combined.png", combined_image)


combine(images)

Is there a better way?

Sidewinder
  • 369
  • 3
  • 13

2 Answers2

1

If you're interested in an alternative in NumPy. Here is one way of tiling the 1 to 4 images on a 2x2 grid.

Considering an input x defined as an array of shape (n, c, h, w) where n is the number of images (can either be 1, 2, 3, or 4), c the number of channels (here 3) and h, w the height and width of the images (we will stick with 2x2 but it will work with any dimensions.

For demonstration purposes, x is defined as:

x = np.array([
    np.arange(1, 5).reshape((2,2))*1,
    np.arange(1, 5).reshape((2,2))*10,
    np.arange(1, 5).reshape((2,2))*100,
    np.arange(1, 5).reshape((2,2))*1000,
])
x = np.stack([x]*3, axis=1) # make it 3-channel

First fill the input with zeros as a placeholder for missing images. This will ensure we have a shape of 4, c, h, w i.e. four images on dim=0:

x_fill = np.concatenate([x, np.zeros((4-n, c, h, w))])

Start by boardcasting to a 2x2x... shape (by columns! with order='F'):

x_fill = x_fill.reshape((2, 2, c, h, w), order='F')

Finally concatenate axis=3 then axis=1:

grid = np.concatenate(np.concatenate(x_fill, axis=3), axis=1)

Here are the ouputs (only showing the first channel of grid):

  • 1 image:

    array([[[  1.,   2.,   0.,   0.],
            [  3.,   4.,   0.,   0.],
            [  0.,   0.,   0.,   0.],
            [  0.,   0.,   0.,   0.]]])
    
  • 2 images:

    array([[[  1.,   2.,  10.,  20.],
            [  3.,   4.,  30.,  40.],
            [  0.,   0.,   0.,   0.],
            [  0.,   0.,   0.,   0.]]])
    
  • 3 images:

    array([[[  1.,   2.,  10.,  20.],
            [  3.,   4.,  30.,  40.],
            [100., 200.,   0.,   0.],
            [300., 400.,   0.,   0.]]])
    
  • 4 images:

    array([[[  1.,   2.,  10.,  20.],
            [  3.,   4.,  30.,  40.],
            [100., 200.,1000.,2000.],
            [300., 400.,3000.,4000.]]])
    

Here's how the input x looks like:

array([[[[   1,    2],
         [   3,    4]],

        [[   1,    2],
         [   3,    4]],

        [[   1,    2],
         [   3,    4]]],


       [[[  10,   20],
         [  30,   40]],

        [[  10,   20],
         [  30,   40]],

        [[  10,   20],
         [  30,   40]]],


       [[[ 100,  200],
         [ 300,  400]],

        [[ 100,  200],
         [ 300,  400]],

        [[ 100,  200],
         [ 300,  400]]],


       [[[1000, 2000],
         [3000, 4000]],

        [[1000, 2000],
         [3000, 4000]],

        [[1000, 2000],
         [3000, 4000]]]])
Ivan
  • 34,531
  • 8
  • 55
  • 100
  • Hi @Ivan, thank you for your answer! While I try to read and understand this, I've edited my post to add a working example of my current solution that you asked for. My only issue with it is a) it produces a white space where it's supposed to be transparent in the 3-image output, and b) I'm wondering if it's an efficient way of doing things. Let me know what you think. – Sidewinder Dec 28 '20 at 06:38
  • If you want to stick with OpenCV and have a working solution there, then you certainly can. However, I believe processing your data with NumPy will be faster. Don't quote me on that, and it might depend on the type of operations performed. If you really want to know which is faster you can always run both versions and profile/time them for comparison. – Ivan Dec 28 '20 at 08:09
-1
  1. Create a new Image() of desired size that can hold all your images in a desired shape using Image.new()
  2. Paste all images into the larger image in a shape and position you desire using paste() method.

There are some decisions you will have to make like, what if you have 3 images, do you put one beside another and the third below or above them, or you stack them, or put all three side-by-side. Things get complicated if you need to keep their original sizes. Then you must calculate how to arrange them for the grid to stay nice looking, not just stuck one to another.

It is best if you can resize all the subimages to the same size, then you just need a nested loop through lines and columns and paste each in its alloted position.

Dalen
  • 4,128
  • 1
  • 17
  • 35