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

joe*_*lom 19 python performance numpy image

我正在尝试找到一种最快的方法来从目录中读取一堆图像到一个numpy数组.我的最终目标是计算所有这些图像中像素的最大值,最小值和第n百分位数等统计数据.当所有图像中的像素都在一个大的numpy数组中时,这是直截了当的,因为我可以使用内置的数组方法,如.max.min,以及np.percentile函数.

下面是一些具有25个tiff图像(512x512像素)的示例时序.这些基准测试来自%%timit于jupyter-notebook中的使用.差异太小,不足以对25张图片产生任何实际影响,但我打算将来阅读数千张图片.

# Imports
import os
import skimage.io as io
import numpy as np
Run Code Online (Sandbox Code Playgroud)
  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)
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用字典

    %%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)
    
    Run Code Online (Sandbox Code Playgroud)

对于上面的列表和字典方法,我尝试用相应的理解来替换循环,并且在时间上具有类似的结果.我也试过预先分配字典键,所用时间没有显着差异.要将图像从列表中提取到大数组,我会使用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)
    
    Run Code Online (Sandbox Code Playgroud)
  2. 沿第三维预先分配一个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)
    
    Run Code Online (Sandbox Code Playgroud)

我最初认为numpy preallocation方法会更快,因为循环中没有动态变量扩展,但似乎并非如此.我发现最直观的方法是最后一个,其中每个图像沿阵列的第三轴占据一个单独的尺寸,但这也是最慢的.所需的额外时间不是由于预分配本身,只需要约1毫秒.

我有三个问题:

  1. 为什么numpy预分配方法不比字典和列表解决方案快?
  2. 哪个是将数千个图像读入一个大型numpy阵列的最快方法?
  3. 我是否可以从外观numpy和scikit-images中获益,以获得更快的图像读取模块?我试过plt.imread(),但scikit-image.io模块更快.

Div*_*kar 7

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

按照NumPy数组的行主要顺序存储元素的方式,在每次迭代时沿着最后一个轴存储这些元素时,您正在做正确的事情.这些将占用连续的内存位置,因此对于访问和分配值最有效.因此,如评论中所述,初始化喜欢np.ndarray((512*25,512), dtype='uint16')np.ndarray((25,512,512), dtype='uint16')将会发挥最佳作用.

将它们编译为funcs以测试时间并以随机数组而不是图像进食 -

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
Run Code Online (Sandbox Code Playgroud)

计时 -

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
Run Code Online (Sandbox Code Playgroud)

这些定时确认在一开始提出的表演理论,虽然我预期的最后一个设置定时有在药粥之间的时序app3app1,但也许从最后要到第一线,用于访问和分配效果不是线性的.对此问题的更多调查可能很有意思(这里有跟进问题).

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

App1:

[[[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]]]
Run Code Online (Sandbox Code Playgroud)

因此,在内存空间中,它将是:[x,o,x,o,x,o..]遵循行主要顺序.

App2:

[[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]]
Run Code Online (Sandbox Code Playgroud)

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

App3:

[[[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]]]
Run Code Online (Sandbox Code Playgroud)

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


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
Run Code Online (Sandbox Code Playgroud)


Ger*_*ges 5

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

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

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)
Run Code Online (Sandbox Code Playgroud)

结果显示:

__main__.static_list : 0.07004200006122119 milliseconds
__main__.dynamic_list : 0.10294799994881032 milliseconds
__main__.list_comprehension : 0.05021800006943522 milliseconds
__main__.numpy_flat : 309.80870099983804 milliseconds
Run Code Online (Sandbox Code Playgroud)

显然,您最好的选择是列表理解,但是即使填充一个 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)])
Run Code Online (Sandbox Code Playgroud)

和计时结果:

__main__.static_list : 303.32892100022946 milliseconds
__main__.dynamic_list : 301.86925499992867 milliseconds
__main__.list_comprehension : 300.76925699995627 milliseconds
__main__.numpy_flat : 305.9309459999895 milliseconds
Run Code Online (Sandbox Code Playgroud)

所以它需要更多时间只是一件很麻烦的事情,并且它是相对于数组大小的常量值......