使用Python水平合并多张图片

209

我正在尝试使用Python将一些JPEG图像水平合并。

问题

我有3张图像 - 每张图像的大小为148 x 95 - 如附图所示。我只是复制了同一张图像的3份副本,这就是它们为什么相同的原因。

enter image description hereenter image description hereenter image description here

我的尝试

我正在尝试使用以下代码将它们水平连接:

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的输出。

enter image description here

问题

有没有一种方法可以水平串联这些图像,使得test.jpg中的子图像不会显示额外的部分图像?

附加信息

我正在寻找一种将n个图像水平串联的方法。 如果可能,我不想硬编码图像尺寸; 我想在一行中指定尺寸,以便可以轻松更改。


3
你的代码中为什么要有for i in xrange(...)?难道不应该由paste函数来处理你指定的三个图像文件吗? - msw
问题:你的图像大小会一直保持不变吗? - dermen
dermen:是的,图像大小将始终相同。 msw:我不确定如何循环遍历图像,而不会在它们之间留下空白空间 - 我的方法可能不是最好的。 - edesz
2
这不起作用的唯一原因是你的 xrange(0,444,95)。如果你将其更改为 xrange(0,444,148),一切都应该没问题。这是因为你水平分割了图像,一个图像的宽度是148。 (另外,你想要组合3个图像,所以你的范围对象应该包含3个值是很合理的。) - Jonas De Schouwer
谢谢!你关于宽度部分是正确的。我在问题中确实利用了这个,但是我的步长推导是错误的——参见Image.new('RGB', (444,95))……在这里,我指定了444,因为正如您所指出的,有三张图像,每张图像宽度为148像素,所以连接的图像宽度应该是148 X 3 = 444。无论如何,您是正确的——使用xrange是不正确的。我认为95将是最终图像的高度,这是错误的假设,因为这不是xrange的工作方式 - edesz
13个回答

276

你可以像这样做:

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

enter image description here


嵌套的循环 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')

enter image description here enter image description here enter image description here


两个问题:1. x_offset = 0 - 这是图像中心之间的错开吗?2. 对于垂直连接,你的方法会如何改变? - edesz
2
paste的第二个参数是一个框。 "框参数可以是一个2元组,给出左上角,一个4元组定义左、上、右和下像素坐标,或None(与(0,0)相同)"。所以在2元组中,我们使用x_offset作为left。对于垂直连接,请跟踪y-offsettop。不要使用sum(widths)max(height),而是使用sum(heights)max(widths)并使用2元组框的第二个参数。通过im.size[1]增加y_offset - dting
24
不错的解决方案。需要注意的是,在Python3中,映射(maps)只能迭代一次,因此在第二次迭代图像之前,您必须再次使用images = map(Image.open, image_files)对图像进行映射。 - Naijaba
1
我也遇到了你描述的问题,所以我编辑了DTing的解决方案,改用了列表推导式而不是 map。 - Ben Quigley
1
我不得不在Python3.6中使用列表推导式而不是map - ClementWalter
显示剩余2条评论

127

我会尝试这个:

import numpy as np
import PIL
from PIL import Image

list_im = ['Test1.jpg', 'Test2.jpg', 'Test3.jpg']
imgs    = [ 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([i.resize(min_shape) for i in imgs])

# save that beautiful picture
imgs_comb = Image.fromarray( imgs_comb)
imgs_comb.save( 'Trifecta.jpg' )    

# for a vertical stacking it is simple: use vstack
imgs_comb = np.vstack([i.resize(min_shape) for i in imgs])
imgs_comb = 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:

combined images

Trifecta_vertical.jpg

enter image description here


非常感谢。又一个好答案。如果要进行垂直拼接,min_shape =....imgs_comb....会如何更改?您能在评论中或回复中发布吗? - edesz
4
针对垂直方向,请将hstack更改为vstack - dermen
还有一个问题:您的第一张图片(Test1.jpg)比其他图片要大。在您最终的(水平或垂直)拼接图像中,所有图像的大小都相同。您能解释一下在拼接之前如何缩小第一张图片吗? - edesz
我使用了PIL中的Image.resizemin_shape是一个元组(min_width, min_height),然后(np.asarray(i.resize(min_shape)) for i in imgs)将缩小所有图像到该尺寸。实际上,min_shape可以是任何你想要的(width,height),但请记住,放大低分辨率图像会使它们变得模糊! - dermen
4
如果你只是想简单地将图像组合在一起,而没有具体的要求,那么这可能是本文中最简单、最灵活的答案。它考虑到了不同的图像尺寸、任意数量的图像和各种图片格式。这是一个非常深入思考并且非常有用的回答。我从来没有想过使用numpy。谢谢。 - Noctsol

29

编辑: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()

在此输入图片描述


你的 output = concat_images(output, ... 就是我一直在寻找的方法,感谢你。 - edesz
嗨,ballsatballsdotballs,我有一个关于你的答案的问题。如果我想为每个子图像添加副标题,该怎么做?谢谢。 - user297850

20

这里是一个泛化之前方法的函数,用于在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(images)仅使用一行,或使用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):

enter image description here

pil_grid(dummy_images, 1):

enter image description here


3
在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))原因:如果横向宽度无法整除图像数量,则需要考虑额外的不完整行。 - Bernhard Wagner

13

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

示例串联图像


1
我不确定问题出在哪里,但是这个函数在处理图像时会出现奇怪的情况,导致我正在迭代的对象从总重量25mb变成2gb。因此,在使用这种方法时要小心。 - FlyingZebra1

8
如果所有图片的高度都相同,
from PIL import Image
import numpy as np

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

也许你可以在拼接之前调整图片的大小,就像这样,
import numpy as np

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 and easy. Thanks - Mike de Klerk

3

还有 skimage.util.montage 可以创建相同形状的图像拼贴:

import numpy as np
import PIL
from PIL import Image
from skimage.util import montage

list_im = ['Test1.jpg', 'Test2.jpg', 'Test3.jpg']
imgs    = [ np.array(Image.open(i)) for i in list_im ]

montage(imgs)

3

这是我的解决方案:

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

enter image description here


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

).show()

enter image description here


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

enter image description here


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

输出

enter image description here


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'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接