web-dev-qa-db-ja.com

数千の画像を1つの大きなnumpy配列に読み込む最速のアプローチ

私はディレクトリからnumpy配列に画像の束を読み込むための最速のアプローチを見つけようとしています。私の最終目標は、これらすべての画像からピクセルの最大、最小、n番目のパーセンタイルなどの統計を計算することです。 _.max_や_.min_、および_np.percentile_関数などの組み込み配列メソッドを使用できるため、すべての画像のピクセルが1つの大きなnumpy配列にある場合、これは簡単で高速です。

以下は、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)
    _
  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)
    _

上記のリストおよびディクショナリのアプローチでは、ループをそれぞれの理解度で置き換え、時間的に同様の結果を得ようとしました。また、辞書キーの事前割り当てを試みましたが、所要時間に大きな違いはありませんでした。リストから画像を大きな配列に取得するには、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)
    _
  2. 3次元に沿って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の事前割り当てのアプローチが高速になると考えましたが、そうではないようです。最も直感的なアプローチは、最後のアプローチです。各イメージは、配列の3番目の軸に沿って個別の次元を占有しますが、これも最も低速です。追加の所要時間は、事前割り当て自体によるものではなく、1ミリ秒程度しかかかりません。

これに関して3つの質問があります。

  1. Numpyの事前割り当てアプローチが、辞書およびリストソリューションよりも速くないのはなぜですか?
  2. 1つの大きなnumpy配列に何千もの画像を読み込む最も速い方法はどれですか?
  3. Numpyとscikit-imageの外側を見ると、画像を読み込むためのさらに高速なモジュールが得られますか? plt.imread()を試しましたが、_scikit-image.io_モジュールの方が高速です。
18
joelostblom

パート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

これらのタイミングは、最初に提案されたパフォーマンス理論を確認しますが、最後のセットアップのタイミングはapp3およびapp1、ただし、アクセスおよび割り当てのために最後の軸から最初の軸に移動する効果は線形ではありません。これについてのさらなる調査は興味深いかもしれません( 質問のフォローアップ )。

概略を明確にするために、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]]]

したがって、メモリ空間では、次のようになります。[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]]

したがって、メモリ空間では、次のようになります。[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]]]

したがって、メモリ空間では、前のものと同じになります。


パート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
7
Divakar

この場合、ほとんどの時間はディスクからのファイルの読み取りに費やされ、リストを作成する時間についてはあまり心配しません。

いずれにせよ、これは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配列にデータを追加しても、1000メモリ(メモリから)を読み込むのにわずか310 msです。この場合も、オーバーヘッドはディスクの読み取りになります。

なぜ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

ですから、もっと時間がかかり、配列サイズに対して一定の値です...

4
Gerges