如何在Python中将图像分割成多个部分

59

我正在尝试使用PIL将一张照片分成多个部分。

def crop(Path,input,height,width,i,k,x,y,page):
    im = Image.open(input)
    imgwidth = im.size[0]
    imgheight = im.size[1]
    for i in range(0,imgheight-height/2,height-2):
        print i
        for j in range(0,imgwidth-width/2,width-2):
            print j
            box = (j, i, j+width, i+height)
            a = im.crop(box)
            a.save(os.path.join(Path,"PNG","%s" % page,"IMG-%s.png" % k))
            k +=1

但是它似乎没有起作用。它会将照片分割,但不是精确的方式(您可以尝试一下)。


“exact”宽度和高度是什么意思? - kindall
使用 NumPy 库:tiles = [im[x:x+M,y:y+N] for x in range(0,im.shape[0],M) for y in range(0,im.shape[1],N)] - 请参见下面的答案 - Nir
23个回答

50

将图像分割成MxN像素的瓷砖(假设图像是numpy.ndarray):

将图像分割成大小为MxN像素的图块(假设图像是numpy.ndarray):

tiles = [im[x:x+M,y:y+N] for x in range(0,im.shape[0],M) for y in range(0,im.shape[1],N)]

如果你想将图片分成四个部分:

M = im.shape[0]//2
N = im.shape[1]//2

tiles[0]保存了左上角的瓦片


1
对于我的情况来说,最简单的解决方案也不是计算昂贵的。 - NeStack
@DeepPatel,您的观点是错误的。对于任何N和M的值,由于超出图像边界的切片忽略了多余的部分,因此生成的瓦片不会超出图像大小。对于任何0<=x<=im.shape[0],以下语句都是正确的:assert im[im.shape[0]-x:im.shape[0]+x:,:].shape[0] == x。 - Nir
1
我的错,我之前在用这个做其他事情时可能修改过它,所以出了一些错误。你是对的。 - Deep Patel
有人尝试过在分裂后将它们重新组合吗? - Thomas
1
@Thomas 这个有点晚了,但为了后人,代码给出的瓷砖序列是按行排列的。所以,如果你有6个瓷砖,有2行3列,那么要重新组合它们,你可以这样做:row0=[tiles[0],tiles[1],tiles[2]],row1=[tiles[3],tiles[4],tiles[5]]。 - undefined

44
作为一种替代方案,我们将使用 itertools.product 生成坐标网格来构建瓷砖。我们将忽略边缘上的部分瓷砖,仅迭代两个区间之间的笛卡尔积,即 range(0, h-h%d, d) X range(0, w-w%d, d)
给定filename:图像文件名,d:瓷砖大小,dir_in:包含图像的目录路径和dir_out:输出瓷砖的目录。
from PIL import Image
from itertools import product
def tile(filename, dir_in, dir_out, d):
    name, ext = os.path.splitext(filename)
    img = Image.open(os.path.join(dir_in, filename))
    w, h = img.size
    
    grid = product(range(0, h-h%d, d), range(0, w-w%d, d))
    for i, j in grid:
        box = (j, i, j+d, i+d)
        out = os.path.join(dir_out, f'{name}_{i}_{j}{ext}')
        img.crop(box).save(out)

在此输入图像描述


2
非常实用的解决方案,谢谢。将“fp”作为函数的参数发送可能很有用,因为“filename”和“fp”变量可能会令人困惑。 - Kubra Altun
1
谢谢@Kubra。我在我的源代码中使用了fp,现在已经更改为正确的参数命名。 - Ivan
真的需要这个解决方案。 - gildniy
非常清晰易懂。 - jcaliz
使用这种方法处理非常大的图像时,我似乎会遇到“打开文件太多”的问题,你有什么想法为什么会发生这种情况? - Matrix

43

编辑:我认为这个答案忽略了将图像切割成列和行的意图。这个答案仅将其切割成行。看起来其他的答案是在列和行中切割。

比所有这些都更简单的方法是使用别人发明的轮子 :) 它可能需要更多设置,但使用起来很容易。

这些说明适用于Windows 7;它们可能需要适应其他操作系统。

这里获取并安装pip。

下载安装文件并将其解压到根Python安装目录。打开控制台并键入(如果我记得正确):

python get-pip.py install

然后通过pip获取并安装image_slicer模块,通过在控制台输入以下命令:

python -m pip install image_slicer

将要切片的图像复制到Python根目录中,打开Python Shell(不是“命令行”),并输入以下命令:

import image_slicer
image_slicer.slice('huge_test_image.png', 14)

这个模块的美妙之处在于:

  1. 它被安装在Python中;
  2. 只需要两行代码即可调用图像切割功能;
  3. 可以接受任何偶数作为图像切片参数(例如此示例中的14);
  4. 将该参数自动应用于给定的图像,将其分成相应数量的切片,并自动保存结果编号瓷砖在同一目录下;最后,
  5. 具有将图像拼合回来的功能(我尚未测试);文件名必须按约定命名,您将在测试image_slicer.slice函数后看到拆分文件中的约定。

13
看起来不错,但文档很差。一旦创建,它可以很好地控制图块,但很难看出图像将被切成哪些部分。我原本期望有一种元组来设置行和列的数量。 - Rodrigo Laguna
根据对其他答案的评论,这可能不是内存受限系统上的选项。 - Alex Hall
\get-pip.py': [Errno 2] No such file or directory“\get-pip.py”:[Errno 2] 没有那个文件或目录。 - user7082181
@user7082181 请尝试按照此处(截至本文撰写时)记录的文档安装pip:https://pip.pypa.io/en/latest/installation/ - Alex Hall

39
from PIL import Image

def crop(path, input, height, width, k, page, area):
    im = Image.open(input)
    imgwidth, imgheight = im.size
    for i in range(0,imgheight,height):
        for j in range(0,imgwidth,width):
            box = (j, i, j+width, i+height)
            a = im.crop(box)
            try:
                o = a.crop(area)
                o.save(os.path.join(path,"PNG","%s" % page,"IMG-%s.png" % k))
            except:
                pass
            k +=1

16
为什么这个函数有一个参数 k?在调用函数时,它不应该总是为 0 吗?另外 area 是什么?为什么要对图像进行两次裁剪操作? - Giacomo Alzetta
4
你的所有论点意味着什么? - Sam
如果你读了代码,参数就很明显了,而k是偏移量。 - user7082181

21
  1. crop 函数可以更加通用化,如果你将裁剪代码与图片保存代码分离开来。这也会使调用签名更简单。
  2. im.crop 返回一个 Image._ImageCrop 实例。这种实例没有 save 方法。相反,您必须将 Image._ImageCrop 实例粘贴到一个新的 Image.Image 上。
  3. 您的范围没有正确的步长。(例如,为什么是 height-2 而不是 height?为什么要停在 imgheight-(height/2)?)

因此,你可以尝试像这样做:

import Image
import os

def crop(infile,height,width):
    im = Image.open(infile)
    imgwidth, imgheight = im.size
    for i in range(imgheight//height):
        for j in range(imgwidth//width):
            box = (j*width, i*height, (j+1)*width, (i+1)*height)
            yield im.crop(box)

if __name__=='__main__':
    infile=...
    height=...
    width=...
    start_num=...
    for k,piece in enumerate(crop(infile,height,width),start_num):
        img=Image.new('RGB', (height,width), 255)
        img.paste(piece)
        path=os.path.join('/tmp',"IMG-%s.png" % k)
        img.save(path)

谢谢你的解决方案,但它对我不起作用,图片没有裁剪好,我看到了红色,我认为问题可能在这里:img.paste(piece) - Elteroooo
如果您的内存有限,这是一个特别好的解决方案。当使用image_slicer时,大型图像可能会在内存较低的计算机上失败。 - Dean Liu
@Elteroooo所遇到的行为是由于代码存在错误,位于第18行:img=Image.new('RGB', (height,width), 255)widthheight应该交换。我建议进行编辑,但被拒绝了 ¯_(ツ)_/¯ - reticivis

4

这是一个简洁的、纯 Python 的解决方案,适用于 Python 3 和 2:

from PIL import Image

infile = '20190206-135938.1273.Easy8thRunnersHopefully.jpg'
chopsize = 300

img = Image.open(infile)
width, height = img.size

# Save Chops of original image
for x0 in range(0, width, chopsize):
   for y0 in range(0, height, chopsize):
      box = (x0, y0,
             x0+chopsize if x0+chopsize <  width else  width - 1,
             y0+chopsize if y0+chopsize < height else height - 1)
      print('%s %s' % (infile, box))
      img.crop(box).save('zchop.%s.x%03d.y%03d.jpg' % (infile.replace('.jpg',''), x0, y0))

注意事项:

  • 超出原始图像右侧和底部的裁剪部分已调整到原始图像限制范围内,仅包含原始像素。
  • 通过使用两个chopsize变量并在上面的代码中适当替换chopsize,可以轻松选择不同的w和h的切割尺寸。

  • 仅适用于PNG文件,不适用于目录中的文件,但这非常有帮助,谢谢! - TankorSmash
    对我来说,使用jpg文件效果很好。谢谢。 - Imo

    4

    以下是适用于Python 3的晚期答案

    from PIL import Image
    import os
    
    def imgcrop(input, xPieces, yPieces):
        filename, file_extension = os.path.splitext(input)
        im = Image.open(input)
        imgwidth, imgheight = im.size
        height = imgheight // yPieces
        width = imgwidth // xPieces
        for i in range(0, yPieces):
            for j in range(0, xPieces):
                box = (j * width, i * height, (j + 1) * width, (i + 1) * height)
                a = im.crop(box)
                try:
                    a.save("images/" + filename + "-" + str(i) + "-" + str(j) + file_extension)
                except:
                    pass
    

    使用方法:

    imgcrop("images/testing.jpg", 5, 5)
    

    然后根据指定的 X 和 Y 的数量,将图像裁剪成多个部分,在这种情况下是 5 x 5 = 25 个部分。


    3

    3

    这里是另一个解决方案,只使用NumPy内置的 np.array_split

    def divide_img_blocks(img, n_blocks=(5, 5)):
        horizontal = np.array_split(img, n_blocks[0])
        splitted_img = [np.array_split(block, n_blocks[1], axis=1) for block in horizontal]
        return np.asarray(splitted_img, dtype=np.ndarray).reshape(n_blocks)
    

    该函数返回一个NumPy数组,其维度由n_blocks传递而来。 数组的每个元素都是一个块,因此要访问每个块并将其保存为图像,您应该编写类似以下的代码:

    result = divide_img_blocks(my_image)
    
    for i in range(result.shape[0]):
        for j in range(result.shape[1]):
            cv2.imwrite(f"my_block_{i}_{j}.jpg", result[i,j])
    

    这个答案非常快,比@Nir的回答更快,而且已发布的答案中最干净。此外,它的速度几乎比建议使用的包(即image_slicer)快三个数量级。

    Time taken by divide_img_blocks: 0.0009832382202148438
    Time taken by Nir answer: 0.002960681915283203
    Time taken by image_slicer.slice: 0.4419238567352295
    

    希望它仍然有用。


    1
    这是否假定图像是正方形的?如果您能使用一个已知有效的测试图像更新您的答案,那将非常好。谢谢! - pookie
    嗨@pookie,该方法将接受任何图像大小和任意数量的“块”。 这是测试它的一种方法 - Filippo M. Libardi

    3

    我不确定这是否是最有效的答案,但对我来说可以使用:

    import os
    import glob
    from PIL import Image
    Image.MAX_IMAGE_PIXELS = None # to avoid image size warning
    
    imgdir = "/path/to/image/folder"
    # if you want file of a specific extension (.png):
    filelist = [f for f in glob.glob(imgdir + "**/*.png", recursive=True)]
    savedir = "/path/to/image/folder/output"
    
    start_pos = start_x, start_y = (0, 0)
    cropped_image_size = w, h = (500, 500)
    
    for file in filelist:
        img = Image.open(file)
        width, height = img.size
    
        frame_num = 1
        for col_i in range(0, width, w):
            for row_i in range(0, height, h):
                crop = img.crop((col_i, row_i, col_i + w, row_i + h))
                name = os.path.basename(file)
                name = os.path.splitext(name)[0]
                save_to= os.path.join(savedir, name+"_{:03}.png")
                crop.save(save_to.format(frame_num))
                frame_num += 1
    

    这主要基于DataScienceGuy在这里的回答。


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