21

画像の各ピクセルについて、ピクセルの近傍にあるピクセルの標準偏差を計算する機能を改善しようとしています。私の関数は 2 つの埋め込みループを使用して行列全体を実行していますが、これが私のプログラムのボトルネックになっています。numpyのおかげでループを取り除くことで改善する方法があると思いますが、どうすればよいかわかりません。どんなアドバイスも大歓迎です!

よろしく

def sliding_std_dev(image_original,radius=5) :
    height, width = image_original.shape
    result = np.zeros_like(image_original) # initialize the output matrix
    hgt = range(radius,height-radius)
    wdt = range(radius,width-radius)
    for i in hgt:
        for j in wdt:
            result[i,j] = np.std(image_original[i-radius:i+radius,j-radius:j+radius])
    return result
4

5 に答える 5

32

クールなトリック: 二乗値の合計とウィンドウ内の値の合計だけを指定して、標準偏差を計算できます。

したがって、データに均一なフィルターを使用すると、標準偏差を非常に高速に計算できます。

from scipy.ndimage.filters import uniform_filter

def window_stdev(arr, radius):
    c1 = uniform_filter(arr, radius*2, mode='constant', origin=-radius)
    c2 = uniform_filter(arr*arr, radius*2, mode='constant', origin=-radius)
    return ((c2 - c1*c1)**.5)[:-radius*2+1,:-radius*2+1]

これは、元の関数よりも途方もなく高速です。1024x1024 の配列で半径が 20 の場合、古い関数は 34.11 秒かかり、新しい関数は0.11 秒かかり、300 倍の速度になります。


これは数学的にどのように機能しますか? sqrt(mean(x^2) - mean(x)^2)各ウィンドウの数量を計算します。この量は、sqrt(mean((x - mean(x))^2))次のように標準偏差から導出できます。

Eを期待演算子 (基本的にmean()) とし、データの確率X変数とします。それで:

E[(X - E[X])^2]
= E[X^2 - 2X*E[X] + E[X]^2]
= E[X^2] - E[2X*E[X]] + E[E[X]^2](期待値演算子の線形性
= E[X^2] - 2E[X]*E[X] + E[X]^2による) (再び線形性とそれE[X]が定数であるという事実による)
= E[X^2] - E[X]^2

これは、この手法を使用して計算された量が数学的に標準偏差に等しいことを証明しています。

于 2013-08-24T19:50:07.340 に答える
13

画像処理でこの種のことを行うために最も頻繁に使用される方法は、1984 年にこの論文で紹介されたアイデアである合計面積テーブルを使用することです。右に 1 ピクセル移動する場合、新しいウィンドウにすべての項目を追加する必要はありません。合計から左端の列を差し引いて、新しい右端の列を追加するだけで済みます。したがって、配列から両方の次元で累積合計配列を作成すると、いくつかの合計と減算を使用して、ウィンドウ全体の合計を取得できます。配列とその正方形の合計面積テーブルを保持している場合、これら 2 つの分散を取得するのは非常に簡単です。実装は次のとおりです。

def windowed_sum(a, win):
    table = np.cumsum(np.cumsum(a, axis=0), axis=1)
    win_sum = np.empty(tuple(np.subtract(a.shape, win-1)))
    win_sum[0,0] = table[win-1, win-1]
    win_sum[0, 1:] = table[win-1, win:] - table[win-1, :-win]
    win_sum[1:, 0] = table[win:, win-1] - table[:-win, win-1]
    win_sum[1:, 1:] = (table[win:, win:] + table[:-win, :-win] -
                       table[win:, :-win] - table[:-win, win:])
    return win_sum

def windowed_var(a, win):
    win_a = windowed_sum(a, win)
    win_a2 = windowed_sum(a*a, win)
    return (win_a2 - win_a * win_a / win/ win) / win / win

これが機能することを確認するには:

>>> a = np.arange(25).reshape(5,5)
>>> windowed_var(a, 3)
array([[ 17.33333333,  17.33333333,  17.33333333],
       [ 17.33333333,  17.33333333,  17.33333333],
       [ 17.33333333,  17.33333333,  17.33333333]])
>>> np.var(a[:3, :3])
17.333333333333332
>>> np.var(a[-3:, -3:])
17.333333333333332

これは、畳み込みベースの方法よりも数ノッチ速く実行されるはずです。

于 2013-08-24T22:36:44.053 に答える
3

まず、これを行うには複数の方法があります。

速度の点で最も効率的ではありませんが、を使用scipy.ndimage.generic_filterすると、移動するウィンドウに任意の python 関数を簡単に適用できます。

簡単な例として:

result = scipy.ndimage.generic_filter(data, np.std, size=2*radius)

mode境界条件はkwarg で制御できることに注意してください。


これを行う別の方法は、いくつかのさまざまなストライド トリックを使用して、効果的に移動ウィンドウである配列のビューを作成しnp.std、最後の軸に沿って適用することです。(注:これは私の以前の回答の1つから取られています:https://stackoverflow.com/a/4947453/325565

def strided_sliding_std_dev(data, radius=5):
    windowed = rolling_window(data, (2*radius, 2*radius))
    shape = windowed.shape
    windowed = windowed.reshape(shape[0], shape[1], -1)
    return windowed.std(axis=-1)

def rolling_window(a, window):
    """Takes a numpy array *a* and a sequence of (or single) *window* lengths
    and returns a view of *a* that represents a moving window."""
    if not hasattr(window, '__iter__'):
        return rolling_window_lastaxis(a, window)
    for i, win in enumerate(window):
        if win > 1:
            a = a.swapaxes(i, -1)
            a = rolling_window_lastaxis(a, win)
            a = a.swapaxes(-2, i)
    return a

def rolling_window_lastaxis(a, window):
    """Directly taken from Erik Rigtorp's post to numpy-discussion.
    <http://www.mail-archive.com/numpy-discussion@scipy.org/msg29450.html>"""
    if window < 1:
       raise ValueError, "`window` must be at least 1."
    if window > a.shape[-1]:
       raise ValueError, "`window` is too long."
    shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
    strides = a.strides + (a.strides[-1],)
    return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)

一見すると、ここで何が起こっているのかを理解するのは少し難しいです。私自身の答えの1つを差し込むのではなく、説明を再入力したくないので、https ://stackoverflow.com/a/4924433/325565を見てください。前に「ストライディング」トリック。

a が 5 のランダムな float の 100x100 配列でタイミングを比較すると、オリジナルまたはバージョンradiusよりも ~10 倍高速です。generic_filterただし、このバージョンでは境界条件に柔軟性がありません。(これは現在行っていることと同じですが、generic_filterバージョンは速度を犠牲にして多くの柔軟性を提供します。)

# Your original function with nested loops
In [21]: %timeit sliding_std_dev(data)
1 loops, best of 3: 237 ms per loop

# Using scipy.ndimage.generic_filter
In [22]: %timeit ndimage_std_dev(data)
1 loops, best of 3: 244 ms per loop

# The "stride-tricks" version above
In [23]: %timeit strided_sliding_std_dev(data)
100 loops, best of 3: 15.4 ms per loop

# Ophion's version that uses `np.take`
In [24]: %timeit new_std_dev(data)
100 loops, best of 3: 19.3 ms per loop

「ストライド トリック」バージョンの欠点は、「通常の」ストライド ローリング ウィンドウ トリックとは異なり、このバージョンでコピーが作成され、元の配列よりもはるかに大きいことです。これを大きな配列で使用すると、メモリの問題が発生します。(ちなみに、メモリ使用量と速度の点では、@ Ophionの回答と基本的に同等です。同じことを行うための別のアプローチです。)

于 2013-08-24T16:36:19.647 に答える
1

最初にインデックスを取得してnp.takeから、新しい配列を形成するために使用できます。

def new_std_dev(image_original,radius=5):
    cols,rows=image_original.shape

    #First obtain the indices for the top left position
    diameter=np.arange(radius*2)
    x,y=np.meshgrid(diameter,diameter)
    index=np.ravel_multi_index((y,x),(cols,rows)).ravel()

    #Cast this in two dimesions and take the stdev
    index=index+np.arange(rows-radius*2)[:,None]+np.arange(cols-radius*2)[:,None,None]*(rows)
    data=np.std(np.take(image_original,index),-1)

    #Add the zeros back to the output array
    top=np.zeros((radius,rows-radius*2))
    sides=np.zeros((cols,radius))

    data=np.vstack((top,data,top))
    data=np.hstack((sides,data,sides))
    return data

最初にいくつかのランダム データを生成し、タイミングを確認します。

a=np.random.rand(50,20)

print np.allclose(new_std_dev(a),sliding_std_dev(a))
True

%timeit sliding_std_dev(a)
100 loops, best of 3: 18 ms per loop

%timeit new_std_dev(a)
1000 loops, best of 3: 472 us per loop

より大きな配列の場合、十分なメモリがある限り、常に高速です。

a=np.random.rand(200,200)

print np.allclose(new_std_dev(a),sliding_std_dev(a))
True

%timeit sliding_std_dev(a)
1 loops, best of 3: 1.58 s per loop

%timeit new_std_dev(a)
10 loops, best of 3: 52.3 ms per loop

元の関数は、非常に小さな配列の場合は高速です。損益分岐点はいつのようhgt*wdt >50です。関数が正方形のフレームを取り、インデックスの周りをサンプリングするのではなく、右下のインデックスに std dev を配置していることに注意してください。これは意図的なものですか?

于 2013-08-24T16:44:12.840 に答える