Combine several images horizontally with Python
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
Test2.jpg
Test3.jpg
test.jpg
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')
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
Test2.jpg
Test3.jpg
Trifecta.jpg:
Trifecta_vertical.jpg
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()
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)
:
pil_grid(dummy_images, 3)
:
pil_grid(dummy_images, 1)
:
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')
Related videos on Youtube
edesz
Updated on July 08, 2022Comments
-
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.
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
.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 almost 9 yearsWhy is there a
for i in xrange(...)
in your code? Shouldn'tpaste
take care of the three image files you specify? -
dermen almost 9 yearsquestion, will your images always be the same size ?
-
jsbueno almost 9 yearspossible duplicate of Python Image Library: How to combine 4 images into a 2 x 2 grid?
-
edesz almost 9 yearsdermen: 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 over 2 yearsThe only reason why this doesn't work is because of your
xrange(0,444,95)
. If you change this toxrange(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 over 2 yearsThanks! 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 howxrange
worked.
-
edesz almost 9 yearsTwo questions: 1.
x_offset = 0
- is this is the stagger between image centers? 2. For a vertical concatenation, how does your approach change? -
edesz almost 9 yearsYour
output = concat_images(output, ...
is what I was looking for when I started searching for a way to do this. Thanks. -
edesz almost 9 yearsThanks a lot. Another good answer. How would
min_shape =....
andimgs_comb....
change for a vertical concatenation? Could you post that here as a comment, or in your reply? -
dermen almost 9 yearsFor vertical , change
hstack
tovstack
. -
dting almost 9 yearsThe 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
asleft
. For vertical concat, keep track of they-offset
, ortop
. Instead ofsum(widths)
andmax(height)
, dosum(heights)
andmax(widths)
and use the second argument of the 2-tuple box. incrementy_offset
byim.size[1]
. -
edesz almost 9 yearsOne 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 almost 9 yearsI 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 over 7 yearsHi 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 over 7 yearsNice 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 almost 6 yearsJaijaba I also ran into the problem you describe, so I edited DTing's solution to use a list comprehension instead of a map.
-
ClementWalter over 5 yearsI had to use list comprehension instead of
map
in python3.6 -
Noctsol over 5 yearsIf 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 almost 5 yearsThis 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 almost 5 yearsNoting the same thing as Naijaba: in python3
map
returns an iterator, however instead of callingmap
twice, you could could create a list from the iteratorlist(map(Image.open, infile_paths))
or use a list comprehension[Image.open(path) for path in infile_paths]
. -
Jakub Bláha over 4 yearsMay I ask why are you importing
sys
? -
Mike de Klerk about 4 yearsSimple and easy. Thanks
-
FlyingZebra1 over 3 yearsI'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