【问题标题】:How to detect rectangle in a rectangle?如何检测矩形中的矩形?
【发布时间】:2018-03-29 23:06:38
【问题描述】:

我有一个矩形列表的左上角和右下角的坐标,例如 (a,b) 和 (c,d)。我想检测并删除矩形内的矩形。重叠的矩形可以保留。

我有一个包含 10,000 个矩形的数据集,我想要一种有效的方法来解决这个问题。

目前我正在这样做,

import pandas

data = pd.read_csv('dataset.csv')

l = list(range(len(data)-1))

for i in range(len(data)):
    length = len(l)
    if i >= length:
        break
    for j in range(i+1, length):
        if j >= len(l):
           break
        if (data.iloc[l[i]]['a'] >= data.iloc[l[j]]['a']) and (data.iloc[l[i]]['b'] <= data.iloc[l[j]]['b']) and (data.iloc[l[i]]['c'] <= data.iloc[l[j]]['c']) and (data.iloc[l[i]]['d'] >= data.iloc[l[j]]['d']):
           l.pop(j)

我在按照矩形面积的降序对数据集进行排序后实现了这个算法,因为面积较大的矩形不适合面积较小的矩形。在这里,在检测到它是否在另一个矩形内之后,我从列表 l 中弹出矩形的索引。每次弹出一个元素时,它都会减少迭代次数。

这需要几个小时才能解决,我需要一种有效的方法来解决它,即使是十万个样本。

请帮忙!

【问题讨论】:

  • 你试过查看 shapely 库吗?
  • 没有。我去看看。
  • 我希望每次进行 8 次字典/列表/字典查找会很昂贵,尤其是当您可以少花钱的时候。
  • 是的!我通过列出 index et al 让事情变得非常复杂。可以添加标志。但是对于 10,000 个样本,最坏情况为 O(n^2) 的简单检测算法(相同的检测逻辑)仍需要大约 2 小时。
  • 您的问题缺少关键信息。相对于一组矩形所覆盖的区域,矩形的大小分布是多少。矩形是“相对较小”(然后您可以使用二进制空间分区之类的东西),还是它们是完全随机的,有时相对于整个区域来说非常大?你能提供一段代码来生成大小分布与你的实际数据集相似的矩形吗?

标签: python algorithm performance computational-geometry rectangles


【解决方案1】:

这里有一个分而治之的小算法,你可以试试。

我假设只要你能快速列举出每一对 碰撞矩形,您还可以检查一个是否完全 在恒定时间内包含在另一个中。

所以,我们只需要找到碰撞的矩形。

首先,将其概括如下:假设您有两个集合 矩形AB,并且您只想找到对 (a, b) 使得矩形a 来自Ab 来自B, 和ab 相交。

首先,想法。考虑以下示例 两组AB 的矩形部分 用水平线分隔L

      +----+                    +-----+
      | A1 |                    |  B1 |
      |    |     +-----+        +-----+
      +----+     |  A2 |
                 +-----+     +-----+
                             |  A3 |
_____________________________|_____|________ L
                             |     |
         +-------------------+##+  |
         |                   |##|  |
         |     B2            +##|--+    
         |                      |
         +----------------------+

L 行将集合 AB 细分为三个子集:

A above L: {A1, A2}         (short: A>L)
A intersects L: {A3}        (short: A=L)
A below L: {}               (short: A<L)


B above L: {B1}             (B>L)
B intersects L: {}          (B=L)
B below L: {B2}             (B<L)

请注意,只有以下组中的矩形可以碰撞:

         A<L    A=L    A>L
B<L       y      y      N
B=L       y      y      y
B>L       N      y      y

也就是说,如果我们想找到AB 之间的所有冲突,一旦我们 找到了合适的线路L,我们可以忽略之间的碰撞 A&lt;LB&gt;LA&gt;LB&lt;L。因此,我们得到以下分治算法:当AB 不为空时,找到一条(大致)最大化消除冲突检查数量的合适行,将AB 细分为三个每个组,递归进行七个子组冲突,忽略两个子组组合。

假设如果矩形是“小”,并且组A=LB=L大部分是空的,这将(大致)在每一步中将集合的大小减少一半,因此我们获得了一个算法平均运行在O(n*log(n))而不是O(n*n)

一旦有了任意AB 的一般情况,就可以获取整组矩形R 并使用A = R; B = R 运行算法。

这是 Python 的粗略草图:

def isSubinterval(aStart, aEnd, bStart, bEnd):
  return aStart >= bStart and aEnd <= bEnd

def intersects(aStart, aEnd, bStart, bEnd):
  return not (aEnd < bStart or aStart > bEnd)

class Rectangle:
  def __init__(self, l, r, b, t):
    self.left = l
    self.right = r
    self.bottom = b
    self.top = t

  def isSubrectangle(self, other):
    return (
      isSubinterval(self.left, self.right, other.left, other.right) and
      isSubinterval(self.bottom, self.top, other.bottom, other.top)
    )

  def intersects(self, other):
    return (
      intersects(self.left, self.right, other.left, other.right) and
      intersects(self.bottom, self.top, other.bottom, other.top)
    )

  def __repr__(self):
    return ("[%f,%f]x[%f,%f]" % (self.left, self.right, self.bottom, self.top))

def boundingBox(rects):
  infty = float('inf')
  b = infty
  t = - infty
  l = infty
  r = - infty
  for rect in rects:
    b = min(b, rect.bottom)
    l = min(l, rect.left)
    r = max(r, rect.right)
    t = max(t, rect.top)
  return Rectangle(l, r, b, t)

class DividingLine:
  def __init__(self, isHorizontal, position):
    self.isHorizontal = isHorizontal
    self.position = position

  def isAbove(self, rectangle):
    if self.isHorizontal:
      return rectangle.bottom > self.position
    else:
      return rectangle.left > self.position

  def isBelow(self, rectangle):
    if self.isHorizontal:
      return rectangle.top < self.position
    else:
      return rectangle.right < self.position

def enumeratePossibleLines(boundingBox):
  NUM_TRIED_LINES = 5
  for i in range(1, NUM_TRIED_LINES + 1):
    w = boundingBox.right - boundingBox.left
    yield DividingLine(False, boundingBox.left + w / float(NUM_TRIED_LINES + 1) * i)
    h = boundingBox.top - boundingBox.bottom
    yield DividingLine(True, boundingBox.bottom + h / float(NUM_TRIED_LINES + 1) * i)

def findGoodDividingLine(rects_1, rects_2):
  bb = boundingBox(rects_1 + rects_2)
  bestLine = None
  bestGain = 0
  for line in enumeratePossibleLines(bb):
    above_1 = len([r for r in rects_1 if line.isAbove(r)])
    below_1 = len([r for r in rects_1 if line.isBelow(r)])
    above_2 = len([r for r in rects_2 if line.isAbove(r)])
    below_2 = len([r for r in rects_2 if line.isBelow(r)])

    # These groups are separated by the line, no need to 
    # perform all-vs-all collision checks on those groups!
    gain = above_1 * below_2 + above_2 * below_1
    if gain > bestGain:
      bestGain = gain
      bestLine = line
  return bestLine

# Collides all rectangles from list `rects_1` with 
# all rectangles from list `rects_2`, and invokes
# `onCollision(a, b)` on every colliding `a` and `b`.
def collideAllVsAll(rects_1, rects_2, onCollision):
  if rects_1 and rects_2: # if one list empty, no collisions
    line = findGoodDividingLine(rects_1, rects_2)
    if line:
      above_1 = [r for r in rects_1 if line.isAbove(r)]
      below_1 = [r for r in rects_1 if line.isBelow(r)]
      above_2 = [r for r in rects_2 if line.isAbove(r)]
      below_2 = [r for r in rects_2 if line.isBelow(r)]
      intersect_1 = [r for r in rects_1 if not (line.isAbove(r) or line.isBelow(r))]
      intersect_2 = [r for r in rects_2 if not (line.isAbove(r) or line.isBelow(r))]
      collideAllVsAll(above_1, above_2, onCollision)
      collideAllVsAll(above_1, intersect_2, onCollision)
      collideAllVsAll(intersect_1, above_2, onCollision)
      collideAllVsAll(intersect_1, intersect_2, onCollision)
      collideAllVsAll(intersect_1, below_2, onCollision)
      collideAllVsAll(below_1, intersect_2, onCollision)
      collideAllVsAll(below_1, below_2, onCollision)
    else:
      for r1 in rects_1:
        for r2 in rects_2:
          if r1.intersects(r2):
            onCollision(r1, r2)

这是一个小演示:

rects = [
  Rectangle(1,6,9,10),
  Rectangle(4,7,6,10),
  Rectangle(1,5,6,7),
  Rectangle(8,9,8,10),
  Rectangle(6,9,5,7),
  Rectangle(8,9,1,6),
  Rectangle(7,9,2,4),
  Rectangle(2,8,2,3),
  Rectangle(1,3,1,4)
]

def showInterestingCollision(a, b):
  if a is not b:
    if a.left < b.left:
      print("%r <-> %r collision" % (a, b))

collideAllVsAll(rects, rects, showInterestingCollision)

至少在这种情况下,它确实检测到了所有有趣的碰撞:

[1.000000,6.000000]x[9.000000,10.000000] <-> [4.000000,7.000000]x[6.000000,10.000000] collision
[1.000000,5.000000]x[6.000000,7.000000] <-> [4.000000,7.000000]x[6.000000,10.000000] collision
[4.000000,7.000000]x[6.000000,10.000000] <-> [6.000000,9.000000]x[5.000000,7.000000] collision
[6.000000,9.000000]x[5.000000,7.000000] <-> [8.000000,9.000000]x[1.000000,6.000000] collision
[7.000000,9.000000]x[2.000000,4.000000] <-> [8.000000,9.000000]x[1.000000,6.000000] collision
[2.000000,8.000000]x[2.000000,3.000000] <-> [8.000000,9.000000]x[1.000000,6.000000] collision
[2.000000,8.000000]x[2.000000,3.000000] <-> [7.000000,9.000000]x[2.000000,4.000000] collision
[1.000000,3.000000]x[1.000000,4.000000] <-> [2.000000,8.000000]x[2.000000,3.000000] collision

这是一个更真实的演示:

from random import random
from matplotlib import pyplot as plt

def randomRect():
  w = random() * 0.1
  h = random() * 0.1
  centerX = random() * (1 - w)
  centerY = random() * (1 - h)
  return Rectangle(
    centerX - w/2, centerX + w/2,
    centerY - h/2, centerY + h/2
  )

randomRects = [randomRect() for _ in range(0, 500)]

for r in randomRects:
  plt.fill(
    [r.left, r.right, r.right, r.left], 
    [r.bottom, r.bottom, r.top, r.top],
    'b-',
    color = 'k',
    fill = False
  )

def markSubrectanglesRed(a, b):
  if a is not b:
    if a.isSubrectangle(b):
      plt.fill(
        [a.left, a.right, a.right, a.left], 
        [a.bottom, a.bottom, a.top, a.top],
        'b-',
        color = 'r',
        alpha = 0.4
      )
      plt.fill(
        [b.left, b.right, b.right, b.left], 
        [b.bottom, b.bottom, b.top, b.top],
        'b-',
        color = 'b',
        fill = False
      )

collideAllVsAll(randomRects, randomRects, markSubrectanglesRed)

plt.show()

该图以红色显示所有消除的矩形,以蓝色显示封闭的矩形:

这是一个带有单个碰撞的小示例的准二元空间分区的边界框(黄色)和选择的分割线(青色)的可视化:

对于 10000 个“大小合理”的随机矩形(相交率大致如图所示),它会在 18 秒内计算出所有碰撞,即使代码距离优化还很远。

【讨论】:

    【解决方案2】:

    您的问题是空间邻近性问题之一,因此我建议您考虑对数据进行空间索引。那就是以查询空间关系便宜的方式存储或索引您的矩形。最常见的数据结构见wikipedia

    我使用 R-tree 实现了一个演示。整个“算法”由以下函数组成。它不是特别优雅,因为每个独特的碰撞都被研究了两次。这主要是由于使用的rtree 库提供的访问和查询接口有限。

    import rtree  
    def findCollisions(rects, onCollision):
        idx = rtree.index.Index(interleaved=False)
        for rect in rects:
            idx.insert(rect.id, rect.coords)
    
        for rect in rects:
            ids = idx.intersection(rect.coords)
            for hit in [randomRects[j] for j in ids]:
                onCollision(rect, hit)
    

    我从@AndreyTyukin 那里无耻地复制了周围的基础设施,只做了轻微的修改:

    from random import random
    
    def isSubinterval(aStart, aEnd, bStart, bEnd):
      return aStart >= bStart and aEnd <= bEnd
    
    def intersects(aStart, aEnd, bStart, bEnd):
      return not (aEnd < bStart or aStart > bEnd)
    
    class Rectangle:
      id = 0
      def __init__(self, l, r, b, t):
        self.left = l
        self.right = r
        self.bottom = b
        self.top = t
        self.id = Rectangle.id
        Rectangle.id += 1
    
      @property  
      def coords(self):
          return (self.left, self.right, self.bottom, self.top)
    
      def isSubrectangle(self, other):
        return (
          isSubinterval(self.left, self.right, other.left, other.right) and
          isSubinterval(self.bottom, self.top, other.bottom, other.top)
        )
    
      def intersects(self, other):
        return (
          intersects(self.left, self.right, other.left, other.right) and
          intersects(self.bottom, self.top, other.bottom, other.top)
        )
    
      def __repr__(self):
        return ("[%f,%f]x[%f,%f]" % (self.left, self.right, self.bottom, self.top))
    
    
    def randomRect(ratio=0.1, scale=100):
      w = random() * ratio
      h = random() * ratio
      centerX = random() * (1 - w)
      centerY = random() * (1 - h)
      return Rectangle(
        scale*(centerX - w/2), scale*(centerX + w/2),
        scale*(centerY - h/2), scale*(centerY + h/2),
      )
    

    与@Andrey 的解决方案比较产生了大约一个数量级的改进。这可能主要是由于 python rtree 使用底层 C 实现。

    【讨论】:

    • @property 允许在不带括号的情况下调用coords?整洁的。是的,正如我所说,这个实现的性能无法与经过调整的 C 实现竞争。目前,我只关心渐近行为和正确性。如果删除所有不必要的重复迭代,即使在纯 Python 中,实现速度也可能至少快 4 倍。
    【解决方案3】:

    如果矩形分布合理,您可以将其视为一维问题,首先关注 X(或 Y)轴,从而节省时间。

    每个矩形都有一个最小和最大 X 坐标,即其左上角和右下角的 X 坐标。为每个矩形创建两条记录,给出其最小或最大 X 坐标和指向该矩形的指针。将这些记录按 X 坐标的升序排序,然后按此顺序处理它们。

    维护一个按最小 X 坐标排序的矩形数组,当你看到它的最小 X 坐标时插入一条记录,当你看到它的最大 X 坐标时从其中删除一条记录。就在你删除一条记录之前,你可以在数组中进行二分搜索,找到所有最小 X 坐标不超过你要删除的记录的最小 X 坐标和最大 X 坐标的记录至少是您要删除的记录。检查这些以查看它们的 Y 坐标是否还包含您要删除的记录。如果是这样,您找到了一条完全包含您要删除的记录的记录。这应该会找到所有的 X 包含,因为该数组包含每个矩形的记录,这些矩形与 X 维度中的当前 X 点重叠 - 它们已插入但尚未删除。

    (事实上,如果 X 坐标有关联,您将需要比这更小心)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-30
      相关资源
      最近更新 更多