Combine várias imagens horizontalmente com Python

121

Estou tentando combinar horizontalmente algumas imagens JPEG em Python.

Problema

Eu tenho 3 imagens - cada uma tem 148 x 95 - consulte o anexo. Acabei de fazer 3 cópias da mesma imagem - é por isso que são iguais.

insira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aqui

Minha tentativa

Estou tentando uni-los horizontalmente usando o seguinte código:

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

No entanto, isso está produzindo a saída anexada como test.jpg.

insira a descrição da imagem aqui

Questão

Existe uma maneira de concatenar horizontalmente essas imagens de modo que as subimagens em test.jpg não tenham uma imagem parcial extra aparecendo?

informação adicional

Estou procurando uma maneira de concatenar n imagens horizontalmente. Eu gostaria de usar este código de maneira geral, então prefiro:

  • não codificar as dimensões da imagem, se possível
  • especifique as dimensões em uma linha para que possam ser facilmente alteradas
Edesz
fonte
2
Por que existe um for i in xrange(...)em seu código? Não deveria pastecuidar dos três arquivos de imagem que você especifica?
msw
pergunta, suas imagens serão sempre do mesmo tamanho?
dermen de
dermen: sim, as imagens serão sempre do mesmo tamanho. msw: Eu não tinha certeza de como percorrer as imagens, sem deixar um espaço em branco no meio - minha abordagem provavelmente não é a melhor para usar.
edesz

Respostas:

171

Você pode fazer algo assim:

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

insira a descrição da imagem aqui


O aninhado para for i in xrange(0,444,95):é colar cada imagem 5 vezes, com intervalos de 95 pixels separados. Cada iteração do loop externo é colada sobre a anterior.

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

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

DTing
fonte
Duas perguntas: 1. x_offset = 0- este é o escalonamento entre os centros de imagem? 2. Para uma concatenação vertical, como sua abordagem muda?
edesz
2
O segundo argumento de colar é uma caixa. "O argumento da caixa é uma 2-tupla dando o canto superior esquerdo, uma 4-tupla definindo a coordenada de pixel esquerda, superior, direita e inferior, ou Nenhum (igual a (0, 0))." Portanto, na 2-tupla estamos usando x_offsetcomo left. Para concat vertical, acompanhe o y-offset, ou top. Em vez de sum(widths)e max(height), faça sum(heights)e max(widths)e use o segundo argumento da caixa de 2 tuplas. incrementar y_offsetem im.size[1].
DTing de
21
Ótima solução. Observe em python3 que os mapas só podem ser iterados uma vez, então você teria que fazer images = map (Image.open, image_files) novamente antes de iterar pelas imagens pela segunda vez.
Naijaba de
1
Jaijaba Eu também encontrei o problema que você descreve, então editei a solução do DTing para usar uma compreensão de lista em vez de um mapa.
Ben Quigley
1
Tive que usar a compreensão de lista em vez de mapem python3.6
ClementWalter
89

Eu tentaria isso:

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

Deve funcionar desde que todas as imagens sejam da mesma variedade (todas RGB, RGBA ou em tons de cinza). Não deve ser difícil garantir que este seja o caso com mais algumas linhas de código. Aqui estão minhas imagens de exemplo e o resultado:

Test1.jpg

Test1.jpg

Test2.jpg

Test2.jpg

Test3.jpg

Test3.jpg

Trifecta.jpg:

imagens combinadas

Trifecta_vertical.jpg

insira a descrição da imagem aqui

dermen
fonte
Muito obrigado. Outra boa resposta. Como seria min_shape =....e imgs_comb....mudaria para uma concatenação vertical? Você poderia postar isso aqui como um comentário ou em sua resposta?
edesz
3
Para vertical, mude hstackpara vstack.
dermen de
Mais uma pergunta: sua primeira imagem ( Test1.jpg ) é maior do que as outras imagens. Em sua imagem concatenada final (horizontal ou vertical), todas as imagens têm o mesmo tamanho. Você poderia explicar como conseguiu encolher a primeira imagem antes de concatená-la?
edesz
Eu usei Image.resizedo PIL. min_shapeé uma tupla de (min_width, min_height) e então (np.asarray( i.resize(min_shape) ) for i in imgs )encolherá todas as imagens para esse tamanho. Na verdade, min_shapepode ser o que (width,height)você desejar, mas lembre-se de que ampliar imagens em baixa resolução as tornará desfocadas!
dermen de
3
Se você está procurando apenas combinar imagens sem nenhuma especificação, esta é provavelmente a resposta mais simples e flexível aqui. É responsável por diferentes tamanhos de imagem, qualquer número de imagens e vários formatos de imagem. Esta foi uma resposta muito bem pensada e EXTREMAMENTE útil. Nunca teria pensado em usar numpy. Obrigado.
Noctsol
26

Edit: A resposta do DTing é mais aplicável à sua pergunta, uma vez que usa PIL, mas deixarei isso no caso de você querer saber como fazê-lo em numpy.

Aqui está uma solução numpy / matplotlib que deve funcionar para N imagens (apenas imagens coloridas) de qualquer tamanho / forma.

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

Aqui está um exemplo de uso:

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

insira a descrição da imagem aqui

derricw
fonte
Você output = concat_images(output, ...é o que eu procurava quando comecei a procurar uma maneira de fazer isso. Obrigado.
edesz
Oi ballsatballsdotballs, tenho uma pergunta sobre sua resposta. Se eu quiser adicionar o subtítulo para cada subimagem, como fazer isso? Obrigado.
user297850
12

Com base na resposta da DTing, criei uma função que é mais fácil de usar:

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

Permite escolher a cor de fundo e o alinhamento da imagem. Também é fácil fazer recursão:

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

Exemplo de imagem concatenada

Teekarna
fonte
8

Aqui está uma função generalizando abordagens anteriores, criando uma grade de imagens em 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

Isso reduzirá cada linha e coluna da grade ao mínimo. Você pode ter apenas uma linha usando pil_grid (imagens), ou apenas uma coluna usando pil_grid (imagens, 1).

Um benefício de usar PIL em vez de soluções baseadas em numpy array é que você pode lidar com imagens estruturadas de forma diferente (como escala de cinza ou imagens baseadas em paleta).

Saídas de exemplo

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

insira a descrição da imagem aqui

pil_grid(dummy_images, 1):

insira a descrição da imagem aqui

Máxima
fonte
Esta linha em pil_grid: h_sizes, v_sizes = [0] * n_horiz, [0] * (n_images // n_horiz) deve ser: h_sizes, v_sizes = [0] * n_horiz, [0] * ((n_images // n_horiz) + (1 if n_images % n_horiz > 0 else 0)) Razão: Se a largura horizontal não divide o número de imagens em inteiros, você precisa acomodar para a linha adicional, se incompleta.
Bernhard Wagner
3

Se todas as alturas da imagem forem iguais,

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

talvez você possa redimensionar as imagens antes da concatenação como esta,

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
  )
)
plhn
fonte
1
Simples e fácil. Obrigado
Mike de Klerk
2

Esta é minha solução:

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
    )

Para essas imagens:

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

Os resultados serão semelhantes a:


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

insira a descrição da imagem aqui


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

).show()

insira a descrição da imagem aqui


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

insira a descrição da imagem aqui

Mikhail Gerasimov
fonte
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'
Raj Yadav
fonte
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'))

Resultado

insira a descrição da imagem aqui

Jayesh Baviskar
fonte
0

Apenas adicionando as soluções já sugeridas. Assume a mesma altura, sem redimensionamento.

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')
Kelmok
fonte
0

minha solução seria:

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]
Avral
fonte