如何提高索贝尔边缘探测器的效率

时间:2017-10-01 14:16:35

标签: python performance numpy edge-detection sobel

我在Python中从头开始编写computer vision library以使用rpi相机。目前,我已实施转换为greyscale和其他一些基本img操作,这些操作在model B rpi3上运行速度相对较快。

但是,使用sobel运算符(wikipedia description)的边缘检测功能比其他功能要慢得多,尽管它确实有效。这是:

def sobel(img):
    xKernel = np.array([[-1,0,1],[-2,0,2],[-1,0,1]])
    yKernel = np.array([[-1,-2,-1],[0,0,0],[1,2,1]])
    sobelled = np.zeros((img.shape[0]-2, img.shape[1]-2, 3), dtype="uint8")
    for y in range(1, img.shape[0]-1):
        for x in range(1, img.shape[1]-1):
            gx = np.sum(np.multiply(img[y-1:y+2, x-1:x+2], xKernel))
            gy = np.sum(np.multiply(img[y-1:y+2, x-1:x+2], yKernel))
            g = abs(gx) + abs(gy) #math.sqrt(gx ** 2 + gy ** 2) (Slower)
            g = g if g > 0 and g < 255 else (0 if g < 0 else 255)
            sobelled[y-1][x-2] = g
    return sobelled

并使用此猫的greyscale图像运行它:

greyscale cat

我得到了这个回复,这似乎是正确的:

cat edges

图书馆的应用,特别是这个功能,是一个国际象棋游戏机器人,其中边缘检测将有助于识别棋子的位置。问题是运行需要>15秒,这是一个重大问题,因为它会增加机器人进行大量移动所需的时间。

我的问题是:如何加快速度?

到目前为止,我尝试了几件事:

  1. 取代squaring然后adding,然后square rooting gxgy值来获得总梯度,我只是{{1 } sum值。这样可以提高速度。

  2. 使用absolute相机中较低的resolution图像。这显然是一种简单的方法,可以使这些操作运行得更快,但它不是那么可行,因为它在rpi的最小可用分辨率上仍然非常慢,这可以从相机的最大值{{{1}大幅减少。 1}}。

  3. 编写嵌套的for循环来代替480x360执行3280x2464。由于matrix convolutions返回一个新数组,因此我感到很惊讶,我认为用np.sum(np.multiply(...))执行此操作应该会更快。我认为这可能是因为np.multiply主要是用loops写的,或者新数组实际上没有存储,所以不需要很长时间,但我和#39;我不太确定。

  4. 我们非常感谢任何帮助 - 我认为最重要的是改进点numpy,即C乘法和求和。

3 个答案:

答案 0 :(得分:4)

即使您正在构建自己的库,但您确实应该绝对使用库进行卷积,它们将在后端执行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 ms(平均值±标准偏差,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是一种严重的容忍度,但这是每个元素所以应该没问题。但我也有float图片,你当然会对uint8图片有更大的差异。

请注意,您可以为任何线性可分离过滤器执行此操作!这包括高斯滤波器。另请注意,通常,这需要大量操作。在C或Fortran等等中,它通常只是作为单行/列向量的两个卷积来实现,因为最后它需要实际遍历每个矩阵的每个元素;无论你是只是添加它们还是将它们相乘,所以在C中添加图像值的方式并不比你刚刚进行卷积时更快。但循环遍历numpy数组非常慢,因此这种方法在Python中更快。

答案 1 :(得分:1)

要加上什么价值:

Sobel x kernel:
-1 0 1
-2 0 2
-1 0 1

您不需要单独的内核。 1/3的运算始终为零。只是不计算它们。 其余的可以简化:

sum = -inputvalue[y-1][x-1] - 2 * inputvalue[y][x-1] - inputvalue[y+1][x-1]
+ inputvalue[y-1][x+1] + 2 * inputvalue[y][x+1] + inputvalue[y+1][x+1]

该点的2个乘法3个减法和3个加法以及NO循环,相比之下,朴素的方法是遍历内核的9个乘法和9个加法。 这样可以显着减少计算时间。

我对上面的numpy示例中提到的1000倍加速感到惊讶。 但是这里的这种方法帮助我大大提高了速度。 :)

答案 2 :(得分:0)

我遇到了同样的问题,并且能够使用 Numba 库中的 @jit 将我的代码加速大约 600 倍(请参阅链接:https://numba.pydata.org/numba-doc/latest/user/5minguide.html)。 在我的函数上方添加 @jit(nopython=True) 就足以完成这项工作。