【问题标题】:The fastest way to exclude surrounding zeros from an array representing an image?从表示图像的数组中排除周围零的最快方法?
【发布时间】:2018-09-03 12:08:34
【问题描述】:

我有一个包含从.png 创建的灰度图像的二维数组,如下所示:

import cv2

img = cv2.imread("./images/test.png")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

我想做的是提取一个仅包含包含数据的矩形的子数组 - 忽略图片周围的所有零

例如,如果输入是:

  0   0   0   0   0   0   0   0
  0   0   0   0   0   0   0   0
  0   0 175   0   0   0  71   0
  0   0   0  12   8  54   0   0
  0   0   0   0 255   0   0   0
  0   0   0   2   0   0   0   0
  0   0   0   0   0   0   0   0
  0   0   0   0   0   0   0   0

那么输出应该是:

175   0   0   0  71
  0  12   8  54   0
  0   0 255   0   0
  0   2   0   0   0

我可以向前遍历行以找到第一个非零行,然后向后遍历行以找到最后一个非零行记住索引 - 然后对列重复相同的操作,然后使用该数据提取子数组但我确信有更合适的方法来做同样的事情,甚至可能有一个为此目的而设计的 NumPy 函数。

如果我要在最短代码和最快执行之间进行选择,我会对最快的代码执行更感兴趣。

编辑:
我没有包括最好的例子,因为中间可能有零行/列,如下所示:

输入:

  0   0   0   0   0   0   0   0
  0   0   0   0   0   0   0   0
  0   0 175   0   0   0  71   0
  0   0   0  12   8  54   0   0
  0   0   0   0 255   0   0   0
  0   0   0   0   0   0   0   0
  0   0   0   2   0   0   0   0
  0   0   0   0   0   0   0   0

输出:

175   0   0   0  71
  0  12   8  54   0
  0   0 255   0   0
  0   0   0   0   0
  0   2   0   0   0

【问题讨论】:

  • If I were to choose between shortest code vs fastest execution I'd be more interested in fastest code. fastest code 你的意思是最快的执行,对吧?
  • 是的,我刚刚更正了措辞。
  • 你可以在不迭代的情况下做到这一点,首先屏蔽>0,然后询问每个轴上的最小和最大掩码索引,然后对其进行切片。我不认为那时它会明显更快或更易读,但是……可能值得一试。
  • @abarnet:你为什么不写下答案让我接受呢?似乎 droooze 确实写了一个正确的答案,但后来他删除了它:-/
  • @Chupo_cro 抱歉,我想更改一些内容以使其更通用。

标签: python arrays numpy image-processing


【解决方案1】:

注意,不是 OpenCV 解决方案 - 这通常适用于 n 维 NumPySciPy 数组

(基于 Divakar 的回答,扩展到 n 维)

def crop_new(arr):

    mask = arr != 0
    n = mask.ndim
    dims = range(n)
    slices = [None]*n

    for i in dims:
        mask_i = mask.any(tuple(dims[:i] + dims[i+1:]))
        slices[i] = (mask_i.argmax(), len(mask_i) - mask_i[::-1].argmax())

    return arr[[slice(*s) for s in slices]]

速度测试:

In [42]: np.random.seed(0)

In [43]: a = np.zeros((30, 30, 30, 20),dtype=np.uint8)

In [44]: a[2:-2, 2:-2, 2:-2, 2:-2] = np.random.randint(0,255,(26,26,26,16),dtype
=np.uint8)

In [45]: timeit crop(a) # Old solution
1 loop, best of 3: 181 ms per loop

In [46]: timeit crop_fast(a) # modified fireant's solution for n-dimensions
100 loops, best of 3: 5 ms per loop

In [48]: timeit crop_new(a) # modified Divakar's solution for n-dimensions
100 loops, best of 3: 1.91 ms per loop

旧解决方案

您可以使用np.nonzero 来获取数组的索引。然后这个数组的边界框完全包含在索引的最大值和最小值中。

def _get_slice_bbox(arr):
    nonzero = np.nonzero(arr)
    return [(min(a), max(a)+1) for a in nonzero]

def crop(arr):
    slice_bbox = _get_slice_bbox(arr)
    return arr[[slice(*a) for a in slice_bbox]]

例如

>>> img = np.array([[  0,   0,   0,   0,   0,   0,   0,   0],
                    [  0,   0,   0,   0,   0,   0,   0,   0],
                    [  0,   0, 175,   0,   0,   0,  71,   0],
                    [  0,   0,   0,  12,   8,  54,   0,   0],
                    [  0,   0,   0,   0, 255,   0,   0,   0],
                    [  0,   0,   0,   2,   0,   0,   0,   0],
                    [  0,   0,   0,   0,   0,   0,   0,   0],
                    [  0,   0,   0,   0,   0,   0,   0,   0]],  dtype='uint8')
>>> print crop(img)
[[175   0   0   0  71]
 [  0  12   8  54   0]
 [  0   0 255   0   0]
 [  0   2   0   0   0]]

【讨论】:

  • 注意,如果你想扩展它以支持彩色图像,你必须弄清楚颜色通道的位置然后适当地切片。截至目前,此解决方案不完全支持彩色图像。
  • 这正是我需要的,谢谢!事实上,我什至可以使用每像素 1 位的图像。
  • 我已经使用 opencv 方法使用更简单的函数更新了我的答案,这可能与基于 numpy 的方法一样快或更快。
【解决方案2】:

我们可以使用argmax 来获取开始、停止行和列的索引,如this post 中的一些详细讨论。我们还打算使用布尔数组/掩码进行有效处理。因此,使用这些工具/想法,我们将拥有一个矢量化解决方案,就像这样 -

def remove_black_border(a): 
    # Mask of non-zeros
    mask = a!=0 # Use a >tolerance for a tolerance defining black border

    # Mask of non-zero rows and columns
    mask_row = mask.any(1)
    mask_col = mask.any(0)

    # First, last indices among the non-zero rows
    sr0,sr1 = mask_row.argmax(), len(mask_row) - mask_row[::-1].argmax()

    # First, last indices among the non-zero columns
    sc0,sc1 = mask_col.argmax(), len(mask_col) - mask_col[::-1].argmax()

    # Finally slice along the rows & cols with the start and stop indices to get 
    # cropped image. Slicing helps for an efficient operation.
    return a[sr0:sr1, sc0:sc1]

示例运行 -

In [56]: a # Input image array
Out[56]: 
array([[  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0, 175,   0,   0,   0,  71],
       [  0,   0,   0,   0,  12,   8,  54,   0],
       [  0,   0,   0,   0,   0, 255,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   2,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0]])

In [57]: out = remove_black_border(a)

In [58]: out
Out[58]: 
array([[175,   0,   0,   0,  71],
       [  0,  12,   8,  54,   0],
       [  0,   0, 255,   0,   0],
       [  0,   0,   0,   0,   0],
       [  0,   2,   0,   0,   0]])

内存效率:

输出是输入数组的视图,因此不需要额外的内存或复制,这有助于提高内存效率。让我们验证视图部分 -

In [59]: np.shares_memory(a, out)
Out[59]: True

所有建议方法在更大图像上的时间安排

In [105]: # Setup for 1000x1000 2D image and 100 offsetted boundaries all across
     ...: np.random.seed(0)
     ...: a = np.zeros((1000,1000),dtype=np.uint8)
     ...: a[100:-100,100:-100] = np.random.randint(0,255,(800,800),dtype=np.uint8)

In [106]: %timeit crop_fast(a) # @fireant's soln
     ...: %timeit crop(a)      # @droooze's soln
     ...: %timeit remove_black_border(a) # from this post
100 loops, best of 3: 4.58 ms per loop
10 loops, best of 3: 127 ms per loop
10000 loops, best of 3: 155 µs per loop

【讨论】:

  • 感谢您指出一种更快的方法。我根据您的方法添加了一个新的解决方案来处理 n 维,它应该能够处理图像堆栈。
  • 当我最后一次看到答案时,fireant 的答案是最快的,我只是想将其标记为解决方案,但现在看来您的解决方案是最快的,并且 drooze 用解决方案更新了他的答案根据你的回答。现在我不再确定将哪个答案标记为解决方案:-/ 此外,我现在才意识到,如果有 an information about the coordinates of the extracted subarray,我的程序可能会受益。我不确定是否发布具有附加要求的新问题(提取子数组and return its coordinates inside the original array):-/
  • @Chupo_cro 我想在 - an information about the coordinates of the extracted subarray 部分发布一个新问题。
【解决方案3】:

更新 这种使用 opencv 函数的更简单方法实际上更快,并且可能比此处其他答案中提供的其他方法更快。

def crop_fastest(arr):
    return cv2.boundingRect(cv2.findNonZero(arr))

这将返回边界框的 x、y、宽度和高度。在我的台式电脑上使用我的旧代码 1000 loops, best of 3: 562 µs per loop 而对于这个新代码 10000 loops, best of 3: 179 µs per loop

又一次更新

正如 Chupo_cro 指出的那样,简单地调用 cv2.boundingRect(arr) 会返回相同的结果,这似乎是由于 the code in this function 在内部进行了转换。

上一个答案

可能有更快的方法。这个更简单的函数稍微快一些。

from scipy import ndimage
def crop_fast(arr):
    slice_x, slice_y = ndimage.find_objects(arr>0)[0]
    return arr[slice_x, slice_y]

比较 droooze 的代码和这个代码的速度,

arr = np.zeros(shape=(50000,6), dtype=np.uint8)
arr[2] = [9,8,0,0,1,1]
arr[1] = [0,3,0,0,1,1]

然后%timeit crop(arr) 在我的笔记本电脑上返回1000 loops, best of 3: 1.62 ms per loop%timeit crop_fast(arr) 返回1000 loops, best of 3: 979 µs per loop。也就是说,crop_fast() 花费了 crop() 大约 60% 的时间。

【讨论】:

  • 当我最后一次看到答案时,您的答案是最快的,我只是想将其标记为解决方案,但现在似乎 Divakar 的解决方案是最快的,并且 drooze 用解决方案更新了他的答案基于 Divakar 的回答。现在我不再确定将哪个答案标记为解决方案:-/
  • @Chupo_cro 我更新了我的答案,可能这是最快的代码,你可以试试自己的数据,看看是不是这样。
  • 我尝试只使用cv2.boundingRect(arr) 而不是cv2.boundingRect(cv2.findNonZero(arr)),这也给出了正确的结果——这怎么可能? findNonZero(arr) 返回非零像素的 list of coordinates 并将其替换为 arr 给出相同的结果:-O cv2.boundingRect() 期望一个点集,但即使传递数组而不是点集,一切正常。
  • @Chupo_cro 你是对的,我刚刚检查过,这让我感到惊讶。我试图快速挖掘代码,看看为什么会这样。似乎 this function 进行了转换。
  • 我刚刚检查了 OpenCV 2.4.12cv2.boundingRect(arr) 没有工作,这意味着稍后添加了自动转换。在旧版本的 OpenCV 中,只有 cv2.boundingRect(cv2.findNonZero(arr)) 运行良好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-04
  • 1970-01-01
  • 2015-07-30
相关资源
最近更新 更多