以2D阵列填充边界框

时间:2019-02-11 20:42:25

标签: python arrays numpy bounding-box numpy-ndarray

我有一个二维numpy数组,看起来像

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.]]) `

我想在上面显示的1上方创建边框(如蒙版)。例如,它应该看起来像这样

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.]])

我如何轻松做到这一点?如果还存在其他编号(例如2,3等),但我想忽略它们,并且组大多为2,我该怎么办。

4 个答案:

答案 0 :(得分:11)

这是解决此问题的一种方法。其背后的总体思路是使用迭代解决方案,该解决方案在每个步骤中采用矩阵的 2D卷积和一组过滤器,以便检测并填充落在{{3} }。

这将通过一个示例更加清楚。假设我们有以下ndarray

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

此方法背后的想法是检测具有至少两个 orthogal 邻居(相距1个细胞)且彼此之间成 90°角的细胞其中包含非零值。

通过迭代查找这些单元格并用它们填充它们,我们将获得预期的输出。因此,对于此示例,第一次迭代后的输出将是:

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

在以下迭代中:

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

如何检测这些细胞?

一种方法是将ndarray的2D卷积与一组专门设计用于检测目标细胞的预定义滤镜一起使用。为此,我们可以使用 scipy's Bounding Box

2D卷积本质上是通过将2D滤镜移过ndarray并在每个步骤计算逐元素相乘之和来进行的。使用以下动画(convolve2D)可能会更直观:

image from


因此有必要提出一些过滤器以检测感兴趣的细胞。一种方法可能是:

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

乍一看,这个过滤器可以执行此任务,因为它将检测到周围的邻居。但是,此过滤器还将考虑相隔两个单元格的样本,因此,例如,它将在过滤器的第一行和最后一行中求和,并且如前所述,我们想查找成一定角度的邻居彼此成90°因此,我们可以做的是应用一系列考虑这种情况所有可能性的过滤器:

要应用的二维过滤器

[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]

通过应用这些过滤器中的每一个,我们可以检测到哪些小区具有至少两个满足上述要求的邻居,并用它们填充它们。


通用解决方案

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

示例

让我们看一下建议的示例的结果:

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]])

对于另一个示例:

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]])

答案 1 :(得分:5)

虽然先前的回复非常好,但是您可以使用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用定义邻居的3x3-“ ones”矩阵进行连接的组件标记。 find_objects然后为每个组件确定边界框,然后可以使用该边界框将其中的所有内容都设置为1。

答案 2 :(得分:4)

有一个解决方案,但它有点黑,我不会为您编程。

OpenCV-图像处理库,具有用于查找矩形轮廓->直线或旋转的算法。您可能想要做的是将阵列转换为2D灰度图像,找到轮廓并将轮廓内的1s写入。

检查此图片-来自Opencv DOC- 7.a -https://docs.opencv.org/3.4/dd/d49/tutorial_py_contour_features.html

enter image description here

您将对绿线内的所有内容感兴趣。


说实话,在我看来,这似乎比为边界框编程算法容易得多

注意

当然,您确实不需要做图像工作,但是我认为使用opencv的算法对边界框(数量)就足够了

答案 3 :(得分:1)

这是一个有趣的问题。 2D卷积是一种自然的方法。但是,如果输入矩阵稀疏(如您的示例中所示),那么这可能会很昂贵。对于稀疏矩阵,另一种方法是使用聚类算法。这仅从输入框a(示例中的数组)中提取非零像素,并运行分层聚类。聚类基于特殊的距离矩阵(元组)。如果框在任一方向上最多相隔1个像素,则会发生合并。您还可以对初始化步骤中所需的任何数字应用过滤器(例如,仅对a [row,col] == 1进行操作,并跳过任何其他数字,或任何您希望的数字。

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

这将返回由角点(左上角和右下角)定义的框的列表。 x是行号,y是列号。初始化遍历整个矩阵。但是之后,我们只处理很小的点子集。通过更改dist函数,您可以自定义框定义(重叠,不重叠等)。可以进一步优化性能(例如,如果i或j大于for循环中的框的长度,则比单纯地从merge函数返回并继续操作来中断)。