1

I have 15 tiles or tiff files a folder and I would like combine it as a single file with all the images as one tiff image. All the tiles should be stitched as a single tiff image. How do I do that?

What I tried so far?

import imageio
import os

path = "path/to/dir"
image_path_list = os.listdir(path)

with imageio.get_writer("new_image.tif") as new_image:
    for image_path in image_path_list:
        image = imageio.imread(path+image_path)
        new_image.append_data(image)

This saves as a separate image in a tiff file. I would like to stitch all the images together and save it like the following:

Desired Output

1,2,3...,15 represent the tiles. Needs to be stitched as a single image.

disukumo
  • 321
  • 6
  • 15
  • Does all images have identical dimensions (width and height)? – Daweo Jul 23 '21 at 14:40
  • @Daweo Yes, all the images have identical dimensions – disukumo Jul 23 '21 at 14:40
  • 1
    You can just do it in your Terminal with **ImageMagick**, no need for Python `magick montage -tile 3x -geometry +0+0 image{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}.tif BigBoy.tif` – Mark Setchell Jul 23 '21 at 15:27
  • 1
    you can use Image.paste() in pillow see: https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=paste()#PIL.Image.Image.paste, first you create an empty image of right size then paste each of your image inside it – pippo1980 Jul 23 '21 at 15:35
  • @Mark Setchell I get the following error if I use ImageMagick```such file or directory @ error/blob.c/OpenBlob/3537. montage: 'BigBoy.tiff' @ error/montage.c/MontageImageCommand/1806.``` How do I solve this? – disukumo Jul 23 '21 at 15:48
  • How are you images named? – Mark Setchell Jul 23 '21 at 15:52
  • @Mark Setchell the images are named the following: ```09A, 09B, 09C, ...``` etc. All are tiff images – disukumo Jul 23 '21 at 15:58
  • Try `magick mogrify -geometry +0+0 -tile 3x 09*tif result.tif` – Mark Setchell Jul 23 '21 at 16:06
  • Mark Setchell I get the following error: ```mogrify: unable to open image '3x': No such file or directory @ error/blob.c/OpenBlob/3537. mogrify: no decode delegate for this image format `' @ error/constitute.c/ReadImage/572. mogrify: unable to open image 'result.tif': No such file or directory @ error/blob.c/OpenBlob/3537. mogrify: unable to open image 'result.tif': No such file or directory @ error/blob.c/OpenBlob/3537.``` – disukumo Jul 23 '21 at 16:28
  • Not sure what's going on. Let's try something simpler. `magick montage 09* result.tif` – Mark Setchell Jul 23 '21 at 16:42
  • You aren't on Windows are you? – Mark Setchell Jul 23 '21 at 16:43

5 Answers5

4

It seems from your comments that you are prepared to consider a non-Python solution, so I used ImageMagick in the Terminal to montage 15 images as follows:

magick montage -tile 3x -geometry +0+0 09*tif result.tif

enter image description here

To demonstrate how you can lay out 5 images across instead of 3, add a different background and affect the horizontal and vertical spacing differently, here is a variation:

magick montage -background magenta -tile 5x -geometry +5+15 09*tif result.tif

enter image description here


Just FYI, I made the 15 randomly coloured blocks like this:

for x in {a..o} ; do magick xc: +noise random -scale 80x50\! 09$x.tif ; done 
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
1

Read all images in a list. Iterate over this list using two nested for loops. One in range of 3 and one in range of 5. Use numpy.hstack() and numpy.vstack() to make a final 3x5 image assuming that the size of each tile image is same.

saad_saeed
  • 163
  • 8
1

given one directory with 15 images of same size

using PIL (pillow), I ended up with:

from PIL import Image


import os

path_to_file ='tiff-files'


images = []



for i in os.listdir(path_to_file):
    with Image.open(path_to_file+'/'+i) as im:
        images.append(im.copy())

    
new_image = Image.new(images[0].mode, (images[0].size[0]*3,images[0].size[1]*5))



new_image.paste(images[0])
new_image.paste(images[1],(images[0].size[0]*1,0))
new_image.paste(images[2],(images[0].size[0]*2,0))
new_image.paste(images[3],(0,images[0].size[1]*1))
new_image.paste(images[4],(images[0].size[0]*1,images[0].size[1]*1))
new_image.paste(images[5],(images[0].size[0]*2,images[0].size[1]*1))
new_image.paste(images[6],(0,images[0].size[1]*2))
new_image.paste(images[7],(images[0].size[0]*1,images[0].size[1]*2))
new_image.paste(images[8],(images[0].size[0]*2,images[0].size[1]*2))
new_image.paste(images[9],(0,images[0].size[1]*3))
new_image.paste(images[10],(images[0].size[0]*1,images[0].size[1]*3))
new_image.paste(images[11],(images[0].size[0]*2,images[0].size[1]*3))
new_image.paste(images[12],(0,images[0].size[1]*4))
new_image.paste(images[13],(images[0].size[0]*1,images[0].size[1]*4))
new_image.paste(images[14],(images[0].size[0]*2,images[0].size[1]*4))

new_image.show()

let me know if it works.....

After Mark Setchell suggestion here a new version, hope it is better

from PIL import Image
import os

path_to_file ='tiff-files'



def stich_tile(path_to_file, xx , yy):
    images = []
    for i in os.listdir(path_to_file):
            images.append(i)

    
    if len(images) >= xx*yy:
        pass
    
    else:
        raise ValueError('not enough images in path_to_file !!!!!!!!!!!')
        
    
    sq_x = xx
    sq_y = yy
    img_x = (Image.open(path_to_file+'/'+images[0]).size[0])
    img_y = (Image.open(path_to_file+'/'+images[0]).size[1])
    img_mode = (Image.open(path_to_file+'/'+images[0]).mode)
    
    new_image = Image.new(img_mode, (img_x*sq_x, img_y*sq_y))
    
    x = 0
    y = 0
    cnt = 0
    for i in images:
        with Image.open(path_to_file+'/'+i) as img:
            new_image.paste(img, (x,y))
            cnt += 1
            x += img_x 
            if cnt == sq_x:
                x = 0
                y += img_y
                cnt = 0
            else:
                pass
                
  
    return new_image
 

stich_tile(path_to_file, 3, 5).show()

And thinking more along the lines of https://stackoverflow.com/a/68468658/2836621

import numpy as np
from PIL import Image
import os

# path_to_file ='tiff-files'

path_to_file ='tiff-files2'

# path_to_file ='tiff-files3'



    

image = []
for i in os.listdir(path_to_file):
    with Image.open(path_to_file+'/'+i) as im:
        image.append(im.copy()) 
        
     


w, h = image[0].size



new_image = np.zeros((4 * h, 3 * w)).astype('uint8')


col = 0
row = -1
for i, img in enumerate(image):
    if not i % 3 :
        row += 1
        col = 0
    img = np.array(img)
    new_image[row * h: (row + 1) * h, col * w: (col + 1) * w] = img
    col += 1




image_pillow = Image.fromarray(new_image, mode = 'L')

image_pillow.save('prova.tif', mode = 'L')


image_pillow.show()

tested with .tif images grayscale 8-bit

modify adding 3 channel for RGB et similia:

new_image = np.zeros((3 * h, 3 * w,3)).astype('uint8')

new_image[row * h: (row + 1) * h,col * w: (col + 1) * w,:] = img

once more the last example as function for 8 bit grayscale images:

import numpy as np
from PIL import Image
import os

path_to_file ='tiff-files'

# path_to_file ='tiff-files2'

# path_to_file ='tiff-files3'

# path_to_file ='tiff-files5'

    
def stich_img(path_to_file, x , y):

    image = []
    for i in os.listdir(path_to_file):
            image.append(path_to_file+'/'+i)
    
    print(image)
         
    if len(image) >= x*y:
        pass
    
    else:
        # raise ValueError('not enough images in path_to_file !!!!!!!!!!!')
        raise ValueError('EXCEPTION not enough images in path_to_file !!!!!!!!!!!', x*y ,'images  needed : ', len(image),'images present !!!')
    
    
    image = image[:x*y] #-----> riduce lista immagini al numero richiesto
    
    
    with Image.open(image[0]) as img0:
        w, h = img0.size
   
    
    
    
    # new_image = np.zeros((4 * h, 3 * w)).astype('uint8')
    new_image = np.zeros((y * h, x * w)).astype('uint8')
    
    
     
    col = 0
    row = -1
    for i, imgs in enumerate(image):
        with Image.open(imgs) as img:
            if not i % x :
                row += 1
                col = 0
            img = np.array(img)
            new_image[row * h: (row + 1) * h, col * w: (col + 1) * w] = img
            col += 1
            
    

    
    image_pillow = Image.fromarray(new_image, mode = 'L')
    
    return image_pillow

img_stiched = stich_img(path_to_file, 3,5)   

# img_stiched.save('prova.tif', mode = 'L')


img_stiched.show()
pippo1980
  • 2,181
  • 3
  • 14
  • 30
  • 2
    Rather than loading all 15 images unnecessarily into memory at once, you would probably do better to load them one at a time and halve the demand on RAM. Also, rather than do 15 repetitive `paste()` calls inline like that, you would do better in a loop, where you just added the width and height as the x and y increment for each iteration. – Mark Setchell Jul 24 '21 at 10:22
  • from here: 'https://legacy.imagemagick.org/discourse-server/viewtopic.php?t=14991' : ImageMagick allocates a number of your images in memory until memory is exhausted and then places the rest on your SSD. .... not sure if it still valid – pippo1980 Jul 24 '21 at 10:31
  • I wasn't talking about ImageMagick, that is an entirely different proposition based very much on *ease of use*, i.e. a single line command in Terminal and no coding knowledge required. I was suggesting that if you plan to go to the trouble of learning, writing and using Python, you can do a lot better than write code that hogs memory and is repetitive, hard to maintain and inflexible in its implementation. – Mark Setchell Jul 24 '21 at 10:42
  • I already started to figure out how to implement your suggestions. One question why images.append(im) instead of images.append(im.copy()) in my code produce a list of images that cannot be shown using .show() ? errors says missing attribute but have size and mode ? – pippo1980 Jul 24 '21 at 10:51
  • @MarkSetchell is it better now ? – pippo1980 Jul 24 '21 at 12:57
  • I was thinking more along these lines... https://stackoverflow.com/a/68468658/2836621 – Mark Setchell Jul 24 '21 at 21:44
  • Can’t figure out what the first : is for in ‘new_image[:, row * h: (row + 1) * h, col * w: (col + 1) * w] = img’ , I’ll try with numpy too . Should it be faster than using image.paste for very big puzzles?? – pippo1980 Jul 24 '21 at 22:01
  • @MarkSetchell added the https://stackoverflow.com/a/68468658/2836621 suggested mode . Is it what was expected ? – pippo1980 Jul 25 '21 at 15:53
  • @pippo1980 that really helps thanks! But, when I stitch the images I get extra blank tiles added after images 3, 6,9,12,15. How do I remove that? – disukumo Jul 28 '21 at 11:49
  • which script are you talking about 1st 2nd or 3rd ? for 3rd one you need to adjust this line: new_image = np.zeros((4 * h, 3 * w)).astype('uint8') with (5* h, 3*w) to match your example – pippo1980 Jul 28 '21 at 13:33
  • 1
    3rd one uses pillow instead of imageio, pillow does swap x and y axes when passing it to numpy array with: img_numpy = numpy.array(imge_opened_with_pillow_Image.open) – pippo1980 Jul 28 '21 at 13:47
  • @disukumo added stich(path_to_file, x , y) version of last script – pippo1980 Jul 28 '21 at 13:56
  • @pippo1980 I was talking about the first script – disukumo Jul 28 '21 at 14:37
  • Sorry, it was the one with stich_tile(path_to_file, xx , yy). It was very much helpful for me. But, I don't know how to remove the black tiles that were added to the image while stitching. – disukumo Jul 28 '21 at 14:51
  • @disukumo tried it with my test panel of images both showing and savit it I don't have any extra tiles ?? I'll think about it talking about first script – pippo1980 Jul 28 '21 at 14:51
  • @pippo1980 no I don't have extra tile. But, I tried to save the stitched image using stich_tile(path_to_file, 3, 5).save('filename.tiff'). Could that be a problem? – disukumo Jul 28 '21 at 14:53
  • @disukumo use pippo = stich_tile(path_to_file, 3, 5) [run func] --- pippo.show() [show image returned by func ] ---- pippo.save('2nd_script_15_img.tiff') -- [save image] -- but your one work the same – pippo1980 Jul 28 '21 at 15:01
1

Using numpy: This script accepts generator of images (to work faster with large images). It does not check their size in advance. If image height does not fit row height or if rows have not the same width, it will fail.

    #!/usr/bin/env python3
import numpy as np
from imageio import imread, imwrite
from pathlib import Path


def tile_images(images, cols):
    """Tile images of same size to grid with given number of columns.
    
    Args:
        images (collection of ndarrays)
        cols (int): number of colums 
    
    Returns:
        ndarray: stitched image
    """
    images = iter(images)
    first = True
    rows = []
    i = 0
    while True:
        
        try:
            im = next(images)
            print(f"add image, shape: {im.shape}, type: {im.dtype}")
        except StopIteration:
            if first:
                break
            else:
                im = np.zeros_like(im)  # black background
                
        if first:
            row = im  # start next row
            first = False  
        else:    
            row = np.concatenate((row, im), axis=1)  # append to row
            
        i += 1
        if not i % cols:
            print(f"row done, shape: {row.shape}")
            rows.append(row) # finished row
            first = True
            
    tiled = np.concatenate(rows)   # stitch rows    
    return tiled        

def main():
    images = (imread(f) for f in Path().glob("*.*") if f.suffix in (".jpg", ".png") if f.name != "new.png") 
    new = tile_images(images, cols=3)
    imwrite("new.png", new)


def test():
    im1 = np.arange(65536).reshape(256,256)
    im2 = np.arange(65536/2).reshape(128,256)
    
    images = [im1,im1,im1,im2,im2,im2]
    
    # works
    new = tile_images(images, 3)
    imwrite("new.png", new)
    
    # failes
    new = tile_images(images, 2)
    imwrite("new2.png", new)
    
    
if __name__ == "__main__":
    main()
    # test()
ffsedd
  • 186
  • 9
  • Thanks for the help. But, I get the following error. ```ValueError: all the input array dimensions for the concatenation axis must match exactly, but along dimension 0, the array at index 0 has size 776 and the array at index 1 has size 5000``` How do I solve it? – disukumo Jul 27 '21 at 07:20
  • 1
    Make sure all images in folder are of same size. This script doesn't care. It accepts generator of images, so it is faster, but it does not check the size of images in advance. It tries to stitch files and if height of images in one row differ or if width of rows differ, it will fail. I excluded "new.png" form image list, maybe this was the image with differnent size. – ffsedd Jul 27 '21 at 08:59
0

The following elaborates on @saad_saeed answer.

Note, the following will break:

  1. if your list_of_images doesn't have enough images to build the num_mosaic_rows x num_mosaic_cols mosaic. I've left it to the user to add the handling of this (e.g. adding an if/else).

  2. if each img in your list_of_images doesn't have the same shape

    def build_mosaic(list_of_images, num_mosaic_rows, num_mosaic_cols):
    
        list_of_mosaic_rows = []
    
        for row_number in range(num_mosaic_rows):
    
            list_of_mosaic_rows = list_of_images[row_number*num_mosaic_cols,(row_number+1)*num_mosaic_cols]
    
        mosaic = np.vstack(list_of_mosaic_rows)
    
        return mosaic
    
user3731622
  • 4,844
  • 8
  • 45
  • 84