【问题标题】:Find bounding box contour with largest surface area excluding intersection areas查找具有最大表面积的边界框轮廓,不包括相交区域
【发布时间】:2021-11-07 18:33:17
【问题描述】:

我有一组来自对象检测系统的边界框。 它们的格式为:

[[x,y], [x,y], [x,y], [x,y]]

我想找到最大的边界框,它不与任何其他提供的框相交,也​​不在排除的框内。

我正在使用 python,但欢迎使用任何编程语言回复 :)

视觉示例

我如何尝试并未能解决这个问题。

方法一。

遍历每个点并找到 x 和 y 的最小值和最大值。

然后使用这些坐标裁剪成一个多边形。

问题是示例图像上的算法会删除图像的顶部,但没有必要,因为我们“错过”了左上角和右上框。

方法二。

尝试选择一次只裁剪一侧,因为通常在我的数据集中要排除的内容位于一侧。例如删除前 100 像素

所以我像以前一样计算了 x 和 y 的最小值和最大值。 然后计算每个可能切割的面积 - 左、右、上、下并选择一个面积最小的。

当图片左右两侧有框时,这种方法很快就失败了

【问题讨论】:

  • @EricDuminil,是的,这是相关的,我在问这个问题之前没有找到它。但要成为精确链接的问题是在凸多边形中寻找解决方案,我正在使用正交多边形。但这并不意味着我不能使用凸解决方案。我将通读链接的论文,如果我找到满意的解决方案,我会将其添加到问题中:) 感谢链接
  • 这不是简单地通过对所有框之间的交叉点进行强力成对检查来完成的吗?对于那些不是没有交叉点的排除框的框,请选择具有最大面积的框? stackoverflow.com/questions/40795709/…
  • 如果您需要实际生成可能的最大边界框,那么这是一个非常不同的问题。
  • @JasonChia 我可以进行估计,如果算法能在 90% 的时间里工作我会没事的

标签: python image opencv image-processing python-imaging-library


【解决方案1】:

考虑一个完整的重新调整(最初是整个图片)并拿走一个排除的盒子。您将获得 2x2x2x2=16 个可能的矩形细分,例如这个。

  ┌────────────────────────┐
  │                        │
  │                        │
  ├───────┬───────┬────────┤
  │       │  exc  │        │
  │       │ lude  │        │
  │       ├───────┴────────┤
  │       │                │
  │       │                │
  └───────┴────────────────┘

对于细分中的每个框,取出下一个排除框。 这样做N次,取最后一步最大的盒子。

【讨论】:

    【解决方案2】:

    这是一种寻找具有最大表面积的边界框轮廓的潜在解决方案。我们有两个要求:

    1. 最大边界框不与任何其他框相交
    2. 最大边界框不在另一个框内

    基本上我们可以将这两个要求改写为:

    1. 给定 C1 和 C2,确定 C1 和 C2 是否相交
    2. 给定 C1 和 C2,检查 C2 中是否存在来自 C1 的点

    为了解决#1,我们可以创建一个contour_intersect 函数,它使用AND 的按位运算和np.logical_and() 来检测交集。这个想法是为每个轮廓创建两个单独的掩码,然后对它们使用逻辑AND 操作。任何具有正值的点(1True)都将是交点。本质上,如果整个数组是False,那么轮廓之间就没有交集。但是如果有一个True,那么轮廓会在某个点接触并因此相交。

    对于#2,我们可以创建一个函数contour_inside 并使用cv2.pointPolygonTest() 来确定一个点是在轮廓的内部、外部还是在轮廓的边缘。该函数返回+1-10 以分别指示一个点是在轮廓内部、外部还是在轮廓上。我们找到 C1 的质心,然后检查该点是否在 C2 内。


    这是一个可视化场景的示例:

    输入具有三个轮廓的图像。这里没什么特别的,预期的答案是面积最大的轮廓。

    答案:

    Contour #0 is the largest
    

    接下来我们添加两个额外的轮廓。等高线#3 将代表交叉点场景,等高线#4 将代表内部等高线场景。

    答案:

    Contour #0 has failed test
    Contour #1 has failed test
    Contour #2 is the largest
    

    为了解决这个问题,我们找到轮廓,然后使用轮廓面积从大到小排序。接下来,我们将此轮廓与所有其他轮廓进行比较,并检查这两种情况。如果任一情况失败,我们将转储当前轮廓并移动到下一个最大轮廓。通过所有其他轮廓的两个测试的第一个轮廓是我们最大的边界框轮廓。通常,轮廓 #0 将是我们最大的,但它未通过交叉测试。然后我们移动到轮廓#1,但内部测试失败。因此,通过两个测试的最后一个轮廓是轮廓 #2。

    import cv2
    import numpy as np
    
    # Check if C1 and C2 intersect
    def contour_intersect(original_image, contour1, contour2):
        # Two separate contours trying to check intersection on
        contours = [contour1, contour2]
    
        # Create image filled with zeros the same size of original image
        blank = np.zeros(original_image.shape[0:2])
    
        # Copy each contour into its own image and fill it with '1'
        image1 = cv2.drawContours(blank.copy(), contours, 0, 1)
        image2 = cv2.drawContours(blank.copy(), contours, 1, 1)
    
        # Use the logical AND operation on the two images
        # Since the two images had bitwise and applied to it,
        # there should be a '1' or 'True' where there was intersection
        # and a '0' or 'False' where it didnt intersect
        intersection = np.logical_and(image1, image2)
    
        # Check if there was a '1' in the intersection
        return intersection.any()
    
    # Check if C1 is in C2
    def contour_inside(contour1, contour2):
        # Find centroid of C1
        M = cv2.moments(contour1)
        cx = int(M['m10']/M['m00'])
        cy = int(M['m01']/M['m00'])
    
        inside = cv2.pointPolygonTest(contour2, (cx, cy), False)
    
        if inside == 0 or inside == -1:
            return False
        elif inside == 1:
            return True
    
    # Load image, convert to grayscale, Otsu's threshold
    image = cv2.imread('1.png')
    original = image.copy()
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
    
    # Find contours, sort by contour area from largest to smallest
    cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = cnts[0] if len(cnts) == 2 else cnts[1]
    sorted_cnts = sorted(cnts, key=lambda x: cv2.contourArea(x), reverse=True)
    
    # "Intersection" and "inside" contours
    # Add both contours to test 
    # --------------------------------
    intersect_contour = np.array([[[230, 93]], [[230, 187]], [[326, 187]], [[326, 93]]])
    sorted_cnts.append(intersect_contour)
    cv2.drawContours(original, [intersect_contour], -1, (36,255,12), 3)
    
    inside_contour = np.array([[[380, 32]], [[380, 229]], [[740, 229]], [[740, 32]]])
    sorted_cnts.append(inside_contour)
    cv2.drawContours(original, [inside_contour], -1, (36,255,12), 3)
    # --------------------------------
    
    # Find centroid for each contour and label contour number
    for count, c in enumerate(sorted_cnts):
        M = cv2.moments(c)
        cx = int(M['m10']/M['m00'])
        cy = int(M['m01']/M['m00'])
        cv2.putText(original, str(count), (cx-5, cy+5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (246,255,12), 3)
    
    # Find largest bounding box contour
    largest_contour_name = ""
    largest_contour = ""
    contours_length = len(sorted_cnts)
    for i1 in range(contours_length):
        found = True
        for i2 in range(i1 + 1, contours_length):
            c1 = sorted_cnts[i1]
            c2 = sorted_cnts[i2]
            
            # Test intersection and "inside" contour
            if contour_intersect(original, c1, c2) or contour_inside(c1, c2):
                print('Contour #{} has failed test'.format(i1))
                found = False
                continue
        if found:
            largest_contour_name = i1
            largest_contour = sorted_cnts[i1]
            break
    print('Contour #{} is the largest'.format(largest_contour_name))
    print(largest_contour)
    
    # Display
    cv2.imshow('thresh', thresh)
    cv2.imshow('image', image)
    cv2.imshow('original', original)
    cv2.waitKey()
    

    注意:假设您有一个来自cv2.findContours() 的等高线数组,格式如下例:

    cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = cnts[0] if len(cnts) == 2 else cnts[1]
    sorted_cnts = sorted(cnts, key=lambda x: cv2.contourArea(x), reverse=True)
    for c in sorted_cnts:
        print(c)
        print(type(c))
        x,y,w,h = cv2.boundingRect(c)
        print((x,y,w,h))
    

    输出

    [[[230  93]]
    
     [[230 187]]
    
     [[326 187]]
    
     [[326  93]]]
    <class 'numpy.ndarray'>
    (230, 93, 97, 95)
    

    性能说明:交点检查功能在性能方面受到影响,因为它会创建输入图像的三个副本来绘制轮廓,并且在执行时间较长时可能会更慢轮廓或更大的输入图像尺寸。我把这个优化步骤留给你!

    【讨论】:

    • 这两个要求不一样吗?
    【解决方案3】:

    假设:您想要数组中符合您规则的最大框,并且它不是符合规则的最大 NEW 边界框。

    这是伪代码,你还是要填空

    int largestBoxIndex = -1;
    int largestBoxArea = -1;
    
    for (i=0; i<allBoxes[].length; i++)
    {
        box CurrentBox =  allBoxes[i];
        bool isComply = false;
        for (j=0; j<allBoxes[].length; j++)
        {
            isComply = false;
    
            if(i==j) break;
    
            ComparedBox = allBoxes[j]
    
            if (isIntersected(CurrentBox, ComparedBox)) break;
    
            if (isInside(CurrentBox, ComparedBox)) break;
            
            isComply = true;    
        }
    
        if(isComply)
            if(Area(allBoxes[i]) > largestBoxArea)
            {
                largestBoxArea = Area(allBoxes[i]):
                largestBoxIndex =i;
            }
    
    }
    
    if(largestBoxIndex != -1)
        largestBoxIndex;//this is the largest box
    

    【讨论】:

      【解决方案4】:

      问题的简单数学解决方案

      假设给你 5 个矩形,如下所示:

      rects = [[100, 100, 200, 200],
               [200, 200, 200, 200],
               [200, 500, 200, 200],
               [350,  50, 150, 200],
               [500, 400, 200, 300]]
      

      注意这些矩形的格式是:[x, y, width, height] 其中,(x, y) 是矩形左上角的坐标,width & height 分别是矩形的宽度和高度。您必须先以这种格式转换您的坐标。

      这 5 个中有 3 个是相交的。

      现在我们要做的是一个接一个地遍历这些矩形,并且对于每个矩形,一个接一个地找到这个矩形与其他矩形的交点。如果发现任何矩形与任何其他矩形相交,那么我们将两个矩形的flag 值设置为0。如果发现一个矩形不与任何其他矩形相交,则其flag 值将设置为1。 (默认标志值为-1)。最后,我们将在标志值为 1 的矩形中找到面积最大的矩形。

      我们看一下求两个矩形相交区域的代码:

      # Rect : [x, y, w, h]
      def Intersection(Rect1, Rect2):
          x = max(Rect1[0], Rect2[0])
          y = max(Rect1[1], Rect2[1])
          w = min(Rect1[0] + Rect1[2], Rect2[0] + Rect2[2]) - x
          h = min(Rect1[1] + Rect1[3], Rect2[1] + Rect2[3]) - y
          
          if w < 0 or h < 0: 
              return None
          return [x, y, w, h]
      

      如果这些矩形之间没有相交区域,则此函数将返回None,否则它将返回相交矩形的坐标(当前问题忽略此值。这可能对其他问题有所帮助)。

      现在,让我们看看算法。

      n = len(rects)
      
      # -1 : Not determined
      #  0 : Intersects with some
      #  1 : No intersection
      flag = [-1]*n
      
      for i in range(n):
          if flag[i] == 0:
              continue
      
          isIntersecting = False
          for j in range(n):
              if i == j or flag[j] == 1:
                  continue
              
              Int_Rect = Intersection(rects[i], rects[j])
              if Int_Rect is not None:
                  isIntersecting = True
                  flag[j] = 0
                  flag[i] = 0
                  break
          
          if isIntersecting == False:
              flag[i] = 1
      
      # Finding the maximum area rectangle without any intersection.
      maxRect = None
      maxArea = -1
      
      for i in range(n):
          if flag[i] == 1:
              if rects[i][2] * rects[i][3] > maxArea:
                  maxRect = rects[i]
                  maxArea = rects[i][2] * rects[i][3]
      
      print(maxRect)
      

      注意:将“排除区域”矩形坐标添加到rects 列表中,并将它们的flag 值指定为0,以避免它们被选为最大区域矩形。

      此解决方案不涉及任何图像,因此除非经过优化,否则它将是最快的算法。

      【讨论】:

        【解决方案5】:

        您可以使用cv2.boundingRect()方法获取每个边界框的x, y, w, h,并使用每个边界框的x, y, w, h,您可以使用条件x2 + w2 &gt; x1 &gt; x2 - w1 and y2 + h2 &gt; y1 &gt; y2 - h1来检查任何两个边界框是否相交或彼此之间:

        import cv2
        import numpy as np
        
        def intersect(b1, b2):
            x1, y1, w1, h1 = b1
            x2, y2, w2, h2 = b2
            return x2 + w2 > x1 > x2 - w1 and y2 + h2 > y1 > y2 - h1
        
        # Here I am generating a random array of 10 boxes in the format [[x,y], [x,y], [x,y], [x,y]]
        np.random.seed(55)
        boxes = np.random.randint(10, 150, (10, 4, 2)) + np.random.randint(0, 300, (10, 1, 2))
        
        bounds = [cv2.boundingRect(box) for box in boxes]
        valids = [b1 for b1 in bounds if not any(intersect(b1, b2) for b2 in bounds if b1 != b2)]
        if valids:
            x, y, w, h = max(valids, key=lambda b: b[2] * b[3])
            print(f"x: {x} y: {y} w: {w} h: {h}")
        else:
            print("All boxes intersect.")
        

        输出:

        x: 75 y: 251 w: 62 h: 115
        

        对于可视化:

        import cv2
        import numpy as np
        
        def intersect(b1, b2):
            x1, y1, w1, h1 = b1
            x2, y2, w2, h2 = b2
            return x2 + w2 > x1 > x2 - w1 and y2 + h2 > y1 > y2 - h1
        
        np.random.seed(55)
        boxes = np.random.randint(10, 150, (10, 4, 2)) + np.random.randint(0, 300, (10, 1, 2))
        
        bounds = [cv2.boundingRect(box) for box in boxes]
        valids = [b1 for b1 in bounds if not any(intersect(b1, b2) for b2 in bounds if b1 != b2)]
        
        img = np.zeros((500, 500), "uint8")
        for x, y, w, h in bounds:
            cv2.rectangle(img, (x, y), (x + w, y + h), 255, 1)
        
        if valids:
            x, y, w, h = max(valids, key=lambda b: b[2] * b[3])
            cv2.rectangle(img, (x, y), (x + w, y + h), 128, -1)
        
        cv2.imshow("IMAGE", img)
        cv2.waitKey(0)
        

        输出:

        【讨论】:

          【解决方案6】:

          Find the biggest square in numpy array

          也许这会有所帮助?如果您知道整个区域的大小,则可以计算 numpy 数组中最大的框。如果将所有给定的框设置为 1,将整个区域设置为 0,则需要找到唯一的最大区域,而不是 1。

          【讨论】:

            【解决方案7】:

            换句话说:

            1. 共享点的矩形被排除在外
            2. 在剩余的矩形中,取最大的

            无需轮廓、质心、边界框、遮罩或重绘像素!

            如前所述,在提供的情况下,矩形坐标包含重复项。在这里,我们使用一个类来存储矩形的外部边界。来自@samgak 的this answer 的分离轴定理用于intersects() 方法。

            from __future__ import annotations # optional
            from dataclasses import dataclass # optional ?
            
            @dataclass
            class Rectangle:
                left: int
                top: int
                right: int
                bottom: int
                def __repr__(self):
                    """String representation of the rectangle's coordinates."""
                    return f"⟔ {self.left},{self.top} ⟓ {self.right},{self.bottom}"
                def intersects(self, other: Rectangle):
                    """Whether this Rectangle shares points with another Rectangle."""
                    h = self.right < other.left or self.left > other.right
                    v = self.bottom < other.top or self.top > other.bottom
                    return not h or not v
                def size(self):
                    """An indicator of the Rectangle's size, equal to half the perimeter."""
                    return self.right - self.left + self.bottom - self.top
            
            main = Rectangle(100, 100, 325, 325)
            
            others = {
                0: Rectangle(100, 100, 400, 400),
                1: Rectangle(200, 200, 300, 300),
                2: Rectangle(200, 300, 300, 500),
                3: Rectangle(300, 300, 500, 500),
                4: Rectangle(500, 500, 600, 600),
                5: Rectangle(350, 350, 600, 600),
            }
            
            for i, r in others.items():
                print(i, main.intersects(r), r.size())
            

            简单地说,如果另一个矩形完全靠左或靠右,h 就是TruevTrue,如果它位于顶部或底部。 intersects() 方法返回 True 如果矩形共享点(甚至是一个角)。

            输出:

            0 True 600
            1 True 200
            2 True 300
            3 True 400
            4 False 500
            5 False 200
            

            然后找到最大的就很简单了:

            valid = {r.size():i for i, r in others.items() if not main.intersects(r)}
            print('Largest:', valid[max(valid)], 'with size', max(valid))
            

            输出:

            Largest: 4 with size 500
            

            此答案假定所有矩形均使用 left &lt; righttop &lt; bottom

            以下函数将提供的矩形坐标转换为上面Rectangle 类使用的类型。这假定订单是[[l, t], [r, t], [r, b], [l, b]](路径)。

            def trim(coordinates):
                """Remove redundant coordinates in a path describing a rectangle."""
                return coordinates[0][0], coordinates[1][1], coordinates[2][0], coordinates[3][1]
            

            最后,我们希望对所有矩形都执行此操作,而不仅仅是“主”矩形。我们可以简单地让每个矩形依次成为主要矩形。在 list 等可迭代对象上使用 itertools.combinations()

            itertools.combinations(rectangles, 2)
            

            这将确保我们不会多次比较两个矩形。

            【讨论】:

              【解决方案8】:

              这是一个 O(n^2) 的解决方案。 find_maxbox 获取矩形数组并将它们转换为 Box 对象,然后比较每对框以消除无效矩形。此解决方案假定矩形的边平行于 X-Y 轴。

              class Box():
                  def __init__(self, coordinates):
                      self.coordinates = tuple(sorted(coordinates))
                      self.original = coordinates
                      self.height = abs(self.coordinates[0][1] - self.coordinates[3][1])
                      self.width = abs(self.coordinates[0][0] - self.coordinates[3][0])
                      self.excluded = False
              
                  def __eq__(self, b2):
                      return self.coordinates == b2.coordinates
              
                  def get_area(self):
                      return self.height * self.width
              
                  def bounding_box(self, b2):
                      maxX, maxY = map(max, zip(*self.coordinates, *b2.coordinates))
                      minX, minY = map(min, zip(*self.coordinates, *b2.coordinates))
                      return Box([(minX, minY), (maxX, minY), (minX, maxY), (maxX, maxY)])
              
                  def intersects(self, b2):
                      box = self.bounding_box(b2)
                      if box.height < self.height + b2.height and box.width < self.width + b2.width:
                          return True
                      else: return False
              
                  def encloses(self, b2):
                      return self == self.bounding_box(b2)
              
                  def exclude(self):
                      self.excluded = True
              
                  def is_excluded(self):
                      return self.excluded
              
                  def __str__(self):
                      return str(self.original)
              
                  def __repr__(self):
                      return str(self.original)
              
              # Pass array of rectangles as argument.
              def find_maxbox(boxes):
                  boxes = sorted(map(Box, boxes), key=Box.get_area, reverse=True)
                  _boxes = []
                  _boxes.append((boxes[0], boxes[0]))
                  for b1 in boxes[1:]:
                      b2, bb2 = _boxes[-1]
                      bbox = b1.bounding_box(bb2)
                      if not b1.intersects(bb2):
                          _boxes.append((b1, bbox))
                          continue
                      for (b2, bb2) in reversed(_boxes):
                          if not b1.intersects(bb2):
                              break
                          if b1.intersects(b2):
                              if b2.encloses(b1):
                                  b1.exclude()
                                  break
                              b1.exclude()
                              b2.exclude()
                      _boxes.append((b1, bbox))
              
                  for box in boxes:
                      if box.is_excluded():
                          continue
                      else: return box.original
              
                  return None
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2014-06-17
                • 1970-01-01
                • 1970-01-01
                • 2015-06-24
                • 1970-01-01
                • 2020-06-17
                • 1970-01-01
                相关资源
                最近更新 更多