Combine all tiff images into one single image

Question:

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.

Asked By: disukumo

||

Answers:

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 3×5 image assuming that the size of each tile image is same.

Answered By: saad_saeed

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 
Answered By: Mark Setchell

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()
Answered By: pippo1980

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()
Answered By: ffsedd

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
    
Answered By: user3731622