Fill Bounding Boxes in 2D array

徘徊边缘 提交于 2019-12-04 17:04:10

问题


I have a 2D numpy array which looks like

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., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 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.],
       [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., 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., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.]]) `

I want to create bounding box like masks over the 1s shown above. For example it should look like this

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., 0., 0., 0.],
       [0., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 1., 1., 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., 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., 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., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0.]])

How can I do it it easily? Also how do I do it if other no.s like 2,3 etc exist but I want to ignore them and the groups are mostly 2.


回答1:


Here's one approach to solve this problem. The general idea behind it is to use an iterative solution which takes the 2D convolution of the matrix and a set of filters at each step in order to detect and fill the cells that fall in a Bounding Box.

This will be much clearer with an example. Say we have the following ndarray:

a = np.array([[0,0,0,0],
              [0,0,0,0],
              [1,0,0,0],
              [1,1,1,0]])

The idea behind this method is to detect cells which have at least two orthogal neighbours (at a distance of 1 cell) which are at an angle of 90° between each other with non-zero values in them.

By iteratively finding these cells and filling them with ones, we'll obtain the expected output. So for this example, the output after the first iteration will be:

a = np.array([[0,0,0,0],
              [0,0,0,0],
              [1,1,0,0],
              [1,1,1,0]])

And on the following iteration:

a = np.array([[0,0,0,0],
              [0,0,0,0],
              [1,1,1,0],
              [1,1,1,0]])

How can these cells be detected?

One way is by taking the 2D convolution of the ndarray with a set of predefined filters, specifically designed to detect the cells of interest. For that purpose we can use scipy's convolve2D.

The 2D convolution is essentially taken by shifting a 2D filter through a ndarray and computing at each step the sum of the element-wise multiplication. It may be more intuitive with the following animation (image from):


So it will be necessary to come up with some filter in order to detect the cells of interest. One approach could be:

array([[0, 1, 0],
       [1, 0, 1],
       [0, 1, 0]])

At first glance this filter sould do the task, given that it will detect the sorrounding neighbours. However, this filter would also take into account samples which are two cells away, so, for instance it would add up the values in the first and last row in the filter, and as mentioned previously we want to find neighbours that are at an angle of 90° of each other. So what we could do is apply a sequence of filters contemplating all possibilities of such case:

2-dimensional filters to apply

[0, 1, 0]   [0, 1, 0]   [0, 0, 0]   [0, 0, 0]
[0, 0, 1] , [1, 0, 0] , [1, 0, 0] , [0, 0, 1]
[0, 0, 0]   [0, 0, 0]   [0, 1, 0]   [0, 1, 0]

By applying each of these filters we could detect which cells have at least two neighbours with the mentioned requirements, and fill them with ones.


General Solution

def fill_bounding_boxes(a):
    '''
    Detects contiguous non-zero values in a 2D array
    and fills with ones all missing values in the 
    minimal rectangular boundaries that enclose all 
    non-zero entries, or "Minimal Bounding Boxes"
    ----
    a: np.array
       2D array. All values > 0 are considered to define
       the bounding boxes
    ----       
    Returns:
       2D array with missing values filled 

    '''
    import numpy as np
    from scipy.signal import convolve2d
    # Copy of the original array so it remains unmodified
    x = np.copy(a).clip(0,1)
    # Indicator. Set to false when no additional
    # changes in x are found
    is_diff = True
    # Filter to be used for the 2D convolution
    # The other filters are obtained by rotating this one
    f = np.array([[0,1,0], [0,0,1], [0,0,0]])
    # Runs while indicator is True
    while is_diff:
        x_ = np.copy(x)
        # Convolution between x and the filters
        # Only values with sums > 1 are kept, as it will mean
        # that they had minimum 2 non-zero neighbours
        # All filters are applied by rotating the initial filter
        x += sum((convolve2d(x, np.rot90(f, i), mode='same') > 1) 
                 for i in range(4))
        # Clip values between 0 and 1
        x = x.clip(0,1)
        # Set indicator to false if matrix x is unmodified
        if (x == x_).all():
            is_diff = False
    return x

Examples

Lets have a look at the result with the proposed example:

print(a)
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, 0, 0, 0],
       [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 1, 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],
       [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, 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, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]])

fill_bounding_boxes(a)
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, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 1, 1, 1, 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, 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, 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, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0]])

And for this other example:

print(a)
array([[0, 0, 0, 0, 1, 0],
       [0, 0, 0, 0, 1, 1],
       [1, 0, 0, 0, 0, 0],
       [1, 1, 1, 0, 0, 0],
       [1, 0, 1, 0, 0, 0],
       [0, 0, 0, 0, 0, 0],
       [0, 0, 1, 0, 0, 0],
       [0, 1, 1, 0, 0, 1],
       [0, 0, 0, 0, 1, 0]])

fill_bounding_boxes(a)
array([[0, 0, 0, 0, 1, 1],
       [0, 0, 0, 0, 1, 1],
       [1, 1, 1, 0, 0, 0],
       [1, 1, 1, 0, 0, 0],
       [1, 1, 1, 0, 0, 0],
       [0, 0, 0, 0, 0, 0],
       [0, 1, 1, 0, 0, 0],
       [0, 1, 1, 0, 1, 1],
       [0, 0, 0, 0, 1, 1]])



回答2:


While the previous responses are perfectly fine, here's how you could do it with scipy.ndimage:

import numpy as np
from scipy import ndimage

def fill_bboxes(x):
    x_components, _ = ndimage.measurements.label(x, np.ones((3, 3)))
    bboxes = ndimage.measurements.find_objects(x_components)

    for bbox in bboxes:
        x[bbox] = 1

    return x

ndimage.measurements.label does a connected component labelling with the 3x3-"ones" matrix defining the neighbourhood. find_objects then determines the bounding box for each component, which you can then use to set everything within to 1.




回答3:


There is one solution, but its a little bit hacky and I will not program it for you.

OpenCV - Image processing library, has a algorithm for finding Rectangular contour -> Straight or Rotated. What you may want to do is to transform your array into 2D grayscale image, find contours and write inside the contours your 1s.

Check this image - it is from Opencv DOC - 7.a - https://docs.opencv.org/3.4/dd/d49/tutorial_py_contour_features.html

You would be interested in everything that is inside green lines.


To be honest, I think seems to me much easier than programming some algorithm for bounding boxes

Note

Of course you dont really need to do the image stuff, but I think it is enough to use opencv's algorithm for the bounding boxes(countours)




回答4:


This is an interesting problem. A 2D convolution is a natural approach. However, if the input matrix is sparse (as it appears in your example), this can be costly. For sparse matrix, another approach is to use a clustering algorithm. This extracts only the non-zero pixels from the input box a (the array in your example), and runs a hierarchical clustering. The clustering is based on a special distance matrix (a tuple). Merging happens if boxes are separated by a max of 1 pixel in either direction. You can also apply filter for any numbers you need in the initialization step (say only do for a[row, col]==1 and skip any other numbers, or whatever you wish.

from collections import namedtuple 

Point = namedtuple("Point",["x","y"]) # a pixel on the matrix
Box = namedtuple("Box",["tl","br"]) # a box defined by top-lef/bottom-right

def initialize(a):
    """ create a separate bounding box at each non-zero pixel. """
    boxes = []
    rows, cols = a.shape
    for row in range(rows):
        for col in range(cols):
            if a[row, col] != 0:
                boxes.append(Box(Point(row, col),Point(row, col)))
    return boxes

def dist(box1, box2):
    """ dist between boxes is from top-left to bottom-right, or reverse. """
    x = min(abs(box1.br.x - box2.tl.x), abs(box1.tl.x - box2.br.x))
    y = min(abs(box1.br.y - box2.tl.y), abs(box1.tl.y - box2.br.y))
    return x, y

def merge(boxes, i, j):
    """ pop the boxes at the indices, merge and put back at the end. """
    if i == j:
        return

    if i >= len(boxes) or j >= len(boxes):
        return

    ii = min(i, j)
    jj = max(i, j)
    box_i = boxes[ii]
    box_j = boxes[jj]
    x, y = dist(box_i, box_j)

    if x < 2 or y < 2:
        tl = Point(min(box_i.tl.x, box_j.tl.x),min(box_i.tl.y, box_j.tl.y))
        br = Point(max(box_i.br.x, box_j.br.x),max(box_i.br.y, box_j.br.y))
        del boxes[ii]
        del boxes[jj-1]
        boxes.append(Box(tl, br))


def cluster(a, max_iter=100):
    """ 
        initialize the cluster. then loop through the length and merge 
        boxes. break if `max_iter` reached or no change in length.
    """
    boxes = initialize(a)
    n = len(boxes)
    k = 0

    while k < max_iter:
        for i in range(n):
            for j in range(n):
                merge(boxes, i, j)
        if n == len(boxes):
            break
        n = len(boxes)
        k = k+1

    return boxes

cluster(a)
# output: [Box(tl=Point(x=2, y=2), br=Point(x=5, y=4)),Box(tl=Point(x=11, y=9), br=Point(x=14, y=11))]

# performance 275 µs ± 887 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# compares to 637 µs ± 9.36 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) for 
#the method based on 2D convolution

This returns a list of boxes defined by the corner points (top-left and bottom-right). Here x is the row number and y is the column numbers. The initialization loops through the entire matrix. But after that we only process a very small subset of points. By changing the dist function, you can customize the box definition (overlapping, non-overlapping etc). Performance can further be optimized (for e.g. breaking if i or j greater the length of boxes within the for loops, than simply returning from the merge function and continue).



来源:https://stackoverflow.com/questions/54638817/fill-bounding-boxes-in-2d-array

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!