即使您正在构建自己的库,您也绝对应该使用库进行卷积,它们将在后端用C或Fortran进行结果运算,这将大大加快速度。
但是,如果您愿意,可以自己做,使用线性可分离滤波器。 这是想法:
图片:
1 2 3 4 5
2 3 4 5 1
3 4 5 1 2
Sobel x内核:
-1 0 1
-2 0 2
-1 0 1
结果:
8, 3, -7
在卷积的第一个位置,您将计算9个值。 首先,为什么? 您永远不会添加中间列,不必费心将其相乘。 但这不是线性可分离滤波器的重点。 这个想法很简单。 将内核放在第一个位置时,会将第三列乘以[1, 2, 1] 。 但是随后的两个步骤,您将第三列乘以[-1, -2, -1] 。 真是浪费! 您已经计算过了,现在只需要取反即可。 这就是线性可分离滤波器的想法。 请注意,您可以将过滤器分解为两个向量的矩阵外积:
[1]
[2] * [-1, 0, 1]
[1]
将外部乘积在这里得到相同的矩阵。 因此,这里的想法是将操作分为两部分。 首先将整个图像乘以行向量,然后乘以列向量。 以行向量
-1 0 1
在整个图像上,我们最终得到
2 2 2
2 2 -3
2 -3 -3
然后将列向量传递给乘法和求和,我们再次得到
8, 3, -7
另一种可能有用或无效的妙招(取决于您在内存和效率之间的权衡):
请注意,在单行乘法中,您忽略中间值,而只从左边的值中减去右边的值。 这意味着您正在有效地执行以下操作减去这两个图像:
3 4 5 1 2 3
4 5 1 - 2 3 4
5 1 2 3 4 5
如果将图像的前两列剪掉,则会得到左矩阵,如果将后两列剪掉,则会得到右矩阵。 因此,您只需将卷积的第一部分计算为
result_h = img[:,2:] - img[:,:-2]
然后,您可以遍历sobel运算符的其余列。 或者,您甚至可以继续进行,然后做我们刚才做的相同的事情。 这次是垂直情况,您只需要添加第一行和第三行,第二行加倍即可; 或者,使用numpy加法:
result_v = result_h[:-2] + result_h[2:] + 2*result_h[1:-1]
大功告成! 我可能会在不久的将来在此处添加一些时间。 对于包络的某些计算(例如,在1000x1000图像上草草的Jupyter笔记本计时):
新方法(图像总和):每个循环8.18 ms±399 µs(平均±标准偏差,共运行7次,每个循环100个)
旧方法(双循环):每个循环7.32 s±207毫秒(平均±标准偏差,共运行7次,每个循环1次)
是的,您没看错:1000倍加速。
这是比较两者的一些代码:
import numpy as np
def sobel_x_orig(img):
xKernel = np.array([[-1,0,1],[-2,0,2],[-1,0,1]])
sobelled = np.zeros((img.shape[0]-2, img.shape[1]-2))
for y in range(1, img.shape[0]-1):
for x in range(1, img.shape[1]-1):
sobelled[y-1, x-1] = np.sum(np.multiply(img[y-1:y+2, x-1:x+2], xKernel))
return sobelled
def sobel_x_new(img):
result_h = img[:,2:] - img[:,:-2]
result_v = result_h[:-2] + result_h[2:] + 2*result_h[1:-1]
return result_v
img = np.random.rand(1000, 1000)
sobel_new = sobel_x_new(img)
sobel_orig = sobel_x_orig(img)
assert (np.abs(sobel_new-sobel_orig) < 1e-12).all()
当然, 1e-12容忍度很严格,但这是1e-12每个元素的,因此应该可以。 但是我还有一个float图像,对于uint8图像,您当然会有更大的差异。
请注意,您可以对任何线性可分离滤波器执行此操作! 包括高斯滤波器。 还要注意,通常,这需要很多操作。 在C或Fortran或任何其他语言中,通常仅将其实现为单行/列向量的两个卷积,因为最后,它实际上仍然需要遍历每个矩阵的每个元素; 不管您是将它们相加还是相乘,因此在C中用这种方法添加图像值并不比卷积要快。 但是遍历numpy数组非常慢,因此在Python中这种方法要快得多。