从代表图像的数组中排除周围零的最快方法是什么?

Chu*_*cro 9 python arrays numpy image-processing

我有一个包含灰度图像的2D数组.png,如下所示:

import cv2

img = cv2.imread("./images/test.png")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
Run Code Online (Sandbox Code Playgroud)

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

例如,如果输入是:

  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
Run Code Online (Sandbox Code Playgroud)

然后输出应该是:

175   0   0   0  71
  0  12   8  54   0
  0   0 255   0   0
  0   2   0   0   0
Run Code Online (Sandbox Code Playgroud)

我可以向前遍历行以找到第一个非零行,然后向后遍历行以找到记住索引的最后一个非零行 - 然后对列重复相同的行,然后使用该数据提取子数组但我是确定有更合适的方法可以做同样的事情,甚至可能有为此目的设计的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
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

dro*_*oze 11

注意,不是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]]
Run Code Online (Sandbox Code Playgroud)

速度测试:

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
Run Code Online (Sandbox Code Playgroud)

老解决方案

您可以使用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]]
Run Code Online (Sandbox Code Playgroud)

例如

>>> 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]]
Run Code Online (Sandbox Code Playgroud)


Div*_*kar 7

我们可以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]
Run Code Online (Sandbox Code Playgroud)

样品运行 -

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]])
Run Code Online (Sandbox Code Playgroud)

记忆效率:

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

In [59]: np.shares_memory(a, out)
Out[59]: True
Run Code Online (Sandbox Code Playgroud)

时间安排所有提出的方法更大的图像

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
Run Code Online (Sandbox Code Playgroud)


fir*_*ant 5

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

def crop_fastest(arr):
    return cv2.boundingRect(cv2.findNonZero(arr))
Run Code Online (Sandbox Code Playgroud)

这将返回边界框的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)返回相同的结果,这似乎是由于此函数的代码在内部进行转换.

以前的答案

可能有更快的方法.这个更简单的功能稍微快一些.

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

为了比较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]
Run Code Online (Sandbox Code Playgroud)

然后%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()大约需要60%的时间crop().