[英]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
我希望這段代碼可以幫助其他有同樣疑問的人。
問候。
@Tashus 下面的評論是正確的,因此@dudemeister 的回答可能更符合標准。 他建議的函數也更有效,因為它避免了直接的 2D 卷積和所需的操作數量。
我相信你正在做兩個 1d 卷積,第一個每列和第二個每行,並用第二個的結果替換第一個的結果。
請注意,帶有'same'
參數的numpy.convolve
返回一個與所提供的最大數組形狀'same'
的數組,因此當您進行第一個卷積時,您已經填充了整個data
數組。
在這些步驟中可視化數組的一種好方法是使用Hinton 圖,這樣您就可以檢查哪些元素已經具有值。
您可以嘗試將兩個卷積的結果相加(使用data[:,c] += ..
而不是data[:,c] =
在第二個for
循環中),如果您的卷積矩陣是使用一個的結果維H_r
和H_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_stride
的numpy.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.