将数千张图像读入一个大 numpy 数组的最快方法

新手上路,请多包涵

我正在尝试找到最快的方法来将目录中的一堆图像读取到 numpy 数组中。我的最终目标是计算统计数据,例如所有这些图像中像素的最大、最小和第 n 个百分位数。当所有图像的像素都在一个大的 numpy 数组中时,这是直接且快速的,因为我可以使用内置数组方法,例如 .max.minnp.percentile 函数。

以下是 25 张 tiff 图像(512x512 像素)的几个示例计时。这些基准来自于在 jupyter-notebook 中使用 %%timit 。差异太小,仅对 25 张图像没有任何实际意义,但我打算在未来阅读数千张图像。

 # Imports
import os
import skimage.io as io
import numpy as np
  1. 附加到列表
   %%timeit
   imgs = []
   img_path = '/path/to/imgs/'
   for img in os.listdir(img_path):
       imgs.append(io.imread(os.path.join(img_path, img)))
   ## 32.2 ms ± 355 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

  1. 使用字典
   %%timeit
   imgs = {}
   img_path = '/path/to/imgs/'
   for img in os.listdir(img_path):
       imgs[num] = io.imread(os.path.join(img_path, img))
   ## 33.3 ms ± 402 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

对于上面的列表和字典方法,我尝试用时间上具有相似结果的相应理解替换循环。我还尝试预分配字典键,所用时间没有显着差异。要将图像从列表获取到大数组,我会使用 np.concatenate(imgs) ,这只需要 ~1 毫秒。

  1. 沿第一个维度预分配一个 numpy 数组
   %%timeit
   imgs = np.ndarray((512*25,512), dtype='uint16')
   img_path = '/path/to/imgs/'
   for num, img in enumerate(os.listdir(img_path)):
       imgs[num*512:(num+1)*512, :] = io.imread(os.path.join(img_path, img))
   ## 33.5 ms ± 804 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

  1. 沿三维预分配一个 numpy
    %%timeit
   imgs = np.ndarray((512,512,25), dtype='uint16')
   img_path = '/path/to/imgs/'
   for num, img in enumerate(os.listdir(img_path)):
       imgs[:, :, num] = io.imread(os.path.join(img_path, img))
   ## 71.2 ms ± 2.22 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

我最初认为 numpy 预分配方法会更快,因为循环中没有动态变量扩展,但事实似乎并非如此。我发现最直观的方法是最后一种,其中每个图像沿数组的第三轴占据一个单独的维度,但这也是最慢的。额外花费的时间不是由于预分配本身,它只需要大约 1 毫秒。

我对此有三个问题:

  1. 为什么 numpy 预分配方法不比字典和列表解决方案快?
  2. 将数千张图像读入一个大的 numpy 数组的最快方法是什么?
  3. 我可以从 numpy 和 scikit-image 的外部寻找一个更快的图像读取模块吗?我尝试了 plt.imread() ,但是 scikit-image.io 模块更快。

原文由 joelostblom 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 512
2 个回答

A 部分:访问和分配 NumPy 数组

顺便说一句,对于 NumPy 数组,元素以行优先顺序存储,在每次迭代中沿着最后一个轴存储这些元素时,您做的是正确的事情。这些将占据连续的内存位置,因此对于访问和分配值来说是最有效的。因此像 np.ndarray((512*25,512), dtype='uint16')np.ndarray((25,512,512), dtype='uint16') 这样的初始化效果最好,正如评论中提到的那样。

在将它们编译为用于测试时序的函数并输入随机数组而不是图像后 -

 N = 512
n = 25
a = np.random.randint(0,255,(N,N))

def app1():
    imgs = np.empty((N,N,n), dtype='uint16')
    for i in range(n):
        imgs[:,:,i] = a
        # Storing along the first two axes
    return imgs

def app2():
    imgs = np.empty((N*n,N), dtype='uint16')
    for num in range(n):
        imgs[num*N:(num+1)*N, :] = a
        # Storing along the last axis
    return imgs

def app3():
    imgs = np.empty((n,N,N), dtype='uint16')
    for num in range(n):
        imgs[num,:,:] = a
        # Storing along the last two axes
    return imgs

def app4():
    imgs = np.empty((N,n,N), dtype='uint16')
    for num in range(n):
        imgs[:,num,:] = a
        # Storing along the first and last axes
    return imgs

时间 -

 In [45]: %timeit app1()
    ...: %timeit app2()
    ...: %timeit app3()
    ...: %timeit app4()
    ...:
10 loops, best of 3: 28.2 ms per loop
100 loops, best of 3: 2.04 ms per loop
100 loops, best of 3: 2.02 ms per loop
100 loops, best of 3: 2.36 ms per loop

这些时间确认了开始时提出的性能理论,尽管我预计最后一次设置的时间介于 app3app1 之间,但也许是去的效果从最后一个轴到第一个用于访问和分配的轴不是线性的。对此进行更多调查可能很有趣( 在这里跟进问题)。

为了示意性地说明,考虑我们正在存储图像数组,由 x (图像 1)和 o (图像 2)表示,我们将有:

应用程序 1:

 [[[x 0]
  [x 0]
  [x 0]
  [x 0]
  [x 0]]

 [[x 0]
  [x 0]
  [x 0]
  [x 0]
  [x 0]]

 [[x 0]
  [x 0]
  [x 0]
  [x 0]
  [x 0]]]

因此,在内存空间中,它将是: [x,o,x,o,x,o..] 按照行优先顺序。

应用程序2:

 [[x x x x x]
 [x x x x x]
 [x x x x x]
 [o o o o o]
 [o o o o o]
 [o o o o o]]

因此,在内存空间中,它将是: [x,x,x,x,x,x...o,o,o,o,o..]

应用程序 3:

 [[[x x x x x]
  [x x x x x]
  [x x x x x]]

 [[o o o o o]
  [o o o o o]
  [o o o o o]]]

因此,在内存空间中,它将与前一个相同。


B 部分:从磁盘读取图像作为数组

现在,关于读取图像的部分,我看到 OpenCV 的 imread 要快得多。

作为测试,我从维基页面下载了蒙娜丽莎的图像并测试了图像读取的性能 -

 import cv2 # OpenCV

In [521]: %timeit io.imread('monalisa.jpg')
100 loops, best of 3: 3.24 ms per loop

In [522]: %timeit cv2.imread('monalisa.jpg')
100 loops, best of 3: 2.54 ms per loop

原文由 Divakar 发布,翻译遵循 CC BY-SA 3.0 许可协议

在这种情况下,大部分时间都花在从磁盘读取文件上,我不会太担心填充列表的时间。

无论如何,这是一个比较四种方法的脚本,没有从磁盘读取实际图像的开销,而只是从内存中读取一个对象。

 import numpy as np
import time
from functools import wraps

x, y = 512, 512
img = np.random.randn(x, y)
n = 1000

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        r = func(*args, **kwargs)
        end = time.perf_counter()
        print('{}.{} : {} milliseconds'.format(func.__module__, func.__name__, (end - start)*1e3))
        return r
    return wrapper

@timethis
def static_list(n):
    imgs = [None]*n
    for i in range(n):
        imgs[i] = img
    return imgs

@timethis
def dynamic_list(n):
    imgs = []
    for i in range(n):
        imgs.append(img)
    return imgs

@timethis
def list_comprehension(n):
    return [img for i in range(n)]

@timethis
def numpy_flat(n):
    imgs = np.ndarray((x*n, y))
    for i in range(n):
        imgs[x*i:(i+1)*x, :] = img

static_list(n)
dynamic_list(n)
list_comprehension(n)
numpy_flat(n)

结果显示:

 __main__.static_list : 0.07004200006122119 milliseconds
__main__.dynamic_list : 0.10294799994881032 milliseconds
__main__.list_comprehension : 0.05021800006943522 milliseconds
__main__.numpy_flat : 309.80870099983804 milliseconds

显然,最好的选择是列表理解,但是即使填充了一个 numpy 数组,读取 1000 张图像(从内存中)也只需 310 毫秒。同样,开销将是磁盘读取。

为什么 numpy 比较慢?

这是 numpy 在内存中存储数组的方式。如果我们修改 python 列表函数以将列表转换为 numpy 数组,时间是相似的。

修改后的函数返回值:

 @timethis
def static_list(n):
    imgs = [None]*n
    for i in range(n):
        imgs[i] = img
    return np.array(imgs)

@timethis
def dynamic_list(n):
    imgs = []
    for i in range(n):
        imgs.append(img)
    return np.array(imgs)

@timethis
def list_comprehension(n):
    return np.array([img for i in range(n)])

和计时结果:

 __main__.static_list : 303.32892100022946 milliseconds
__main__.dynamic_list : 301.86925499992867 milliseconds
__main__.list_comprehension : 300.76925699995627 milliseconds
__main__.numpy_flat : 305.9309459999895 milliseconds

所以这只是一个麻木的事情,它需要更多的时间,而且它是相对于数组大小的常量值……

原文由 Gerges 发布,翻译遵循 CC BY-SA 3.0 许可协议

推荐问题