【问题标题】:Group intersecting lines together将相交线组合在一起
【发布时间】:2021-06-18 23:10:34
【问题描述】:

你好,

我正在尝试将一堆碰撞在一起的线条组合在一起。例如,如果 A 线与 B 线相撞,B 线与 C 线相撞,但 A 线不会与 C 线相撞。我希望它们都在一起。

我尝试利用集合的属性,所以我只有一个独特的解决方案并避免重复。我也试着让它有点快。我拥有的行数据是浮点数列表:[x1, y1, x2, y2]。

all_lines = lines.tolist()
lines_combinations = list()

for l in all_lines:
    #Find if the potential line already belongs to a group in line_combination
    remove_list_l = False  #is line (l) in a group in lines_combination
    remove_list_ol = False #is other line (ol) in a group in lines_combination
    line_group = False;

    for j in lines_combinations: #Iterate through all combination found already
        if tuple(l) in j:
            line_group = j
            remove_list_l = True
    if not line_group:
        line_group = {tuple(l)}
    
    #Remove a from the total line list 
    all_lines.remove(l)

    #Iterate through all potential lines
    for ol in all_lines:
        if check_collision(l, ol, px): 
            #If collision, check if k is in another group
            for k in lines_combinations: 
                 if tuple(ol) in k:
                     #If true, append both groups together
                     line_group.union(k)
                     replace_list_ol = True
            if not replace_list_ol:
                line_group.add(tuple(ol))
            
    #Remove both groups and add the new joint one
    if remove_list_l:   
        lines_combinations.remove(j)
    if replace_list_ol:
         if j != k:
             lines_combinations.remove(k)
    
    lines_combinations.append(line_group)

问题是效果不太好,而且速度很慢……比如下图:

算法无法将所有白线和所有棕色线明显相交时合并在一起(白色下面还有一些粉线也应该属于该组)。我不知道这是否是因为我在不同的组中复制了该行,或者算法是否在其他地方失败。最终,我希望任何一行在一个独特的组中出现一次,这样我就可以避免重复。我看不出算法哪里出错了……

我很乐意就如何做到这一点提供建议并使其更快!非常感谢!

编辑:大家好,我能够通过将“line_group.union(k)”行替换为“line_group = line_group.union(k)”来解决我的问题。所以它有效!

但是,每张图片的处理时间约为 30 秒……这需要太多时间。我正在寻找可能的优化建议!

编辑 2:您可以在此处找到我正在训练我的算法的数据: lines.txt 我已将其导出为 pd.DataFrame,因此您可以轻松加载它!我还添加了用于查找交叉点的 check_collision.py 脚本。与此处 M Oehm 的答案非常相似:stack-overflow。但是由于共线性的编辑。

事实证明我的碰撞脚本或排序算法都有问题,因为我将不应该属于一起的组绘制在一起。

就像下面一组的情节:

【问题讨论】:

  • 我觉得lines_combination 应该是一个集合,而不是一个列表,因此查找和删除是 O(1)。您可能还应该对空间进行分区,以便仅检查附近的线的碰撞。
  • 我试过了,因为这似乎是最好的方法。但是,似乎我无法在 Python 中创建一组集合。执行 list_combination.add(line_group) 时出现错误:“TypeError: unhashable type: 'set'”。
  • 30s的运行时间在不知道问题大小的情况下是没有用的。一张图片通常有多少行?您有可以分享的示例数据集吗?
  • 视图片而定。这里是 2500 行。我计划做的第一件事是做矩阵计算。使用数组一次性测试一条线与所有其他线,因此我不必执行 ~ 2500 * 2500 循环。我想知道是否有办法同时测试几行与几行。
  • 嗯,我已经写了一个答案:使用扫描线,这很容易实现。如果您需要更快,请使用更高效的 Bentley-Ottmann。如果还不够,请使用带有 BO 算法的图形库。 (具有 2500 条线的扫描线应该足够快,尤其是因为您的大部分线都很短。)如果您愿意,可以使用结果来填充 SIMD 矩阵。

标签: python algorithm sorting


【解决方案1】:

我认为您正在尝试在这里实现 union-find 的变体。您可以谷歌了解更多详情。您需要使用 O(n) 额外空间,其中 n 是行号。当两条线相交而不是将它们收集到一个集合中时,只需标记它们都属于同一组。所以如果 a & b 相交 a 将是 b 的根。现在如果 b & c 相交根 c 是 b 的根,它只是 a。现在,如果两行具有相同的根,则意味着它们在同一个组中。

  1. 用行号本身初始化这个额外的空间。即一开始每个节点都是它自己的根。 rarr = [0,1,2]

  2. 得到所有的交叉点。 (l1,l2), (l1,l3)。

  3. 对于每个交点,将第二个的根标记为第一个的根。

    rarr[l2] = root(l1).

  4. 元素的根是 - 获取交集的父元素,直到留在自己的索引中。

    根(x){

    while(rarr[x] != x) x = rarr[x];

    返回 rarr[x]; }

【讨论】:

  • 这并没有真正奏效。您能否确认我的实施是正确的。这是代码和由此产生的图片:imgur.com/a/CJbwQxQ.
  • 你不能只设置rarr[j] = root(i),你必须在根目录下加入两个列表:rarr[root(j)] = root(i)。现在所有根为j 的节点现在都将拥有根i(因为它们首先经过j。)
【解决方案2】:

由于我的 Python 练习不佳,我留下了一个 MATHEMATICA 脚本,我希望它是所需算法的实现

第一个生产数据

SeedRandom[1]
n = 100;
points = Table[RandomReal[{0, 100}, 2], {i, 1, n}];
segs = Table[{points[[k]] + RandomReal[{-20, 20}, 2], points[[k]] + RandomReal[{-20, 20}, 2]}, {k, 1, n}];
grseg = Table[ParametricPlot[x segs[[k, 1]] + (1 - x) segs[[k, 2]], {x, 0, 1}], {k, 1, n}];
Show[grseg, PlotRange -> All, Axes -> False]

跟随处理

Clear[intersects]
intersects[i_, j_, segs_] := Module[{si, sj, xi, xj, xi0, xj0, sol},
  si = segs[[i]]; sj = segs[[j]]; 
  sol = Solve[Thread[xi si[[1]] + (1 - xi) si[[2]] == xj sj[[1]] + (1 - xj) sj[[2]]], {xi, xj}]; 
  {xi0, xj0} = {xi, xj} /. sol[[1]]; 
  If[0 <= xi0 && xi0 <= 1 && 0 <= xj0 && xj0 <= 1, Return[True], Return[False]]
]

list = {};
For[i = 1, i <= n, i++,
 For[j = 1, j < i, j++,
  If[i != j && intersects[i, j, segs], AppendTo[list, Sort[{i, j}]]]
  ]
]

list = Union[list];
nl = Length[list];
LIST = {};
For[i = 1, i <= nl, i++,
 lista = list[[i]];
 For[j = 1, j <= nl, j++,
  If[i != j && Length[Intersection[lista, list[[j]]]] > 0, lista = Union[lista, list[[j]]]];
  ];
 AppendTo[LIST, lista];
]

LIST = Union[LIST];
nf = -2;
nL = -1;
While[nL != nf,
 LISTF = {};
 nL = Length[LIST];
 For[i = 1, i <= nL, i++,
  lista = LIST[[i]];
  For[j = 1, j <= nL, j++,
   If[i != j && Length[Intersection[lista, LIST[[j]]]] > 0, lista = Union[lista, LIST[[j]]]];
  ];
  AppendTo[LISTF, lista]
  ];
  LISTF = Union[LISTF];
  nf = Length[LISTF];
  LIST = LISTF
]

LISTF

还有最后的情节

listgr = {};
For[k = 1, k <= nf, k++, ns = Length[LISTF[[k]]];
 inds = LISTF[[k]];
 ni = Length[inds];
 color = RGBColor[RandomReal[{0, 1}], RandomReal[{0, 1}], RandomReal[{0, 1}]];
 AppendTo[listgr,Table[ParametricPlot[x segs[[inds[[j]], 1]] + (1 - x) segs[[inds[[j]], 2]], {x, 0, 1}, PlotStyle -> {color, Thick}], {j, 1, ni}]]]
Show[grseg, listgr, PlotRange -> All, Axes -> False]

注意

python版本如下。

from random import seed
from random import random
from operator import add
import numpy as np
import pylab as pl
from matplotlib import collections  as mc

seed(2)

# First producing data

n       = 100
span1   = 100
span2   = 15
centers = []

for _ in range(n):
    centers.append([span1*random(),span1*random()])

segments = []
for i in range(n):
    var1 = [2*span2*(random()-0.5),2*span2*(random()-0.5)]
    var2 = [2*span2*(random()-0.5),2*span2*(random()-0.5)]
    segments.append([list(map(add,centers[i],var1)),list(map(add,centers[i],var2))])

def intersect(s1,s2):
    c1 = list(map(lambda i, j: i-j,s1[1],s1[0]))
    c2 = list(map(lambda i, j: i-j,s2[1],s2[0]))
    c0 = list(map(lambda i, j: i-j,s2[0],s1[0]))
    c11 = np.dot(c1,c1)
    c22 = np.dot(c2,c2)
    c12 = np.dot(c1,c2)
    c01 = np.dot(c0,c1)
    c02 = np.dot(c0,c2)
    discr = c12*c12-c11*c22
    x=(c12*c02-c01*c22)/discr
    y=(c11*c02-c01*c12)/discr
    if ((0 < x) and (x < 1) and (0 < y) and (y < 1)):
        return(True)
    else:
        return(False)

def intersection(lst1, lst2): 
    return list(set(lst1).intersection(lst2)) 

def union(lst1, lst2): 
    final_list = list(set(lst1) | set(lst2)) 
    return final_list 

list_intersects = []
for i in range(n):
    si = segments[i]
    for j in range(i):
        sj = segments[j]
        if (i !=  j) and intersect(si,sj):
            list_intersects.append(sorted([i,j]))

print(list_intersects)
        
nl = len(list_intersects)
print(nl)

LIST = []
for i in range(nl):
    lista = list_intersects[i]
    for j in range(nl):
        if (i != j) and len(intersection(lista,list_intersects[j])) > 0:
            lista = sorted(union(lista,list_intersects[j]))
        if (lista not in LIST):
            LIST.append(lista)
print(LIST)

nF = -2
nL = -1
while (nL != nF):
    LISTF = []
    nL = len(LIST)
    for i in range(nL):
        lista = LIST[i]
        for j in range(nL):
            if (i != j) and len(intersection(lista,LIST[j])) > 0:
                lista = sorted(union(lista,LIST[j]))
        if (lista not in LISTF):
            LISTF.append(lista)
    nF = len(LISTF)
    LIST = LISTF
            
# final groups
    
(fig, ax) = pl.subplots(1)
c = np.array([(1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1)])       
nF = len(LISTF)
for i in range(nF):
    #print(">> ",LISTF[i])
    inds = LISTF[i]
    bunch = []
    for j in range(len(inds)):
        bunch.append(segments[inds[j]])
    #print(bunch)
    lc = mc.LineCollection(bunch, colors=c[i % 3], linewidths=2)
    ax.add_collection(lc)    
ax.autoscale()
ax.margins(0.1)

【讨论】:

    【解决方案3】:

    为了正确:Python 集合当然是集合的正确数据结构,但是在这里,您必须经常合并两个集合并删除合并的一个集合,常见的不相交Krishna 和 Caesareo 已经建议的 set 实现可能更适合:

    • 这些线被组织在一个有向图中,其中每条线都有一个出边,即父边。
    • 每个集合都由其“根”标识。根的父级就是它自己。
    • 一开始,每一行都在自己的集合中。
    • 可以通过跟随父级直到找到根来找到根。
    • 可以通过连接它们的根来连接两个集合,即将第一个根作为第二个的父级。

    这种表示方式适用于快速加入集合。它不适合遍历所有元素,这是您最后只需要做一次的事情。加入的集合越多,图中的链就越长。在找到根之后,有一个技巧可以用根替换非根的父,这样最终,根将成为集合中所有行的直接父。

    这可以在Line 类中编码:

    class Line:
        def __init__(self, p, q):
            self.p = p
            self.q = q
            self.parent = self
            
        def __repl__(self):
            return "Line(%r, %r)" % (self.p, self.q)
            
        def __getitem__(self, index):
            return (self.p, self.q)[index]
            
        def join(self, other):
            sp = self.root()
            op = other.root()
    
            op.parent = sp
    
        def root(self):
            if self.parent == self:
                return self
    
            self.parent = self.parent.root()
            
            return self.parent
    

    l.root() 方法找到关联的根节点,l.join(k) 加入两个集合。那么基本算法是:

    for i, l in enumerate(lines):           # find intersections
        g = l.root()
    
        for k in lines[i + 1:]:
            if g != k.root() and intersects(l, k):
                l.join(k)
    
    groups = {}
    
    for l in lines:                         # find groups
        h = l.root()
        
        groups.setdefault(h, []);
        groups[h] += [l]
    

    这将测试三角形中所有可能的交点。它不会测试已经在同一组中的行,但仍然是 O(N ²)。

    加快速度:加快空间查找或匹配算法(例如您的算法)的一种方法是减少搜索空间。有几种方法,点的桶或 kd 树就是示例。

    在您的情况下,几乎所有空间都有长线,您可以使用扫描线。假设您以正 y 方向从下到上扫描图像。为每一行创建两个“事件”:一个用于下层节点,一个用于上层节点节点。每个事件都有一个 y 坐标、对行的引用和类型,它是“进入”或“离开”之一:

    enter(line, min(y) − Δ)
    leave(line, max(y) + Δ)
    

    y 坐标对所有点进行排序。创建一个活动集并遍历已排序的数组。当您点击输入事件时,针对该行测试活动集中的所有行,然后将该行添加到活动集中。当您遇到离开事件时,从活动集中删除该行。

    这将使您只查看搜索空间的一小部分。

    sweep = []
    delta = 1.0
    
    for l in lines:
        p, q = l
        
        sweep.append((min(p.y, q.y) - delta, True, l))
        sweep.append((max(p.y, q.y) + delta, False, l))
    
    sweep.sort()    
    active = set()
    
    for x, activate, line in sweep:    
        if activate:    
            for other in active:
                if intersects(line, other):
                    line.join(other)
    
            active.add(line)
        else:  
            active.remove(line)
    

    将集合分类成组如上。 (我不检查两个碰撞候选是否在同一组中,因为测试似乎会稍微降低性能,但这可能取决于输入集。)

    让它更快:其他算法,例如Bentley-Ottmann 算法,通过在垂直方向上划分活动集来改进扫描线算法。

    同样在 C 中实现上述“简单”的扫描线算法可以大大加快速度。 (我想这很大程度上是因为数据的局部性好。)

    因此,如果您想要真正快速,请寻找具有 Python 接口并实现 Bentley-Ottmann 算法或更好算法的图形库。

    【讨论】:

    • 我一直在看你的回答。事实证明 l.group() 在您的 Line 类中不存在。你的意思是 l.root() 吗?
    • 是的,一定是l.root()。对此感到抱歉。
    • 这也应该是 g != l.root() 在 l.join(k) 之前的 if 语句中。您也对该解决方案的稳健性有信心吗?这些行仍然被错误地分组,我处于我的 python 知识的极限。我正在尝试确定问题是来自我的交集函数还是算法仍然错误地分组。
    • 我在引入group作为组名之后重命名了root方法,所以每次.group()都应该是.root()。 (顺便说一下,你的代码与remove_replace有同样的问题。)
    • 我对我的解决方案很有信心,是的。在我的测试中,我使用了一种简单的相交算法,而不是带有半径的算法。我看到你已经提供了数据。我以后可能会研究一下。
    猜你喜欢
    • 2019-12-31
    • 1970-01-01
    • 1970-01-01
    • 2020-12-29
    • 1970-01-01
    • 2017-03-02
    • 2022-01-12
    • 2018-07-30
    • 1970-01-01
    相关资源
    最近更新 更多