簡體   English   中英

使用 python 和 numpy 的二維卷積

[英]2d convolution using python and numpy

我正在嘗試使用 numpy 在 python 中執行二維卷積

我有一個二維數組,如下所示,行為 kernel H_r,列為 H_c

data = np.zeros((nr, nc), dtype=np.float32)

#fill array with some data here then convolve

for r in range(nr):
    data[r,:] = np.convolve(data[r,:], H_r, 'same')

for c in range(nc):
    data[:,c] = np.convolve(data[:,c], H_c, 'same')

data = data.astype(np.uint8);

它不會產生我所期待的 output,這段代碼看起來沒問題嗎,我認為問題在於從 float32 到 8 位的轉換。 最好的方法是什么

謝謝

也許它不是最優化的解決方案,但這是我之前在 Python 的 numpy 庫中使用的一個實現:

def convolution2d(image, kernel, bias):
    m, n = kernel.shape
    if (m == n):
        y, x = image.shape
        y = y - m + 1
        x = x - m + 1
        new_image = np.zeros((y,x))
        for i in range(y):
            for j in range(x):
                new_image[i][j] = np.sum(image[i:i+m, j:j+m]*kernel) + bias
return new_image

我希望這段代碼可以幫助其他有同樣疑問的人。

問候。

編輯 [2019 年 1 月]

@Tashus 下面的評論是正確的,因此@dudemeister 的回答可能更符合標准。 他建議的函數也更有效,因為它避免了直接的 2D 卷積和所需的操作數量。

可能的問題

我相信你正在做兩個 1d 卷積,第一個每列和第二個每行,並用第二個的結果替換第一個的結果。

請注意,帶有'same'參數的numpy.convolve返回一個與所提供的最大數組形狀'same'的數組,因此當您進行第一個卷積時,您已經填充了整個data數組。

在這些步驟中可視化數組的一種好方法是使用Hinton 圖,這樣您就可以檢查哪些元素已經具有值。

可能的解決方案

您可以嘗試將兩個卷積的結果相加(使用data[:,c] += ..而不是data[:,c] =在第二個for循環中),如果您的卷積矩陣是使用一個的結果維H_rH_c矩陣如下:

卷積核加法

另一種方法是將scipy.signal.convolve2d與 2d 卷積數組一起使用,這可能是您首先想要做的。

由於您已經分離了內核,您應該簡單地使用 scipy 中的 sepfir2d 函數:

from scipy.signal import sepfir2d
convolved = sepfir2d(data, H_r, H_c)

另一方面,您在那里的代碼看起來不錯......

它也可能不是最優化的解決方案,但它比@omotto 提出的解決方案快大約十倍,並且它只使用基本的 numpy 函數(如 reshape、expand_dims、tile...)並且沒有 'for' 循環:

def gen_idx_conv1d(in_size, ker_size):
    """
    Generates a list of indices. This indices correspond to the indices
    of a 1D input tensor on which we would like to apply a 1D convolution.

    For instance, with a 1D input array of size 5 and a kernel of size 3, the
    1D convolution product will successively looks at elements of indices [0,1,2],
    [1,2,3] and [2,3,4] in the input array. In this case, the function idx_conv1d(5,3) 
    outputs the following array: array([0,1,2,1,2,3,2,3,4]).

    args:
        in_size: (type: int) size of the input 1d array.
        ker_size: (type: int) kernel size.

    return:
        idx_list: (type: np.array) list of the successive indices of the 1D input array
        access to the 1D convolution algorithm.

    example:
        >>> gen_idx_conv1d(in_size=5, ker_size=3)
        array([0, 1, 2, 1, 2, 3, 2, 3, 4])
    """
    f = lambda dim1, dim2, axis: np.reshape(np.tile(np.expand_dims(np.arange(dim1),axis),dim2),-1)
    out_size = in_size-ker_size+1
    return f(ker_size, out_size, 0)+f(out_size, ker_size, 1)

def repeat_idx_2d(idx_list, nbof_rep, axis):
    """
    Repeats an array of indices (idx_list) a number of time (nbof_rep) "along" an axis
    (axis). This function helps to browse through a 2d array of size
    (len(idx_list),nbof_rep).

    args:
        idx_list: (type: np.array or list) a 1D array of indices.
        nbof_rep: (type: int) number of repetition.
        axis: (type: int) axis "along" which the repetition will be applied.

    return
        idx_list: (type: np.array) a 1D array of indices of size len(idx_list)*nbof_rep.

    example:
        >>> a = np.array([0, 1, 2])
        >>> repeat_idx_2d(a, 3, 0) # repeats array 'a' 3 times along 'axis' 0
        array([0, 0, 0, 1, 1, 1, 2, 2, 2])

        >>> repeat_idx_2d(a, 3, 1) # repeats array 'a' 3 times along 'axis' 1
        array([0, 1, 2, 0, 1, 2, 0, 1, 2])

        >>> b = np.reshape(np.arange(3*4), (3,4))
        >>> b[repeat_idx_2d(np.arange(3), 4, 0), repeat_idx_2d(np.arange(4), 3, 1)]
        array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
    """
    assert axis in [0,1], "Axis should be equal to 0 or 1."
    tile_axis = (nbof_rep,1) if axis else (1,nbof_rep)
    return np.reshape(np.tile(np.expand_dims(idx_list, 1),tile_axis),-1)

def conv2d(im, ker):
    """
    Performs a 'valid' 2D convolution on an image. The input image may be
    a 2D or a 3D array.

    The output image first two dimensions will be reduced depending on the 
    convolution size. 

    The kernel may be a 2D or 3D array. If 2D, it will be applied on every
    channel of the input image. If 3D, its last dimension must match the
    image one.

    args:
        im: (type: np.array) image (2D or 3D).
        ker: (type: np.array) convolution kernel (2D or 3D).

    returns:
        im: (type: np.array) convolved image.

    example:
        >>> im = np.reshape(np.arange(10*10*3),(10,10,3))/(10*10*3) # 3D image
        >>> ker = np.array([[0,1,0],[-1,0,1],[0,-1,0]]) # 2D kernel
        >>> conv2d(im, ker) # 3D array of shape (8,8,3)
    """
    if len(im.shape)==2: # it the image is a 2D array, it is reshaped by expanding the last dimension
        im = np.expand_dims(im,-1)

    im_x, im_y, im_w = im.shape

    if len(ker.shape)==2: # if the kernel is a 2D array, it is reshaped so it will be applied to all of the image channels
        ker = np.tile(np.expand_dims(ker,-1),[1,1,im_w]) # the same kernel will be applied to all of the channels 

    assert ker.shape[-1]==im.shape[-1], "Kernel and image last dimension must match."

    ker_x = ker.shape[0]
    ker_y = ker.shape[1]

    # shape of the output image
    out_x = im_x - ker_x + 1 
    out_y = im_y - ker_y + 1

    # reshapes the image to (out_x, ker_x, out_y, ker_y, im_w)
    idx_list_x = gen_idx_conv1d(im_x, ker_x) # computes the indices of a 1D conv (cf. idx_conv1d doc)
    idx_list_y = gen_idx_conv1d(im_y, ker_y)

    idx_reshaped_x = repeat_idx_2d(idx_list_x, len(idx_list_y), 0) # repeats the previous indices to be used in 2D (cf. repeat_idx_2d doc)
    idx_reshaped_y = repeat_idx_2d(idx_list_y, len(idx_list_x), 1)

    im_reshaped = np.reshape(im[idx_reshaped_x, idx_reshaped_y, :], [out_x, ker_x, out_y, ker_y, im_w]) # reshapes

    # reshapes the 2D kernel
    ker = np.reshape(ker,[1, ker_x, 1, ker_y, im_w])

    # applies the kernel to the image and reduces the dimension back to the one of original input image
    return np.squeeze(np.sum(im_reshaped*ker, axis=(1,3)))

我試圖添加很多注釋來解釋該方法,但總體思路是將 3D 輸入圖像重塑為形狀 (output_image_height, kernel_height, output_image_width, kernel_width, output_image_channel) 的 5D 之一,然后直接使用基本的應用內核數組乘法。 當然,這種方法會使用更多的內存(在執行過程中,圖像的大小因此乘以 kernel_height*kernel_width)但速度更快。

為了完成這個重塑步驟,我“過度使用”了 numpy 數組的索引方法,尤其是將 numpy 數組作為 numpy 數組的索引的可能性。

這種方法也可用於使用基本數學函數在 Pytorch 或 Tensorflow 中重新編碼 2D 卷積產品,但我毫不懷疑地說它會比現有的 nn.conv2d 運算符慢......

我真的很喜歡只使用 numpy 基本工具來編寫這種方法。

我檢查了許多實現,但沒有找到適合我的目的,這應該非常簡單。 所以這是一個非常簡單的 for 循環實現

def convolution2d(image, kernel, stride, padding):
    image = np.pad(image, [(padding, padding), (padding, padding)], mode='constant', constant_values=0)

    kernel_height, kernel_width = kernel.shape
    padded_height, padded_width = image.shape

    output_height = (padded_height - kernel_height) // stride + 1
    output_width = (padded_width - kernel_width) // stride + 1

    new_image = np.zeros((output_height, output_width)).astype(np.float32)

    for y in range(0, output_height):
        for x in range(0, output_width):
            new_image[y][x] = np.sum(image[y * stride:y * stride + kernel_height, x * stride:x * stride + kernel_width] * kernel).astype(np.float32)
    return new_image

嘗試第一輪然后投射到 uint8:

data = data.round().astype(np.uint8);

最明顯的方法之一是對內核進行硬編碼。

img = img.convert('L')
a = np.array(img)
out = np.zeros([a.shape[0]-2, a.shape[1]-2], dtype='float')
out += a[:-2, :-2]
out += a[1:-1, :-2]
out += a[2:, :-2]
out += a[:-2, 1:-1]
out += a[1:-1,1:-1]
out += a[2:, 1:-1]
out += a[:-2, 2:]
out += a[1:-1, 2:]
out += a[2:, 2:]
out /= 9.0
out = out.astype('uint8')
img = Image.fromarray(out)

這個例子做了一個完全展開的 3x3 框模糊。 您可以將具有不同值的值相乘,然后將它們除以不同的數量。 但是,如果你真的想要最快和最臟的方法,就是這樣。 我認為它比 Guillaume Mougeot 的方法高 5 倍。他的方法比其他方法高 10 倍。

如果您正在執行諸如高斯模糊之類的操作,它可能會丟失幾步。 並且需要乘以一些東西。

我寫了這個使用convolve_stridenumpy.lib.stride_tricks.as_strided 此外,它支持步幅和擴張。 它也兼容階數 > 2 的張量。

import numpy as np
from numpy.lib.stride_tricks import as_strided
from im2col import im2col

def conv_view(X, F_s, dr, std):
    X_s = np.array(X.shape)
    F_s = np.array(F_s)
    dr = np.array(dr)
    Fd_s = (F_s - 1) * dr + 1
    if np.any(Fd_s > X_s):
        raise ValueError('(Dilated) filter size must be smaller than X')
    std = np.array(std)
    X_ss = np.array(X.strides)
    Xn_s = (X_s - Fd_s) // std + 1
    Xv_s = np.append(Xn_s, F_s)
    Xv_ss = np.tile(X_ss, 2) * np.append(std, dr)
    return as_strided(X, Xv_s, Xv_ss, writeable=False)

def convolve_stride(X, F, dr=None, std=None):
    if dr is None:
        dr = np.ones(X.ndim, dtype=int)
    if std is None:
        std = np.ones(X.ndim, dtype=int)
    if not (X.ndim == F.ndim == len(dr) == len(std)):
        raise ValueError('X.ndim, F.ndim, len(dr), len(std) must be the same')
    Xv = conv_view(X, F.shape, dr, std)
    return np.tensordot(Xv, F, axes=X.ndim)

%timeit -n 100 -r 10 convolve_stride(A, F)
#31.2 ms ± 1.31 ms per loop (mean ± std. dev. of 10 runs, 100 loops each)

僅使用基本 numpy 的超簡單快速卷積:

import numpy as np

def conv2d(image, kernel):
    # apply kernel to image, return image of the same shape
    # assume both image and kernel are 2D arrays
    # kernel = np.flipud(np.fliplr(kernel))  # optionally flip the kernel
    k = kernel.shape[0]
    width = k//2
    # place the image inside a frame to compensate for the kernel overlap
    a = framed(image, width)
    b = np.zeros(image.shape)  # fill the output array with zeros; do not use np.empty()
    # shift the image around each pixel, multiply by the corresponding kernel value and accumulate the results
    for p, dp, r, dr in [(i, i + image.shape[0], j, j + image.shape[1]) for i in range(k) for j in range(k)]:
        b += a[p:dp, r:dr] * kernel[p, r]
    # or just write two nested for loops if you prefer
    # np.clip(b, 0, 255, out=b)  # optionally clip values exceeding the limits
    return b

def framed(image, width):
    a = np.zeros((image.shape[0]+2*width, image.shape[1]+2*width))
    a[width:-width, width:-width] = image
    # alternatively fill the frame with ones or copy border pixels
    return a

運行:

Image.fromarray(conv2d(image, kernel).astype('uint8'))

不是沿着圖像滑動 kernel 並逐像素計算變換,而是創建一系列與 kernel 中的每個元素相對應的圖像移位版本,並將相應的 kernel 值應用於每個移位圖像版本。

這可能是使用基本 numpy 可以獲得的最快速度; 速度已經與 C 的 scipy convolve2d 實現相當,並且優於 fftconvolve。 這個想法類似於@Tatarize。 此示例僅適用於一種顏色分量; 對於 RGB,只需重復每個(或相應地修改算法)。

通常, Convolution 2D是用詞不當。 理想情況下,在引擎蓋下,所做的是 2 個矩陣的相關性

pad == same 返回與輸入維度相同的 output

它還可以拍攝不對稱圖像。 為了對一批二維矩陣執行相關(深度學習術語中的卷積),可以迭代所有通道,計算每個通道切片與相應濾波器切片的相關性。

例如:如果圖像為 (28,28,3) 並且濾波器大小為 (5,5,3),則從圖像通道中取出 3 個切片中的每一個,並使用上面的自定義 function 執行互相關,並將結果矩陣疊加在 output 的相應維度中。

def get_cross_corr_2d(W, X, pad = 'valid'):

   if(pad == 'same'):
       pr = int((W.shape[0] - 1)/2)
       pc = int((W.shape[1] - 1)/2)
       conv_2d = np.zeros((X.shape[0], X.shape[1]))
       X_pad = np.zeros((X.shape[0] + 2*pr, X.shape[1] + 2*pc))
       X_pad[pr:pr+X.shape[0], pc:pc+X.shape[1]] = X
       for r in range(conv_2d.shape[0]):
           for c in range(conv_2d.shape[1]):
               conv_2d[r,c] = np.sum(np.inner(W, X_pad[r:r+W.shape[0], c:c+W.shape[1]]))
       return conv_2d
    
   else:    
       pr = W.shape[0] - 1
       pc = W.shape[1] - 1
       conv_2d = np.zeros((X.shape[0] - W.shape[0] + 2*pr + 1,
                           X.shape[1] - W.shape[1] + 2*pc + 1))
       X_pad = np.zeros((X.shape[0] + 2*pr, X.shape[1] + 2*pc))
       X_pad[pr:pr+X.shape[0], pc:pc+X.shape[1]] = X
       for r in range(conv_2d.shape[0]):
           for c in range(conv_2d.shape[1]):
               conv_2d[r,c] = np.sum(np.multiply(W, X_pad[r:r+W.shape[0], c:c+W.shape[1]]))
       return conv_2d

此代碼不正確:

for r in range(nr):
    data[r,:] = np.convolve(data[r,:], H_r, 'same')

for c in range(nc):
    data[:,c] = np.convolve(data[:,c], H_c, 'same')

請參閱從多維卷積到一維的 Nussbaumer 變換。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM