我正在尝试找到一种将目录中的一堆图像读入numpy数组的最快方法。我的最终目标是计算所有这些图像中像素的最大值,最小值和第n百分位数等统计数据。当所有图像中的像素都在一个大的numpy数组中时,这是直截了当的,因为我可以使用内置数组方法,如.max
和.min
,以及np.percentile
函数。
以下是一些具有25个tiff图像(512x512像素)的示例时序。这些基准来自于jupyter-notebook中的%%timit
。这些差异太小,不足以对25张图片产生任何实际影响,但我打算将来阅读数以千计的图片。
# Imports
import os
import skimage.io as io
import numpy as np
附加到列表
%%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)
使用字典
%%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毫秒。
沿第一维预先分配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)
沿第三维预先分配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 preallocation方法会更快,因为循环中没有动态变量扩展,但似乎并非如此。我发现最直观的方法是最后一个,其中每个图像沿阵列的第三轴占据一个单独的尺寸,但这也是最慢的。所需的额外时间不是由于预分配本身,只需要约1毫秒。
我有三个问题:
plt.imread()
,但scikit-image.io
模块更快。答案 0 :(得分:5)
A部分:访问和分配NumPy数组
按照NumPy数组的行主要顺序存储元素的方式,每次迭代时沿着最后一个轴存储这些元素时,你做的是正确的。这些将占用连续的内存位置,因此对于访问和分配值最有效。因此,sorted: [192.168.0.1, 192.168.0.5, 192.168.10.21, 192.168.25.1, 192.168.77.1]
或np.ndarray((512*25,512), dtype='uint16')
这样的初始化效果最佳,如评论中所述。
将它们编译为funcs以测试时间并以随机数组而非图像进行 -
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
和app3
的时间之间,但可能会产生影响。最后到第一个访问和分配isn线性的轴。对此问题的更多调查可能很有意思(follow up question here)。
为了示意地表示,考虑我们存储图像数组,由app1
(图像1)和x
(图像2)表示,我们将:
App1:
o
因此,在内存空间中,它将是:[[[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]]]
遵循行主要顺序。
App2:
[x,o,x,o,x,o..]
因此,在内存空间中,它将是:[[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]]
。
App3:
[x,x,x,x,x,x...o,o,o,o,o..]
因此,在内存空间中,它与前一个相同。
B部分:从磁盘读取图像作为数组
现在,关于阅读图像的部分,我已经看到OpenCV的[[[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]]]
要快得多。
作为测试,我从维基页面下载了蒙娜丽莎的图像,并在图像阅读上测试了性能 -
imread
答案 1 :(得分:4)
在这种情况下,大部分时间都花在从磁盘读取文件上,而且我不会太担心填充列表的时间。
在任何情况下,这里都是一个比较四种方法的脚本,没有从磁盘读取实际图像的开销,只是从内存中读取一个对象。
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数组,它只需310毫秒就可以读取1000张图像(来自内存)。所以再次,开销将是磁盘读取。
为什么numpy会变慢?
numpy将数组存储在内存中的方式。如果我们修改python list函数以将列表转换为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
因此,它需要更多时间,而且相对于数组大小而言是恒定值,这只是一个笨拙的事情......