Объедините несколько изображений по горизонтали с помощью Python

Я пытаюсь горизонтально объединить некоторые изображения JPEG в Python.

Проблема

У меня есть 3 изображения - каждое 148 x 95 - смотрите в приложении. Я только что сделал 3 копии одного и того же изображения - поэтому они одинаковые.

введите описание изображения здесь введите описание изображения здесь  введите описание изображения здесь

Моя попытка

Я пытаюсь соединить их по горизонтали, используя следующий код:

import sys
from PIL import Image

list_im = ['Test1.jpg','Test2.jpg','Test3.jpg']

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

Однако это приводит к выводу, прикрепленному как test.jpg.

введите описание изображения здесь

Вопрос

Есть ли способ объединить эти изображения по горизонтали, чтобы на суб-изображениях в test.jpg не отображалось дополнительное частичное изображение?

Дополнительная информация

Я ищу способ объединить n изображений по горизонтали. Я хотел бы использовать этот код в целом, поэтому я бы предпочел:

  • не жестко кодировать размеры изображения, если это возможно
  • укажите размеры в одну строку, чтобы их можно было легко изменить

person edesz    schedule 14.05.2015    source источник
comment
Почему в вашем коде есть for i in xrange(...)? Разве paste не следует позаботиться о трех указанных вами файлах изображений?   -  person msw    schedule 14.05.2015
comment
вопрос, будут ли ваши изображения всегда одного размера?   -  person dermen    schedule 14.05.2015
comment
возможный дубликат Python Библиотека изображений: как объединить 4 изображения в сетку 2 x 2?   -  person jsbueno    schedule 14.05.2015
comment
dermen: да, изображения всегда будут одного размера. msw: Я не знал, как перебирать изображения в цикле, не оставляя пробелов между ними - мой подход, вероятно, не самый лучший для использования.   -  person edesz    schedule 15.05.2015


Ответы (10)


Вы можете сделать что-то вроде этого:

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

введите описание изображения здесь


Вложенный для for i in xrange(0,444,95): - каждое изображение вставляется 5 раз с шагом 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('new_' + elem + '.jpg')

введите описание изображения здесь введите описание изображения здесь  введите описание изображения здесь

person dting    schedule 14.05.2015
comment
Два вопроса: 1. x_offset = 0 - это расхождение между имидж-центрами? 2. Как изменится ваш подход к вертикальной конкатенации? - person edesz; 14.05.2015
comment
Второй аргумент вставки - это коробка. Аргумент box - это либо кортеж из двух элементов, задающий верхний левый угол, либо кортеж из четырех элементов, определяющий координаты левого, верхнего, правого и нижнего пикселей, либо None (то же, что и (0, 0)). Итак, в кортеже из 2 мы используем x_offset как left. Для вертикального объединения следите за y-offset или top. Вместо sum(widths) и max(height) выполните sum(heights) и max(widths) и используйте второй аргумент поля с двумя кортежами. увеличить y_offset на im.size[1]. - person dting; 14.05.2015
comment
Хорошее решение. Обратите внимание, что в python3 карты можно повторять только один раз, поэтому вам придется снова выполнить images = map (Image.open, image_files), прежде чем повторять изображения во второй раз. - person Naijaba; 11.01.2017
comment
Jaijaba Я также столкнулся с проблемой, которую вы описываете, поэтому отредактировал решение DTing, чтобы использовать понимание списка вместо карты. - person Ben Quigley; 29.06.2018
comment
Мне пришлось использовать понимание списка вместо map в python3.6 - person ClementWalter; 30.11.2018
comment
Отмечая то же, что и Naijaba: в python3 map возвращает итератор, однако вместо двойного вызова map вы можете создать список из итератора list(map(Image.open, infile_paths)) или использовать понимание списка [Image.open(path) for path in infile_paths]. - person cheshirekow; 20.07.2019
comment
Могу я спросить, почему вы импортируете sys? - person Jakub Bláha; 04.12.2019

Я бы попробовал это:

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

Он должен работать, пока все изображения одного и того же типа (все RGB, все RGBA или все оттенки серого). Убедиться в этом с помощью еще нескольких строк кода не составит труда. Вот мои примеры изображений и результат:

Test1.jpg

Test1.jpg

Test2.jpg

Test2.jpg

Test3.jpg

Test3.jpg

Trifecta.jpg:

комбинированные изображения

Trifecta_vertical.jpg

введите описание изображения здесь

person dermen    schedule 14.05.2015
comment
Большое спасибо. Еще один хороший ответ. Как min_shape =.... и imgs_comb.... изменились бы для вертикальной конкатенации? Не могли бы вы опубликовать это здесь в качестве комментария или в своем ответе? - person edesz; 14.05.2015
comment
Для вертикальной измените hstack на vstack. - person dermen; 14.05.2015
comment
Еще один вопрос: ваше первое изображение (Test1.jpg) больше других изображений. В конечном (горизонтальном или вертикальном) объединенном изображении все изображения имеют одинаковый размер. Не могли бы вы объяснить, как вам удалось сжать первое изображение перед его объединением? - person edesz; 14.05.2015
comment
Я использовал Image.resize от PIL. min_shape - это кортеж (min_width, min_height), а затем (np.asarray( i.resize(min_shape) ) for i in imgs ) уменьшит все изображения до этого размера. Фактически, min_shape может быть любым (width,height) по вашему желанию, но имейте в виду, что увеличение изображений с низким разрешением сделает их размытыми! - person dermen; 14.05.2015
comment
Если вы хотите просто объединить изображения без каких-либо подробностей, это, вероятно, самый простой и гибкий ответ. Он учитывает различный размер изображения, любое количество изображений и различные форматы изображений. Это был очень хорошо продуманный ответ и ЧРЕЗВЫЧАЙНО полезный. Никогда бы не подумал об использовании numpy. Спасибо. - person Noctsol; 11.01.2019

Изменить: ответ DTing более применим к вашему вопросу, поскольку он использует PIL, но я оставлю это, если вы хотите знать, как это сделать в numpy.

Вот решение numpy / matplotlib, которое должно работать для N изображений (только цветных изображений) любого размера / формы.

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

Вот пример использования:

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

введите описание изображения здесь

person derricw    schedule 14.05.2015
comment
Ваш output = concat_images(output, ... - это то, что я искал, когда начинал искать способ сделать это. Спасибо. - person edesz; 14.05.2015
comment
Привет, ballatballsdotballs, у меня есть один вопрос по поводу вашего ответа. Если я хочу добавить подзаголовок для каждого фрагмента изображения, как это сделать? Спасибо. - person user297850; 11.12.2016

Основываясь на ответе DTing, я создал функцию, которую проще использовать:

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

Позволяет выбрать цвет фона и выравнивание изображения. Также легко сделать рекурсию:

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

Пример составного изображения

person teekarna    schedule 07.10.2017
comment
Я не на 100% понимаю, в чем проблема, но эта функция делает что-то странное с изображениями, в результате чего объекты, которые я повторяю, изменяются с общего веса 25 МБ до 2 ГБ. так что будьте осторожны, используя этот метод - person FlyingZebra1; 07.12.2020

Вот функция, обобщающая предыдущие подходы, создающая сетку изображений в 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

Он сократит каждую строку и столбцы сетки до минимума. У вас может быть только строка, используя pil_grid (изображения), или только столбец, используя pil_grid (images, 1).

Одно из преимуществ использования PIL над решениями на основе массива numpy заключается в том, что вы можете работать с изображениями, структурированными по-разному (например, изображениями в оттенках серого или изображениями на основе палитры).

Примеры выходных данных

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

введите описание изображения здесь

pil_grid(dummy_images, 1):

введите описание изображения здесь

person Maxim    schedule 22.10.2017
comment
Эта строка в pil_grid: h_sizes, v_sizes = [0] * n_horiz, [0] * (n_images // n_horiz) должна читаться: h_sizes, v_sizes = [0] * n_horiz, [0] * ((n_images // n_horiz) + (1 if n_images % n_horiz > 0 else 0)) Причина: если ширина по горизонтали не делит количество изображений в целых числах, вам необходимо добавить дополнительную строку, если она не завершена. - person Bernhard Wagner; 30.05.2019

Если все изображения одинаковой высоты,

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

возможно, вы можете изменить размер изображений перед объединением следующим образом,

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
  )
)
person plhn    schedule 27.03.2019
comment
Просто и легко. Спасибо - person Mike de Klerk; 24.03.2020

Вот мое решение:

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
    )

Для этих изображений:

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

Результаты будут выглядеть так:


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

введите здесь описание изображения


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

).show()

введите здесь описание изображения


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

введите здесь описание изображения

person Mikhail Gerasimov    schedule 12.01.2020

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

Выход

введите здесь описание изображения

person Jayesh Baviskar    schedule 07.09.2018

Просто добавляю к уже предложенным решениям. Предполагается одинаковая высота, без изменения размера.

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')
person Kelmok    schedule 06.09.2018

мое решение было бы:

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]
person Avral    schedule 03.06.2020