如何以有效的方式找到两个轮廓集之间的所有交点

时间:2013-07-02 01:57:32

标签: python numpy matplotlib computational-geometry

我想知道在两组轮廓线之间找到所有交点(到舍入误差)的最佳方法。这是最好的方法吗?这是一个例子:

import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-1,1,500)
X,Y = np.meshgrid(x,x)
Z1 = np.abs(np.sin(2*X**2+Y))
Z2 = np.abs(np.cos(2*Y**2+X**2))
plt.contour(Z1,colors='k')
plt.contour(Z2,colors='r')
plt.show()

enter image description here

我想要一些类似的东西:

intersection_points = intersect(contour1,contour2)
print intersection_points
[(x1,y1),...,(xn,yn)]

2 个答案:

答案 0 :(得分:7)

import collections
import matplotlib.pyplot as plt
import numpy as np
import scipy.spatial as spatial
import scipy.spatial.distance as dist
import scipy.cluster.hierarchy as hier


def intersection(points1, points2, eps):
    tree = spatial.KDTree(points1)
    distances, indices = tree.query(points2, k=1, distance_upper_bound=eps)
    intersection_points = tree.data[indices[np.isfinite(distances)]]
    return intersection_points


def cluster(points, cluster_size):
    dists = dist.pdist(points, metric='sqeuclidean')
    linkage_matrix = hier.linkage(dists, 'average')
    groups = hier.fcluster(linkage_matrix, cluster_size, criterion='distance')
    return np.array([points[cluster].mean(axis=0)
                     for cluster in clusterlists(groups)])


def contour_points(contour, steps=1):
    return np.row_stack([path.interpolated(steps).vertices
                         for linecol in contour.collections
                         for path in linecol.get_paths()])


def clusterlists(T):
    '''
    http://stackoverflow.com/a/2913071/190597 (denis)
    T = [2, 1, 1, 1, 2, 2, 2, 2, 2, 1]
    Returns [[0, 4, 5, 6, 7, 8], [1, 2, 3, 9]]
    '''
    groups = collections.defaultdict(list)
    for i, elt in enumerate(T):
        groups[elt].append(i)
    return sorted(groups.values(), key=len, reverse=True)

# every intersection point must be within eps of a point on the other
# contour path
eps = 1.0

# cluster together intersection points so that the original points in each flat
# cluster have a cophenetic_distance < cluster_size
cluster_size = 100

x = np.linspace(-1, 1, 500)
X, Y = np.meshgrid(x, x)
Z1 = np.abs(np.sin(2 * X ** 2 + Y))
Z2 = np.abs(np.cos(2 * Y ** 2 + X ** 2))
contour1 = plt.contour(Z1, colors='k')
contour2 = plt.contour(Z2, colors='r')

points1 = contour_points(contour1)
points2 = contour_points(contour2)

intersection_points = intersection(points1, points2, eps)
intersection_points = cluster(intersection_points, cluster_size)
plt.scatter(intersection_points[:, 0], intersection_points[:, 1], s=20)
plt.show()

产量

enter image description here

答案 1 :(得分:3)

根据@unutbu的答案和直接从numpy-and-line-intersections拨打的交叉算法,我想出了这个。由于蛮力的方式找到交叉点和循环内循环内的循环,它很慢。可能有一种方法可以使循环更快,但我不确定交集算法。

import matplotlib.pyplot as plt
import numpy as np

def find_intersections(A, B):
    #this function stolen from https://stackoverflow.com/questions/3252194/numpy-and-line-intersections#answer-9110966
    # min, max and all for arrays
    amin = lambda x1, x2: np.where(x1<x2, x1, x2)
    amax = lambda x1, x2: np.where(x1>x2, x1, x2)
    aall = lambda abools: np.dstack(abools).all(axis=2)
    slope = lambda line: (lambda d: d[:,1]/d[:,0])(np.diff(line, axis=0))

    x11, x21 = np.meshgrid(A[:-1, 0], B[:-1, 0])
    x12, x22 = np.meshgrid(A[1:, 0], B[1:, 0])
    y11, y21 = np.meshgrid(A[:-1, 1], B[:-1, 1])
    y12, y22 = np.meshgrid(A[1:, 1], B[1:, 1])

    m1, m2 = np.meshgrid(slope(A), slope(B))
    m1inv, m2inv = 1/m1, 1/m2

    yi = (m1*(x21-x11-m2inv*y21) + y11)/(1 - m1*m2inv)
    xi = (yi - y21)*m2inv + x21

    xconds = (amin(x11, x12) < xi, xi <= amax(x11, x12), 
              amin(x21, x22) < xi, xi <= amax(x21, x22) )
    yconds = (amin(y11, y12) < yi, yi <= amax(y11, y12),
              amin(y21, y22) < yi, yi <= amax(y21, y22) )

    return xi[aall(xconds)], yi[aall(yconds)]

x = np.linspace(-1,1,500)
X,Y = np.meshgrid(x,x)
Z1 = np.abs(np.sin(2*X**2+Y))
Z2 = np.abs(np.cos(2*Y**2+X**2))
contour1 = plt.contour(Z1,colors='k')
contour2 = plt.contour(Z2,colors='r')


xi = np.array([])
yi = np.array([])
for linecol in contour1.collections:
    for path in linecol.get_paths():
        for linecol2 in contour2.collections:
            for path2 in linecol2.get_paths():
                xinter, yinter = find_intersections(path.vertices, path2.vertices)
                xi = np.append(xi, xinter)
                yi = np.append(yi, yinter)

plt.scatter(xi, yi, s=20)                
plt.show()

enter image description here

上面的

编辑: find_intersections不处理垂直和水平线,也不处理重叠的线段。下面的linelineintersect函数确实处理了这些情况,但整个过程仍然很慢。我已经添加了一个计数,所以你可以知道它还需要多长时间。我还将contour1 = plt.contour(Z1,colors='k')更改为contour1 = plt.contour(X,Y,Z1,colors='k'),因此轴和交叉点位于网格的实际X和Y坐标中,而不是网格点的计数。

我认为问题在于你只有很多数据。在一个轮廓集中,有24行,总共12818个线段,另一个轮廓集有29行,有11411个线段。这是要检查的很多线段组合(696次调用linelineintersect)。通过使用较粗糙的网格来计算您的功能,您可以更快地获得结果(100乘100比原来的500乘500快得多)。您可能能够进行某种线扫描算法,但我不知道如何实现这些功能。线路交叉点问题在计算机游戏中有许多应用,称为碰撞检测 - 必须有一些图形库可以快速确定所有交叉点。

import numpy as np
from numpy.lib.stride_tricks import as_strided
import matplotlib.pyplot as plt
import itertools  

def unique(a, atol=1e-08):
    """Find unique rows in 2d array

    Parameters
    ----------
    a : 2d ndarray, float
        array to find unique rows in 
    atol : float, optional
        tolerance to check uniqueness

    Returns
    -------
    out : 2d ndarray, float
        array of unique values

    Notes
    -----
    Adapted to include tolerance from code at https://stackoverflow.com/questions/8560440/removing-duplicate-columns-and-rows-from-a-numpy-2d-array#answer-8564438

    """

    if np.issubdtype(a.dtype, float):        
        order = np.lexsort(a.T)
        a = a[order]
        diff = np.diff(a, axis=0)
        np.abs(diff,out = diff)
        ui = np.ones(len(a), 'bool')
        ui[1:] = (diff > atol).any(axis=1) 
        return a[ui]
    else:
        order = np.lexsort(a.T)
        a = a[order]
        diff = np.diff(a, axis=0)        
        ui = np.ones(len(a), 'bool')
        ui[1:] = (diff != 0).any(axis=1) 
        return a[ui]

def linelineintersect(a, b, atol=1e-08):
    """Find all intersection points of two lines defined by series of x,y pairs

    Intersection points are unordered
    Colinear lines that overlap intersect at any end points that fall within the overlap    

    Parameters
    ----------
    a, b : ndarray
        2 column ndarray of x,y values defining a two dimensional line.  1st 
        column is x values, 2nd column is x values.    

    Notes
    -----
    An example of 3 segment line is: a = numpy.array([[0,0],[5.0,3.0],[4.0,1]])
    Function faster when there are no overlapping line segments
    """    

    def x_y_from_line(line):
        """1st and 2nd column of array"""
        return (line[:, 0],line[:, 1])
    def meshgrid_as_strided(x, y, mask=None):
        """numpy.meshgrid without copying data (using as_strided)"""        
        if mask is None:            
            return (as_strided(x, strides=(0, x.strides[0]), shape=(y.size, x.size)),
                    as_strided(y, strides=(y.strides[0],0), shape=(y.size, x.size)))    
        else:            
            return (np.ma.array(as_strided(x, strides=(0, x.strides[0]), shape=(y.size, x.size)), mask=mask),
                    np.ma.array(as_strided(y, strides=(y.strides[0],0), shape=(y.size, x.size)), mask=mask))

    #In the following the indices i, j represents the pairing of the ith segment of b and the jth segment of a
    #e.g. if ignore[i,j]==True then the ith segment of b and the jth segment of a cannot intersect
    ignore = np.zeros([b.shape[0]-1, a.shape[0]-1], dtype=bool)    

    x11, x21 = meshgrid_as_strided(a[:-1, 0], b[:-1, 0], mask=ignore)
    x12, x22 = meshgrid_as_strided(a[1: , 0], b[1: , 0], mask=ignore)
    y11, y21 = meshgrid_as_strided(a[:-1, 1], b[:-1, 1], mask=ignore)
    y12, y22 = meshgrid_as_strided(a[1: , 1], b[1: , 1], mask=ignore)    

    #ignore segements with non-overlappiong bounding boxes
    ignore[np.ma.maximum(x11, x12) < np.ma.minimum(x21, x22)] = True
    ignore[np.ma.minimum(x11, x12) > np.ma.maximum(x21, x22)] = True
    ignore[np.ma.maximum(y11, y12) < np.ma.minimum(y21, y22)] = True
    ignore[np.ma.minimum(y11, y12) > np.ma.maximum(y21, y22)] = True

    #find intersection of segments, ignoring impossible line segment pairs when new info becomes available      
    denom_ = np.empty(ignore.shape, dtype=float)   
    denom = np.ma.array(denom_, mask=ignore)
    denom_[:, :] = ((y22 - y21) * (x12 - x11)) - ((x22 - x21) * (y12 - y11))    

    ua_ = np.empty(ignore.shape, dtype=float)  
    ua = np.ma.array(ua_, mask=ignore)
    ua_[:, :] = (((x22 - x21) * (y11 - y21)) - ((y22 - y21) * (x11 - x21)))            
    ua_[:, :] /= denom

    ignore[ua < 0] = True
    ignore[ua > 1] = True

    ub_ = np.empty(ignore.shape, dtype=float)  
    ub = np.ma.array(ub_, mask=ignore)
    ub_[:, :] = (((x12 - x11) * (y11 - y21)) - ((y12 - y11) * (x11 - x21)))
    ub_[:, :] /= denom

    ignore[ub < 0] = True
    ignore[ub > 1] = True


    nans_ = np.zeros(ignore.shape, dtype = bool)
    nans = np.ma.array(nans_, mask = ignore)    
    nans_[:,:] = np.isnan(ua)    

    if not np.ma.any(nans):

        #remove duplicate cases where intersection happens on an endpoint
#        ignore[np.ma.where((ua[:, :-1] == 1) & (ua[:, 1:] == 0))] = True
#        ignore[np.ma.where((ub[:-1, :] == 1) & (ub[1:, :] == 0))] = True        
        ignore[np.ma.where((ua[:, :-1] < 1.0 + atol) & (ua[:, :-1] > 1.0 - atol) & (ua[:, 1:] < atol) & (ua[:, 1:] > -atol))] = True
        ignore[np.ma.where((ub[:-1, :] < 1 + atol) & (ub[:-1, :] > 1 - atol) & (ub[1:, :] < atol) & (ub[1:, :] > -atol))] = True   

        xi = x11 + ua * (x12 - x11)
        yi = y11 + ua * (y12 - y11)
        return np.ma.compressed(xi), np.ma.compressed(yi)
    else:
        n_nans = np.ma.sum(nans)               
        n_standard = np.ma.count(x11) - n_nans
        #I initially tried using a set to get unique points but had problems with floating point equality

        #create n by 2 array to hold all possible intersection points, check later for uniqueness
        points = np.empty([n_standard + 2 * n_nans, 2], dtype = float) #each colinear segment pair has two intersections, hence the extra n_colinear points        

        #add standard intersection points
        xi = x11 + ua * (x12 - x11)
        yi = y11 + ua * (y12 - y11)        
        points[:n_standard,0] = np.ma.compressed(xi[~nans])
        points[:n_standard,1] = np.ma.compressed(yi[~nans])        
        ignore[~nans]=True


        #now add the appropriate intersection points for the colinear overlapping segments
        #colinear overlapping segments intersect on the two internal points of the four points describing a straight line
        #ua and ub have already serverd their purpose. Reuse them here to mean:
            #ua is relative position of 1st b segment point along segment a
            #ub is relative position of 2nd b segment point along segment a
        use_x = x12 != x11 #avoid vertical lines diviiding by zero
        use_y = ~use_x

        ua[use_x] = (x21[use_x]-x11[use_x]) / (x12[use_x]-x11[use_x])
        ua[use_y] = (y21[use_y]-y11[use_y]) / (y12[use_y]-y11[use_y])

        ub[use_x] = (x22[use_x]-x11[use_x]) / (x12[use_x]-x11[use_x])
        ub[use_y] = (y22[use_y]-y11[use_y]) / (y12[use_y]-y11[use_y])

        np.ma.clip(ua, a_min=0,a_max = 1, out = ua)
        np.ma.clip(ub, a_min=0,a_max = 1, out = ub)

        xi = x11 + ua * (x12 - x11)
        yi = y11 + ua * (y12 - y11)
        points[n_standard:n_standard + n_nans,0] = np.ma.compressed(xi)
        points[n_standard:n_standard + n_nans,1] = np.ma.compressed(yi)

        xi = x11 + ub * (x12 - x11)
        yi = y11 + ub * (y12 - y11)
        points[n_standard+n_nans:,0] = np.ma.compressed(xi)
        points[n_standard+n_nans:,1] = np.ma.compressed(yi)

        points_unique = unique(points)
        return points_unique[:,0], points_unique[:,1]

x = np.linspace(-1,1,500)
X,Y = np.meshgrid(x,x)
Z1 = np.abs(np.sin(2*X**2+Y))
Z2 = np.abs(np.cos(2*Y**2+X**2))
contour1 = plt.contour(X,Y,Z1,colors='k')
contour2 = plt.contour(X,Y,Z2,colors='r')


xi = np.array([])
yi = np.array([])

i=0            
ncombos = (sum([len(x.get_paths()) for x in contour1.collections]) * 
            sum([len(x.get_paths()) for x in contour2.collections]))
for linecol1, linecol2 in itertools.product(contour1.collections, contour2.collections):
    for path1, path2 in itertools.product(linecol1.get_paths(),linecol2.get_paths()):
        i += 1
        print('line combo %5d of %5d' % (i, ncombos))        
        xinter, yinter = linelineintersect(path1.vertices, path2.vertices)

        xi = np.append(xi, xinter)
        yi = np.append(yi, yinter)

plt.scatter(xi, yi, s=20)                
plt.show()

此图是针对50x50网格的实际X轴和Y轴: enter image description here