Combine several images horizontally with Python

237,471

Solution 1

You can do something like this:

import sys
from PIL import Image

images = [Image.open(x) for x in ['Test1.jpg', 'Test2.jpg', 'Test3.jpg']]
widths, heights = zip(*(i.size for i in images))

total_width = sum(widths)
max_height = max(heights)

new_im = Image.new('RGB', (total_width, max_height))

x_offset = 0
for im in images:
  new_im.paste(im, (x_offset,0))
  x_offset += im.size[0]

new_im.save('test.jpg')

Test1.jpg

Test1.jpg

Test2.jpg

Test2.jpg

Test3.jpg

Test3.jpg

test.jpg

enter image description here


The nested for for i in xrange(0,444,95): is pasting each image 5 times, staggered 95 pixels apart. Each outer loop iteration pasting over the previous.

for elem in list_im:
  for i in xrange(0,444,95):
    im=Image.open(elem)
    new_im.paste(im, (i,0))
  new_im.save('new_' + elem + '.jpg')

enter image description here enter image description here enter image description here

Solution 2

I would try this:

import numpy as np
import PIL
from PIL import Image

list_im = ['Test1.jpg', 'Test2.jpg', 'Test3.jpg']
imgs    = [ Image.open(i) for i in list_im ]
# pick the image which is the smallest, and resize the others to match it (can be arbitrary image shape here)
min_shape = sorted( [(np.sum(i.size), i.size ) for i in imgs])[0][1]
imgs_comb = np.hstack( (np.asarray( i.resize(min_shape) ) for i in imgs ) )

# save that beautiful picture
imgs_comb = Image.fromarray( imgs_comb)
imgs_comb.save( 'Trifecta.jpg' )    

# for a vertical stacking it is simple: use vstack
imgs_comb = np.vstack( (np.asarray( i.resize(min_shape) ) for i in imgs ) )
imgs_comb = Image.fromarray( imgs_comb)
imgs_comb.save( 'Trifecta_vertical.jpg' )

It should work as long as all images are of the same variety (all RGB, all RGBA, or all grayscale). It shouldn't be difficult to ensure this is the case with a few more lines of code. Here are my example images, and the result:

Test1.jpg

Test1.jpg

Test2.jpg

Test2.jpg

Test3.jpg

Test3.jpg

Trifecta.jpg:

combined images

Trifecta_vertical.jpg

enter image description here

Solution 3

Edit: DTing's answer is more applicable to your question since it uses PIL, but I'll leave this up in case you want to know how to do it in numpy.

Here is a numpy/matplotlib solution that should work for N images (only color images) of any size/shape.

import numpy as np
import matplotlib.pyplot as plt

def concat_images(imga, imgb):
    """
    Combines two color image ndarrays side-by-side.
    """
    ha,wa = imga.shape[:2]
    hb,wb = imgb.shape[:2]
    max_height = np.max([ha, hb])
    total_width = wa+wb
    new_img = np.zeros(shape=(max_height, total_width, 3))
    new_img[:ha,:wa]=imga
    new_img[:hb,wa:wa+wb]=imgb
    return new_img

def concat_n_images(image_path_list):
    """
    Combines N color images from a list of image paths.
    """
    output = None
    for i, img_path in enumerate(image_path_list):
        img = plt.imread(img_path)[:,:,:3]
        if i==0:
            output = img
        else:
            output = concat_images(output, img)
    return output

Here is example use:

>>> images = ["ronda.jpeg", "rhod.jpeg", "ronda.jpeg", "rhod.jpeg"]
>>> output = concat_n_images(images)
>>> import matplotlib.pyplot as plt
>>> plt.imshow(output)
>>> plt.show()

enter image description here

Solution 4

Here is a function generalizing previous approaches, creating a grid of images in PIL:

from PIL import Image
import numpy as np

def pil_grid(images, max_horiz=np.iinfo(int).max):
    n_images = len(images)
    n_horiz = min(n_images, max_horiz)
    h_sizes, v_sizes = [0] * n_horiz, [0] * (n_images // n_horiz)
    for i, im in enumerate(images):
        h, v = i % n_horiz, i // n_horiz
        h_sizes[h] = max(h_sizes[h], im.size[0])
        v_sizes[v] = max(v_sizes[v], im.size[1])
    h_sizes, v_sizes = np.cumsum([0] + h_sizes), np.cumsum([0] + v_sizes)
    im_grid = Image.new('RGB', (h_sizes[-1], v_sizes[-1]), color='white')
    for i, im in enumerate(images):
        im_grid.paste(im, (h_sizes[i % n_horiz], v_sizes[i // n_horiz]))
    return im_grid

It will shrink each row and columns of the grid to the minimum. You can have only a row by using pil_grid(images), or only a column by using pil_grid(images, 1).

One benefit of using PIL over numpy-array based solutions is that you can deal with images structured differently (like grayscale or palette-based images).

Example outputs

def dummy(w, h):
    "Produces a dummy PIL image of given dimensions"
    from PIL import ImageDraw
    im = Image.new('RGB', (w, h), color=tuple((np.random.rand(3) * 255).astype(np.uint8)))
    draw = ImageDraw.Draw(im)
    points = [(i, j) for i in (0, im.size[0]) for j in (0, im.size[1])]
    for i in range(len(points) - 1):
        for j in range(i+1, len(points)):
            draw.line(points[i] + points[j], fill='black', width=2)
    return im

dummy_images = [dummy(20 + np.random.randint(30), 20 + np.random.randint(30)) for _ in range(10)]

pil_grid(dummy_images):

line.png

pil_grid(dummy_images, 3):

enter image description here

pil_grid(dummy_images, 1):

enter image description here

Solution 5

Based on DTing's answer I created a function that is easier to use:

from PIL import Image


def append_images(images, direction='horizontal',
                  bg_color=(255,255,255), aligment='center'):
    """
    Appends images in horizontal/vertical direction.

    Args:
        images: List of PIL images
        direction: direction of concatenation, 'horizontal' or 'vertical'
        bg_color: Background color (default: white)
        aligment: alignment mode if images need padding;
           'left', 'right', 'top', 'bottom', or 'center'

    Returns:
        Concatenated image as a new PIL image object.
    """
    widths, heights = zip(*(i.size for i in images))

    if direction=='horizontal':
        new_width = sum(widths)
        new_height = max(heights)
    else:
        new_width = max(widths)
        new_height = sum(heights)

    new_im = Image.new('RGB', (new_width, new_height), color=bg_color)


    offset = 0
    for im in images:
        if direction=='horizontal':
            y = 0
            if aligment == 'center':
                y = int((new_height - im.size[1])/2)
            elif aligment == 'bottom':
                y = new_height - im.size[1]
            new_im.paste(im, (offset, y))
            offset += im.size[0]
        else:
            x = 0
            if aligment == 'center':
                x = int((new_width - im.size[0])/2)
            elif aligment == 'right':
                x = new_width - im.size[0]
            new_im.paste(im, (x, offset))
            offset += im.size[1]

    return new_im

It allows choosing a background color and image alignment. It's also easy to do recursion:

images = map(Image.open, ['hummingbird.jpg', 'tiger.jpg', 'monarch.png'])

combo_1 = append_images(images, direction='horizontal')
combo_2 = append_images(images, direction='horizontal', aligment='top',
                        bg_color=(220, 140, 60))
combo_3 = append_images([combo_1, combo_2], direction='vertical')
combo_3.save('combo_3.png')

Example concatenated image

Share:
237,471

Related videos on Youtube

edesz
Author by

edesz

Updated on July 08, 2022

Comments

  • edesz
    edesz almost 2 years

    I am trying to horizontally combine some JPEG images in Python.

    Problem

    I have 3 images - each is 148 x 95 - see attached. I just made 3 copies of the same image - that is why they are the same.

    enter image description hereenter image description hereenter image description here

    My attempt

    I am trying to horizontally join them using the following code:

    import sys
    from PIL import Image
    
    list_im = ['Test1.jpg','Test2.jpg','Test3.jpg']
    
    # creates a new empty image, RGB mode, and size 444 by 95
    new_im = Image.new('RGB', (444,95))
    
    for elem in list_im:
        for i in xrange(0,444,95):
            im=Image.open(elem)
            new_im.paste(im, (i,0))
    new_im.save('test.jpg')
    

    However, this is producing the output attached as test.jpg.

    enter image description here

    Question

    Is there a way to horizontally concatenate these images such that the sub-images in test.jpg do not have an extra partial image showing?

    Additional Information

    I am looking for a way to horizontally concatenate n images. I would like to use this code generally so I would prefer to:

    • not to hard-code image dimensions, if possible
    • specify dimensions in one line so that they can be easily changed
    • msw
      msw almost 9 years
      Why is there a for i in xrange(...) in your code? Shouldn't paste take care of the three image files you specify?
    • dermen
      dermen almost 9 years
      question, will your images always be the same size ?
    • jsbueno
      jsbueno almost 9 years
    • edesz
      edesz almost 9 years
      dermen: yes, images will always be the same size. msw: I wasn't sure how to loop through the images, without leaving a blank space in between - my approach is probably not the best to use.
    • Jonas De Schouwer
      Jonas De Schouwer over 2 years
      The only reason why this doesn't work is because of your xrange(0,444,95). If you change this to xrange(0,444,148) everything should be fine. This is because you split the images horizontally, and the width of one image is 148. (Also, you want to combine 3 images, so it is logical that your range object should contain 3 values.)
    • edesz
      edesz over 2 years
      Thanks! You are right about the width part. I did make use of this in the question, but my reasoning about the step size was wrong - see Image.new('RGB', (444,95))...here, I specified 444 since, as you also quite correctly pointed out, there were three images and each has a width of 148 pixels so the concatenated image width should be 148 X 3 = 444. Nonetheless, you are correct - xrange was incorrectly used. I thought 95 would be the height of the final image, which was a wrong assumption since that is not how xrange worked.
  • edesz
    edesz almost 9 years
    Two questions: 1. x_offset = 0 - is this is the stagger between image centers? 2. For a vertical concatenation, how does your approach change?
  • edesz
    edesz almost 9 years
    Your output = concat_images(output, ... is what I was looking for when I started searching for a way to do this. Thanks.
  • edesz
    edesz almost 9 years
    Thanks a lot. Another good answer. How would min_shape =.... and imgs_comb.... change for a vertical concatenation? Could you post that here as a comment, or in your reply?
  • dermen
    dermen almost 9 years
    For vertical , change hstack to vstack.
  • dting
    dting almost 9 years
    The second argument of paste is a box. "The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the left, upper, right, and lower pixel coordinate, or None (same as (0, 0))." So in the 2-tuple we are using x_offset as left. For vertical concat, keep track of the y-offset, or top. Instead of sum(widths) and max(height), do sum(heights) and max(widths) and use the second argument of the 2-tuple box. increment y_offset by im.size[1].
  • edesz
    edesz almost 9 years
    One more question: Your first image (Test1.jpg) is larger than the other images. In your final (horizontal or vertical) concatenated image, all the images are the same size. Could you explain how you were able to shrink the first image before concatenating it?
  • dermen
    dermen almost 9 years
    I used Image.resize from PIL. min_shape is a tuple of (min_width, min_height) and then (np.asarray( i.resize(min_shape) ) for i in imgs ) will shrink all images to that size. In fact, min_shape can be any (width,height) you desire, just keep in mind that enlarging low-res images will make them blurry!
  • user297850
    user297850 over 7 years
    Hi ballsatballsdotballs, I have one question regarding your answer. If I want to add the sub-title for each sub-images, how to do that? Thanks.
  • Naijaba
    Naijaba over 7 years
    Nice solution. Note in python3 that maps can only be iterated over once, so you'd have to do images = map(Image.open, image_files) again before iterating through the images the second time.
  • Ben Quigley
    Ben Quigley almost 6 years
    Jaijaba I also ran into the problem you describe, so I edited DTing's solution to use a list comprehension instead of a map.
  • ClementWalter
    ClementWalter over 5 years
    I had to use list comprehension instead of map in python3.6
  • Noctsol
    Noctsol over 5 years
    If you are looking to just combine images together without any specifics, this is probably the most simple and most flexible answer here. It accounts for differing image size, any # of images, and varying picture formats. This was a very well thought out answer and EXTREMELY useful. Would have never thought of using numpy. Thank you.
  • Bernhard Wagner
    Bernhard Wagner almost 5 years
    This line in pil_grid: h_sizes, v_sizes = [0] * n_horiz, [0] * (n_images // n_horiz) should read: h_sizes, v_sizes = [0] * n_horiz, [0] * ((n_images // n_horiz) + (1 if n_images % n_horiz > 0 else 0)) Reason: If the horizontal width does not divide the number of images in integers, you need to accomodate for the additional if incomplete line.
  • cheshirekow
    cheshirekow almost 5 years
    Noting the same thing as Naijaba: in python3 map returns an iterator, however instead of calling map twice, you could could create a list from the iterator list(map(Image.open, infile_paths)) or use a list comprehension [Image.open(path) for path in infile_paths].
  • Jakub Bláha
    Jakub Bláha over 4 years
    May I ask why are you importing sys?
  • Mike de Klerk
    Mike de Klerk about 4 years
    Simple and easy. Thanks
  • FlyingZebra1
    FlyingZebra1 over 3 years
    I'm not 100% on where the issue is, but this function does something weird with images, causing the objects i'm iterating over to go from total weight of 25mb to 2gb. so be careful using this method