1

我正在用 Python 从头开始​​编写一个计算机视觉库rpi来使用相机。目前,我已经实现了转换为greyscale和其他一些基本img操作,它们在我的model B rpi3.

sobel但是,我使用运算符(维基百科描述)的边缘检测功能比其他功能慢得多,尽管它确实有效。这里是:

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并用这张猫的图像运行它:

灰阶猫

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

猫边

该库的应用,特别是这个功能,是在一个下棋机器人上,边缘检测将有助于识别棋子的位置。问题是运行需要>15几秒钟,这是一个严重的问题,因为它会大大增加机器人移动所需的时间。

我的问题是:我怎样才能加快速度?

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

  1. 而不是squaringthen adding,然后square rootinggxgy值来获得总梯度,我只是sumabsolute。这大大提高了速度。

  2. 使用resolution来自rpi相机的较低图像。这显然是一种使这些操作运行得更快的简单方法,但它并不是真正可行的,因为它在最小可用分辨率下仍然非常慢,480x360从相机的最大值大幅降低3280x2464

  3. 编写嵌套的 for 循环来matrix convolutions代替np.sum(np.multiply(...)). 这最终稍微了一点,这让我感到惊讶,因为np.multiply返回了一个新数组,我认为使用loops. 我认为虽然这可能是由于numpy大部分是写入的,C或者新数组实际上没有存储,所以不需要很长时间,但我不太确定。

任何帮助将不胜感激 - 我认为改进的主要内容是 point 3,即matrix乘法和求和。

4

3 回答 3

9

即使您正在构建自己的库,您确实应该绝对使用库进行卷积,它们将在后端使用 C 或 Fortran 执行结果操作,这将快得多。

但是,如果您愿意,可以自己做,使用线性可分离过滤器。这是想法:

图片:

1 2 3 4 5
2 3 4 5 1
3 4 5 1 2

索贝尔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 毫秒 ± 399 微秒(平均值 ± 标准偏差。7 次运行,每次 100 次循环)

旧方法(双 for 循环):每循环 7.32 秒 ± 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这是一些严重的公差,但这是每个元素,所以应该没问题。但我也有一个float图像,你当然会有更大的uint8图像差异。

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

于 2017-10-01T15:36:02.247 回答
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]

与 9 次乘法和 9 次加法相比,该点 2 次乘法 3 次减法和 3 次加法并且没有循环,其中天真的方法是循环内核。这应该会显着减少计算时间。

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

于 2020-06-12T00:24:11.213 回答
0

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

于 2021-03-22T14:15:32.003 回答