Combinar varias imágenes horizontalmente con Python

121

Estoy tratando de combinar horizontalmente algunas imágenes JPEG en Python.

Problema

Tengo 3 imágenes - cada una mide 148 x 95 - ver adjunto. Acabo de hacer 3 copias de la misma imagen, por eso son iguales.

ingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquí

Mi intento

Estoy tratando de unirlos horizontalmente usando el siguiente 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')

Sin embargo, esto produce la salida adjunta como test.jpg.

ingrese la descripción de la imagen aquí

Pregunta

¿Hay alguna forma de concatenar horizontalmente estas imágenes de modo que las subimágenes en test.jpg no muestren una imagen parcial adicional?

Información Adicional

Estoy buscando una forma de concatenar horizontalmente n imágenes. Me gustaría usar este código en general, así que preferiría:

  • no codificar las dimensiones de la imagen, si es posible
  • especificar dimensiones en una línea para que se puedan cambiar fácilmente
edesz
fuente
2
¿Por qué hay un for i in xrange(...)en su código? ¿No debería pasteocuparse de los tres archivos de imagen que especifica?
msw
pregunta, ¿sus imágenes serán siempre del mismo tamaño?
dermen
posible duplicado de la biblioteca de imágenes
jsbueno
dermen: sí, las imágenes siempre serán del mismo tamaño. msw: No estaba seguro de cómo recorrer las imágenes sin dejar un espacio en blanco entre ellas; mi enfoque probablemente no sea el mejor para usar.
edesz

Respuestas:

171

Puedes hacer algo como esto:

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

ingrese la descripción de la imagen aquí


El anidado for i in xrange(0,444,95):es pegar cada imagen 5 veces, escalonadas a 95 píxeles de distancia. Cada iteración del bucle externo se pega sobre la 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')

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

DTing
fuente
Dos preguntas: 1. x_offset = 0- ¿Es este el escalonamiento entre los centros de imagen? 2. Para una concatenación vertical, ¿cómo cambia su enfoque?
edesz
2
El segundo argumento de pegar es una caja. "El argumento de cuadro es una tupla de 2 que da la esquina superior izquierda, una tupla de 4 que define las coordenadas de píxeles izquierda, superior, derecha e inferior, o Ninguno (igual que (0, 0))". Entonces, en la tupla de 2, estamos usando x_offsetcomo left. Para concat vertical, realice un seguimiento de y-offset, o top. En lugar de sum(widths)y max(height), haga sum(heights)y max(widths)utilice el segundo argumento del cuadro de 2 tuplas. Incrementar y_offsetpor im.size[1].
DTing
21
Buena solucion. Tenga en cuenta en python3 que los mapas solo se pueden iterar una vez, por lo que tendría que hacer images = map (Image.open, image_files) nuevamente antes de iterar a través de las imágenes la segunda vez.
Naijaba
1
Jaijaba También me encontré con el problema que describe, así que edité la solución de DTing para usar una lista de comprensión en lugar de un mapa.
Ben Quigley
1
Tuve que usar la comprensión de listas en lugar de mapen python3.6
ClementWalter
89

Probaría esto:

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

Debería funcionar siempre que todas las imágenes sean de la misma variedad (todas RGB, todas RGBA o todas en escala de grises). No debería ser difícil asegurarse de que este sea el caso con algunas líneas más de código. Aquí están mis imágenes de ejemplo y el resultado:

Test1.jpg

Test1.jpg

Test2.jpg

Test2.jpg

Test3.jpg

Test3.jpg

Trifecta.jpg:

imágenes combinadas

Trifecta_vertical.jpg

ingrese la descripción de la imagen aquí

dermen
fuente
Muchas gracias. Otra buena respuesta. ¿Cómo cambiaría min_shape =....y imgs_comb....por una concatenación vertical? ¿Podrías publicar eso aquí como comentario o en tu respuesta?
edesz
3
Para vertical, cambie hstacka vstack.
dermen
Una pregunta más: su primera imagen ( Test1.jpg ) es más grande que las otras imágenes. En su imagen concatenada final (horizontal o vertical), todas las imágenes tienen el mismo tamaño. ¿Podrías explicar cómo pudiste encoger la primera imagen antes de concatenarla?
edesz
Solía Image.resizedel PIL. min_shapees una tupla de (min_width, min_height) y luego (np.asarray( i.resize(min_shape) ) for i in imgs )encogerá todas las imágenes a ese tamaño. De hecho, min_shapepuede ser cualquiera (width,height)que desee, solo tenga en cuenta que ampliar las imágenes de baja resolución las hará borrosas.
dermen
3
Si está buscando simplemente combinar imágenes sin ningún detalle específico, esta es probablemente la respuesta más simple y flexible aquí. Tiene en cuenta los diferentes tamaños de imagen, cualquier número de imágenes y diferentes formatos de imagen. Esta fue una respuesta muy bien pensada y EXTREMADAMENTE útil. Nunca hubiera pensado en usar numpy. Gracias.
Noctsol
26

Editar: la respuesta de DTing es más aplicable a su pregunta ya que usa PIL, pero dejaré esto en caso de que quiera saber cómo hacerlo en numpy.

Aquí hay una solución numpy / matplotlib que debería funcionar para N imágenes (solo imágenes en color) de cualquier tamaño / 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

Aquí hay un ejemplo 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()

ingrese la descripción de la imagen aquí

derricw
fuente
Tu output = concat_images(output, ...es lo que estaba buscando cuando comencé a buscar una forma de hacer esto. Gracias.
edesz
Hola ballsatballsdotballs, tengo una pregunta con respecto a tu respuesta. Si quiero agregar el subtítulo para cada subimagen, ¿cómo puedo hacerlo? Gracias.
user297850
12

Basado en la respuesta de DTing, creé una función que es más 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 elegir un color de fondo y la alineación de la imagen. También es fácil hacer recursividad:

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

Ejemplo de imagen concatenada

teekarna
fuente
8

Aquí hay una función que generaliza enfoques anteriores, creando una cuadrícula de imágenes 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

Reducirá cada fila y columna de la cuadrícula al mínimo. Puede tener solo una fila usando pil_grid (imágenes), o solo una columna usando pil_grid (imágenes, 1).

Un beneficio de usar PIL sobre las soluciones basadas en matrices numpy es que puede manejar imágenes estructuradas de manera diferente (como imágenes en escala de grises o basadas en paletas).

Salidas de ejemplo

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

ingrese la descripción de la imagen aquí

pil_grid(dummy_images, 1):

ingrese la descripción de la imagen aquí

Máxima
fuente
Esta línea en pil_grid: h_sizes, v_sizes = [0] * n_horiz, [0] * (n_images // n_horiz) debería leer: h_sizes, v_sizes = [0] * n_horiz, [0] * ((n_images // n_horiz) + (1 if n_images % n_horiz > 0 else 0)) Razón: Si el ancho horizontal no divide el número de imágenes en números enteros, debe acomodar la línea adicional si está incompleta.
Bernhard Wagner
3

Si todas las alturas de la imagen son iguales,

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

tal vez puedas cambiar el tamaño de las imágenes antes de la concatenación de esta manera,

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
fuente
1
Simple y fácil. Gracias
Mike de Klerk
2

Esta es mi solución:

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 estas imágenes:

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

Los resultados se verán así:


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

ingrese la descripción de la imagen aquí


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

).show()

ingrese la descripción de la imagen aquí


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

ingrese la descripción de la imagen aquí

Mikhail Gerasimov
fuente
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
fuente
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'))

Salida

ingrese la descripción de la imagen aquí

Jayesh Baviskar
fuente
0

Simplemente agregando a las soluciones ya sugeridas. Asume la misma altura, sin cambiar el tamaño.

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
fuente
0

mi solución sería:

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
fuente