【问题标题】:Load just part of an image in python在 python 中仅加载图像的一部分
【发布时间】:2013-10-30 22:44:59
【问题描述】:

这可能是一个愚蠢的问题,但是......

我有几千张图像,我想将它们加载到 Python 中,然后转换为 numpy 数组。显然这进展有点慢。但是,我实际上只对每张图像的一小部分感兴趣。 (相同的部分,图像中心只有 100x100 像素。)

有什么方法可以只加载图像的一部分以加快速度吗?

这是一些示例代码,我在其中生成了一些示例图像,保存它们,然后重新加载它们。

import numpy as np
import matplotlib.pyplot as plt
import Image, time

#Generate sample images
num_images = 5

for i in range(0,num_images):
    Z = np.random.rand(2000,2000)
    print 'saving %i'%i
    plt.imsave('%03i.png'%i,Z)

%load the images
for i in range(0,num_images):
    t = time.time()

    im = Image.open('%03i.png'%i)
    w,h = im.size
    imc = im.crop((w-50,h-50,w+50,h+50))

    print 'Time to open: %.4f seconds'%(time.time()-t)

    #convert them to numpy arrays
    data = np.array(imc)

【问题讨论】:

  • 我很确定你不能,但我很想在这个问题上被证明是错误的
  • 您必须将文件作为原始二进制文件打开,然后使用 file.seek() 等来访问您想要的位
  • @avrono 是的,但问题实际上是如何判断哪些位构成至少一种图像类型的图像中心(无论图像尺寸如何)
  • 查找特定字节更加复杂,因为看起来他使用的是 zlib 压缩的 PNG。
  • png是位图吗? ,我不这么认为。它是压缩的,所以你必须先做一些事情。你的图片可以是位图吗?

标签: python numpy scipy python-imaging-library


【解决方案1】:

虽然在单个线程中您无法比 PIL 裁剪快得多,但您可以使用多个内核来加速一切! :)

我在我的 8 核 i7 机器以及我 7 岁、两核、几乎没有 2ghz 的笔记本电脑上运行了以下代码。两者都看到了运行时间的显着改善。正如您所期望的那样,改进取决于可用内核的数量。

您的代码的核心是相同的,我只是将循环与实际计算分开,以便该函数可以并行应用于值列表。

所以,这个:

for i in range(0,num_images):
    t = time.time()

    im = Image.open('%03i.png'%i)
    w,h = im.size
    imc = im.crop((w-50,h-50,w+50,h+50))

    print 'Time to open: %.4f seconds'%(time.time()-t)

    #convert them to numpy arrays
    data = np.array(imc)

成为:

def convert(filename):  
    im = Image.open(filename)
    w,h = im.size
    imc = im.crop((w-50,h-50,w+50,h+50))
    return numpy.array(imc)

加速的关键是multiprocessing 库的Pool 功能。它使跨多个处理器运行事物变得微不足道。

完整代码:

import os 
import time
import numpy 
from PIL import Image
from multiprocessing import Pool 

# Path to where my test images are stored
img_folder = os.path.join(os.getcwd(), 'test_images')

# Collects all of the filenames for the images
# I want to process
images = [os.path.join(img_folder,f) 
        for f in os.listdir(img_folder)
        if '.jpeg' in f]

# Your code, but wrapped up in a function       
def convert(filename):  
    im = Image.open(filename)
    w,h = im.size
    imc = im.crop((w-50,h-50,w+50,h+50))
    return numpy.array(imc)

def main():
    # This is the hero of the code. It creates pool of 
    # worker processes across which you can "map" a function
    pool = Pool()

    t = time.time()
    # We run it normally (single core) first
    np_arrays = map(convert, images)
    print 'Time to open %i images in single thread: %.4f seconds'%(len(images), time.time()-t)

    t = time.time()
    # now we run the same thing, but this time leveraging the worker pool.
    np_arrays = pool.map(convert, images)
    print 'Time to open %i images with multiple threads: %.4f seconds'%(len(images), time.time()-t)

if __name__ == '__main__':
    main()

非常基本。只需几行额外的代码,并进行一些重构以将转换位移动到它自己的函数中。结果不言自明:

结果:

8 核 i7

Time to open 858 images in single thread: 6.0040 seconds
Time to open 858 images with multiple threads: 1.4800 seconds

2-Core Intel Duo

Time to open 858 images in single thread: 8.7640 seconds
Time to open 858 images with multiple threads: 4.6440 seconds

所以你去吧!即使您拥有一台超级旧的 2 核机器,您也可以将打开和处理图像的时间减半。

注意事项

内存。如果您要处理 1000 张图像,您可能会在某个时候弹出 Python 的内存限制。为了解决这个问题,您只需要分块处理数据。您仍然可以利用所有的多处理优势,只是在较小的部分。类似的东西:

for i in range(0, len(images), chunk_size): 
    results = pool.map(convert, images[i : i+chunk_size]) 
    # rest of code. 

【讨论】:

  • 哦,这真的很有趣。我以为我会受到磁盘读取率的限制,但这看起来好像我实际上受到了裁剪功能的限制?实现良好加速的非常好的实用方法。谢谢!
  • @DanHickstein 衡量一切 :) 我有一个与您的工作非常相似的脚本(例如打开/裁剪/处理)。我确信瓶颈实际上是从磁盘上加载图像(因为有成千上万的图像)。但是,在使用 kernprof 快速进行 line_profile 之后,我意识到可以从光盘上快速读取图像(至少在我的设置中)。也就是说,如果您发现读取 IO 仍然存在问题,您可以使用multiprocessing.dummy.Pool 轻松将 IO 拆分到多个线程。
  • Python3 的注意事项:map 是惰性的 Python: Map calling a function not working 所以第一次单线程调用没有给出任何结果
【解决方案2】:

将文件保存为未压缩的 24 位 BMP。这些以非常规则的方式存储像素数据。从Wikipedia 查看此图表的“图像数据”部分。请注意,图表中的大部分复杂性仅来自标题:

例如,假设您要存储这张图片(此处放大显示):

这是像素数据部分的样子,如果它存储为 24 位未压缩 BMP。请注意,由于某种原因,数据是自下而上存储的,并且以 BGR 形式而不是 RGB 形式存储,因此文件中的第一行是图像的最底部行,第二行是倒数第二行,等:

00 00 FF    FF FF FF    00 00
FF 00 00    00 FF 00    00 00

该数据解释如下:

           |  First column  |  Second Column  |  Padding
-----------+----------------+-----------------+-----------
Second Row |  00 00 FF      |  FF FF FF       |  00 00
-----------+----------------+-----------------+-----------
First Row  |  FF 00 00      |  00 FF 00       |  00 00
-----------+----------------+-----------------+-----------

或:

           |  First column  |  Second Column  |  Padding
-----------+----------------+-----------------+-----------
Second Row |  red           |  white          |  00 00
-----------+----------------+-----------------+-----------
First Row  |  blue          |  green          |  00 00
-----------+----------------+-----------------+-----------

填充用于将行大小填充为 4 字节的倍数。


所以,你所要做的就是为这种特定的文件格式实现一个阅读器,然后计算你必须开始和停止读取每一行的字节偏移量:

def calc_bytes_per_row(width, bytes_per_pixel):
    res = width * bytes_per_pixel
    if res % 4 != 0:
        res += 4 - res % 4
    return res

def calc_row_offsets(pixel_array_offset, bmp_width, bmp_height, x, y, row_width):
    if x + row_width > bmp_width:
        raise ValueError("This is only for calculating offsets within a row")

    bytes_per_row = calc_bytes_per_row(bmp_width, 3)
    whole_row_offset = pixel_array_offset + bytes_per_row * (bmp_height - y - 1)
    start_row_offset = whole_row_offset + x * 3
    end_row_offset = start_row_offset + row_width * 3
    return (start_row_offset, end_row_offset)

然后你只需要处理正确的字节偏移量。例如,假设您要读取 10000x10000 位图中从位置 500x500 开始的 400x400 块:

def process_row_bytes(row_bytes):
    ... some efficient way to process the bytes ...

bmpf = open(..., "rb")
pixel_array_offset = ... extract from bmp header ...
bmp_width = 10000
bmp_height = 10000
start_x = 500
start_y = 500
end_x = 500 + 400
end_y = 500 + 400

for cur_y in xrange(start_y, end_y):
    start, end = calc_row_offsets(pixel_array_offset, 
                                  bmp_width, bmp_height, 
                                  start_x, cur_y, 
                                  end_x - start_x)
    bmpf.seek(start)
    cur_row_bytes = bmpf.read(end - start)
    process_row_bytes(cur_row_bytes)

请注意,处理字节的方式很重要。您可能可以使用 PIL 做一些聪明的事情,然后将像素数据转储到其中,但我不完全确定。如果您以低效的方式执行此操作,则可能不值得。如果速度是一个大问题,您可以考虑使用pyrex 编写它,或者在 C 中实现上述内容,然后从 Python 中调用它。

【讨论】:

  • 哦,这看起来很有趣。今晚我需要多看看这个,看看我能不能让它工作。感谢您如此彻底的回复!
【解决方案3】:

哦,我刚刚意识到可能有一种比我上面写的关于 BMP 文件的方法要简单得多的方法。

如果您仍然要生成图像文件,并且您始终知道要读取哪个部分,只需在生成时将该部分另存为另一个图像文件:

import numpy as np
import matplotlib.pyplot as plt
import Image

#Generate sample images
num_images = 5

for i in range(0,num_images):
    Z = np.random.rand(2000, 2000)
    plt.imsave('%03i.png'%i, Z)
    snipZ = Z[200:300, 200:300]
    plt.imsave('%03i.snip.png'%i, snipZ)

#load the images
for i in range(0,num_images):
    im = Image.open('%03i.snip.png'%i)

    #convert them to numpy arrays
    data = np.array(im)

【讨论】:

  • 啊,是的,这是一个很好的健全性检查,在某些情况下会起作用。但是,更一般地说,我想保存全尺寸图像,然后再定义感兴趣的区域。因此,当我读入一组图像时,感兴趣的区域将是相同的,但在保存图像时我无法轻松定义 ROI。
【解决方案4】:

我已经运行了一些计时测试,但很遗憾地说,我认为您的速度不会比 PIL 裁剪命令快得多。即使使用手动搜索/低级读取,您仍然必须读取字节。以下是计时结果:

%timeit im.crop((1000-50,1000-50,1000+50,1000+50))
fid = open('003.png','rb')
%timeit fid.seek(1000000)
%timeit fid.read(1)
print('333*100*100/10**(9)*1000=%.2f ms'%(333*100*100/10**(9)*1000))


100000 loops, best of 3: 3.71 us per loop
1000000 loops, best of 3: 562 ns per loop
1000000 loops, best of 3: 330 ns per loop
333*100*100/10**(9)*1000=3.33 ms

从底部的计算可以看出,我们有一个读取 1 字节 *10000 字节(100x100 子图像)*333ns per byte=3.33ms 这与上面的裁剪命令相同

【讨论】:

  • 好的,很高兴有一些独立的确认,我已经在速度的局部最大值。如果没有其他解决方案,我会在几天后接受这个答案。谢谢!
  • -1,这不是一个好的比较。在im.crop 点,图像已经加载。 im.crop 只是 returns a proxy object - 这实际上是一个无操作。一个公平的比较是加载整个图像然后裁剪然后转换为数组,而不是只读取相关字节并将它们转换为数组。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-06
  • 2011-11-09
  • 2011-02-02
  • 1970-01-01
相关资源
最近更新 更多