Combinez plusieurs images horizontalement avec Python


121

J'essaie de combiner horizontalement certaines images JPEG en Python.

Problème

J'ai 3 images - chacune mesure 148 x 95 - voir ci-joint. Je viens de faire 3 copies de la même image - c'est pourquoi elles sont identiques.

entrez la description de l'image icientrez la description de l'image icientrez la description de l'image ici

Ma tentative

J'essaye de les joindre horizontalement en utilisant le code suivant:

import sys
from PIL import Image

list_im = ['Test1.jpg','Test2.jpg','Test3.jpg']
new_im = Image.new('RGB', (444,95)) #creates a new empty image, RGB mode, and size 444 by 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')

Cependant, cela produit la sortie jointe en tant que test.jpg.

entrez la description de l'image ici

Question

Existe-t-il un moyen de concaténer horizontalement ces images de telle sorte que les sous-images dans test.jpg n'aient pas une image partielle supplémentaire?

Information additionnelle

Je cherche un moyen de concaténer horizontalement n images. J'aimerais utiliser ce code de manière générale donc je préférerais:

  • ne pas coder en dur les dimensions de l'image, si possible
  • spécifier les dimensions sur une ligne afin qu'elles puissent être facilement modifiées

2
Pourquoi y a-t-il un for i in xrange(...)dans votre code? Vous ne devriez pas vous pasteoccuper des trois fichiers image que vous spécifiez?
msw

question, vos images seront-elles toujours de la même taille?
dermen


dermen: oui, les images seront toujours de la même taille. msw: Je ne savais pas comment parcourir les images en boucle, sans laisser d'espace vide entre les deux - mon approche n'est probablement pas la meilleure à utiliser.
edesz

Réponses:


172

Vous pouvez faire quelque chose comme ceci:

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

entrez la description de l'image ici


Le imbriqué pour for i in xrange(0,444,95):colle chaque image 5 fois, décalés de 95 pixels. Chaque itération de boucle externe collée sur la précédente.

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')

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici


Deux questions: 1. x_offset = 0- est-ce le décalage entre les centres d'images? 2. Pour une concaténation verticale, comment votre approche change-t-elle?
edesz

2
Le deuxième argument de coller est une boîte. "L'argument box est soit un 2-tuple donnant le coin supérieur gauche, un 4-tuple définissant les coordonnées de pixel gauche, supérieur, droit et inférieur, ou None (identique à (0, 0))." Donc, dans le 2-tuple, nous utilisons x_offsetas left. Pour un concat vertical, gardez une trace du y-offset, ou top. Au lieu de sum(widths)and max(height), faites sum(heights)et max(widths)et utilisez le deuxième argument de la boîte à 2 tuples. incrémenter y_offsetde im.size[1].
DTing

21
Belle solution. Notez en python3 que les cartes ne peuvent être répétées qu'une seule fois, vous devrez donc refaire images = map (Image.open, image_files) avant de parcourir les images la deuxième fois.
Naijaba

1
Jaijaba J'ai également rencontré le problème que vous décrivez, j'ai donc édité la solution de DTing pour utiliser une compréhension de liste au lieu d'une carte.
Ben Quigley

1
J'ai dû utiliser la compréhension de liste au lieu de mappython3.6
ClementWalter

89

J'essaierais ceci:

import numpy as np
import PIL
from PIL import Image

list_im = ['Test1.jpg', 'Test2.jpg', 'Test3.jpg']
imgs    = [ PIL.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 = PIL.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 = PIL.Image.fromarray( imgs_comb)
imgs_comb.save( 'Trifecta_vertical.jpg' )

Cela devrait fonctionner tant que toutes les images sont de la même variété (toutes RVB, toutes RVBA ou toutes niveaux de gris). Il ne devrait pas être difficile de s'assurer que c'est le cas avec quelques lignes de code supplémentaires. Voici mes images d'exemple et le résultat:

Test1.jpg

Test1.jpg

Test2.jpg

Test2.jpg

Test3.jpg

Test3.jpg

Trifecta.jpg:

images combinées

Trifecta_vertical.jpg

entrez la description de l'image ici


Merci beaucoup. Une autre bonne réponse. Comment cela min_shape =....et imgs_comb....changer pour une concaténation verticale? Pourriez-vous publier cela ici en commentaire ou dans votre réponse?
edesz

3
Pour vertical, passez hstackà vstack.
dermen

Encore une question: votre première image ( Test1.jpg ) est plus grande que les autres images. Dans votre image concaténée finale (horizontale ou verticale), toutes les images sont de la même taille. Pourriez-vous expliquer comment vous avez pu réduire la première image avant de la concaténer?
edesz

J'ai utilisé Image.resizede PIL. min_shapeest un tuple de (min_width, min_height) et (np.asarray( i.resize(min_shape) ) for i in imgs )réduira ensuite toutes les images à cette taille. En fait, cela min_shapepeut être tout (width,height)ce que vous désirez, gardez simplement à l'esprit que l'agrandissement des images basse résolution les rendra floues!
dermen

3
Si vous cherchez simplement à combiner des images ensemble sans aucun détail, c'est probablement la réponse la plus simple et la plus flexible ici. Cela tient compte de la taille d'image, du nombre d'images et des formats d'image. C'était une réponse très bien pensée et EXTRÊMEMENT utile. N'aurait jamais pensé à utiliser numpy. Je vous remercie.
Noctsol

26

Edit: La réponse de DTing est plus applicable à votre question car elle utilise PIL, mais je laisserai cela au cas où vous voudriez savoir comment le faire dans numpy.

Voici une solution numpy / matplotlib qui devrait fonctionner pour N images (uniquement des images couleur) de toute taille / forme.

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

Voici un exemple d'utilisation:

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

entrez la description de l'image ici


Votre output = concat_images(output, ...est ce que je cherchais quand je commencé à chercher un moyen de le faire. Merci.
edesz

Salut ballsatballsdotballs, j'ai une question concernant votre réponse. Si je veux ajouter le sous-titre pour chaque sous-image, comment faire? Merci.
user297850

12

Sur la base de la réponse de DTing, j'ai créé une fonction plus simple à utiliser:

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

Il permet de choisir une couleur de fond et un alignement d'image. Il est également facile de faire de la récursivité:

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')

Exemple d'image concaténée


8

Voici une fonction généralisant les approches précédentes, créant une grille d'images en 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

Cela réduira au minimum chaque ligne et colonne de la grille. Vous ne pouvez avoir qu'une ligne en utilisant pil_grid (images), ou seulement une colonne en utilisant pil_grid (images, 1).

L'un des avantages de l'utilisation de PIL par rapport aux solutions basées sur numpy-array est que vous pouvez traiter des images structurées différemment (comme des images en niveaux de gris ou en palette).

Exemples de sorties

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):

entrez la description de l'image ici

pil_grid(dummy_images, 1):

entrez la description de l'image ici


Cette ligne dans pil_grid: h_sizes, v_sizes = [0] * n_horiz, [0] * (n_images // n_horiz) doit se lire: h_sizes, v_sizes = [0] * n_horiz, [0] * ((n_images // n_horiz) + (1 if n_images % n_horiz > 0 else 0)) Raison: Si la largeur horizontale ne divise pas le nombre d'images en nombres entiers, vous devez tenir compte de la ligne supplémentaire si incomplète.
Bernhard Wagner

3

Si toutes les hauteurs d'image sont identiques,

imgs = [‘a.jpg’, b.jpg’, c.jpg’]
concatenated = Image.fromarray(
  np.concatenate(
    [np.array(Image.open(x)) for x in imgs],
    axis=1
  )
)

peut-être que vous pouvez redimensionner les images avant la concaténation comme ceci,

imgs = [‘a.jpg’, b.jpg’, c.jpg’]
concatenated = Image.fromarray(
  np.concatenate(
    [np.array(Image.open(x).resize((640,480)) for x in imgs],
    axis=1
  )
)

1
Simple et facile. Merci
Mike de Klerk

2

Voici ma solution:

from PIL import Image


def join_images(*rows, bg_color=(0, 0, 0, 0), alignment=(0.5, 0.5)):
    rows = [
        [image.convert('RGBA') for image in row]
        for row
        in rows
    ]

    heights = [
        max(image.height for image in row)
        for row
        in rows
    ]

    widths = [
        max(image.width for image in column)
        for column
        in zip(*rows)
    ]

    tmp = Image.new(
        'RGBA',
        size=(sum(widths), sum(heights)),
        color=bg_color
    )

    for i, row in enumerate(rows):
        for j, image in enumerate(row):
            y = sum(heights[:i]) + int((heights[i] - image.height) * alignment[1])
            x = sum(widths[:j]) + int((widths[j] - image.width) * alignment[0])
            tmp.paste(image, (x, y))

    return tmp


def join_images_horizontally(*row, bg_color=(0, 0, 0), alignment=(0.5, 0.5)):
    return join_images(
        row,
        bg_color=bg_color,
        alignment=alignment
    )


def join_images_vertically(*column, bg_color=(0, 0, 0), alignment=(0.5, 0.5)):
    return join_images(
        *[[image] for image in column],
        bg_color=bg_color,
        alignment=alignment
    )

Pour ces images:

images = [
    [Image.open('banana.png'), Image.open('apple.png')],
    [Image.open('lime.png'), Image.open('lemon.png')],
]

Les résultats ressembleront à:


join_images(
    *images,
    bg_color='green',
    alignment=(0.5, 0.5)
).show()

entrez la description de l'image ici


join_images(
    *images,
    bg_color='green',
    alignment=(0, 0)

).show()

entrez la description de l'image ici


join_images(
    *images,
    bg_color='green',
    alignment=(1, 1)
).show()

entrez la description de l'image ici


1
""" 
merge_image takes three parameters first two parameters specify 
the two images to be merged and third parameter i.e. vertically
is a boolean type which if True merges images vertically
and finally saves and returns the file_name
"""
def merge_image(img1, img2, vertically):
    images = list(map(Image.open, [img1, img2]))
    widths, heights = zip(*(i.size for i in images))
    if vertically:
        max_width = max(widths)
        total_height = sum(heights)
        new_im = Image.new('RGB', (max_width, total_height))

        y_offset = 0
        for im in images:
            new_im.paste(im, (0, y_offset))
            y_offset += im.size[1]
    else:
        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')
    return 'test.jpg'

1
from __future__ import print_function
import os
from pil import Image

files = [
      '1.png',
      '2.png',
      '3.png',
      '4.png']

result = Image.new("RGB", (800, 800))

for index, file in enumerate(files):
path = os.path.expanduser(file)
img = Image.open(path)
img.thumbnail((400, 400), Image.ANTIALIAS)
x = index // 2 * 400
y = index % 2 * 400
w, h = img.size
result.paste(img, (x, y, x + w, y + h))

result.save(os.path.expanduser('output.jpg'))

Production

entrez la description de l'image ici


0

Il suffit d'ajouter aux solutions déjà proposées. Suppose la même hauteur, pas de redimensionnement.

import sys
import glob
from PIL import Image
Image.MAX_IMAGE_PIXELS = 100000000  # For PIL Image error when handling very large images

imgs    = [ Image.open(i) for i in list_im ]

widths, heights = zip(*(i.size for i in imgs))
total_width = sum(widths)
max_height = max(heights)

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

# Place first image
new_im.paste(imgs[0],(0,0))

# Iteratively append images in list horizontally
hoffset=0
for i in range(1,len(imgs),1):
    **hoffset=imgs[i-1].size[0]+hoffset  # update offset**
    new_im.paste(imgs[i],**(hoffset,0)**)

new_im.save('output_horizontal_montage.jpg')

0

ma solution serait:

import sys
import os
from PIL import Image, ImageFilter
from PIL import ImageFont
from PIL import ImageDraw 

os.chdir('C:/Users/Sidik/Desktop/setup')
print(os.getcwd())

image_list= ['IMG_7292.jpg','IMG_7293.jpg','IMG_7294.jpg', 'IMG_7295.jpg' ]

image = [Image.open(x) for x in image_list]  # list
im_1 = image[0].rotate(270)
im_2 = image[1].rotate(270)
im_3 = image[2].rotate(270)
#im_4 = image[3].rotate(270)

height = image[0].size[0]
width = image[0].size[1]
# Create an empty white image frame
new_im = Image.new('RGB',(height*2,width*2),(255,255,255))

new_im.paste(im_1,(0,0))
new_im.paste(im_2,(height,0))
new_im.paste(im_3,(0,width))
new_im.paste(im_4,(height,width))


draw = ImageDraw.Draw(new_im)
font = ImageFont.truetype('arial',200)

draw.text((0, 0), '(a)', fill='white', font=font)
draw.text((height, 0), '(b)', fill='white', font=font)
draw.text((0, width), '(c)', fill='white', font=font)
#draw.text((height, width), '(d)', fill='white', font=font)

new_im.show()
new_im.save('BS1319.pdf')   
[![Laser spots on the edge][1]][1]
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.