问题
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