Python&算法:如何做简单的几何形状匹配?

时间:2015-05-16 04:46:01

标签: python algorithm geometry

给定一组points (with order),我想知道它的形状是否在某些类型内。类型是:

rectangle = [(0,0),(0,1),(1,1),(1,0)]
hexagon = [(0,0),(0,1),(1,2),(2,1),(2,0),(1,-1)]
l_shape = [(0,0),(0,3),(1,3),(1,1),(3,1),(3,0)]
concave = [(0,0),(0,3),(1,3),(1,1),(2,1),(2,3),(3,3),(3,0)]
cross = [(0,0),(0,-1),(1,-1),(1,0),(2,0),(2,1),(1,1),(1,2),(0,2),(0,1),(-1,1),(-1,0)]

例如,给roratated_rectangle = [(0,0),(1,1),(0,2),(-1,1)] 我们知道它属于上面的rectangle

enter image description here类似于enter image description here

注意:

  1. rotationdifferent length边缘被视为相似。
  2. 输入点为ordered。 (因此可以path模块中的polygon绘制
  3. 我该怎么办?这有什么算法吗?

    我在想什么:

    也许我们可以从给定的lines重建points。从lines开始,我们可以获得angles的形状。通过比较angle series(顺时针和逆时针),我们可以确定输入点是否与上面给出的类型相似。

3 个答案:

答案 0 :(得分:5)

你的想法基本上是正确的想法。您希望将测试形状中的角度序列与预定义形状中的角度序列进行比较(对于每个预定义的形状)。由于测试形状的第一个顶点可能与匹配的预定义形状的第一个顶点不对应,因此我们需要允许测试形状的角度序列相对于预定义形状的序列旋转。 (也就是说,您的测试形状的序列可能是a,b,c,d,但您的预定义形状是c,d,a,b。)此外,测试形状的序列可能会反转,在这种情况下,角度也会相反到预定义的形状的角度。 (即a,b,c,d与-d,-c,-b,-a或等效2π-d,2π-c,2π-b,2π-a。)

我们可以尝试为角度序列选择规范旋转。例如,我们可以找到按字典顺序排列的最小旋转。 (例如,l_shape给出的序列是3π/2,3π/ 2,π/2,3π/2,3π/2,3π/ 2.按字典顺序最小的旋转首先放π/ 2:π /2,3π/2,3π/2,3π/2,3π/2,3π/ 2。)

但是,我认为浮点舍入可能会导致我们为测试形状与预定义形状选择不同的规范旋转。因此,我们只需检查所有轮换。

首先,返回形状角度序列的函数:

import math

def anglesForPoints(points):
    def vector(tail, head):
        return tuple(h - t for h, t in zip(head, tail))

    points = points[:] + points[0:2]
    angles = []
    for p0, p1, p2 in zip(points, points[1:], points[2:]):
        v0 = vector(tail=p0, head=p1)
        a0 = math.atan2(v0[1], v0[0])
        v1 = vector(tail=p1, head=p2)
        a1 = math.atan2(v1[1], v1[0])
        angle = a1 - a0
        if angle < 0:
            angle += 2 * math.pi
        angles.append(angle)
    return angles

(请注意,使用点积计算余弦是不够的,因为我们需要一个有角度的角,但cos(a) == cos(-a)。)

接下来,生成列表的所有旋转的生成器:

def allRotationsOfList(items):
    for i in xrange(len(items)):
        yield items[i:] + items[:i]

最后,确定两个形状是否匹配:

def shapesMatch(shape0, shape1):
    if len(shape0) != len(shape1):
        return False

    def closeEnough(a0, a1):
        return abs(a0 - a1) < 0.000001

    angles0 = anglesForPoints(shape0)
    reversedAngles0 = list(2 * math.pi - a for a in reversed(angles0))
    angles1 = anglesForPoints(shape1)
    for rotatedAngles1 in allRotationsOfList(angles1):
        if all(closeEnough(a0, a1) for a0, a1 in zip(angles0, rotatedAngles1)):
            return True
        if all(closeEnough(a0, a1) for a0, a1 in zip(reversedAngles0, rotatedAngles1)):
            return True
    return False

(注意我们需要使用模糊比较,因为浮点舍入误差。由于我们知道角度总是在小的固定范围0 ...2π,我们可以使用绝对误差限制。)

>>> shapesMatch([(0,0),(1,1),(0,2),(-1,1)], rectangle)
True
>>> shapesMatch([(0,0),(1,1),(0,2),(-1,1)], l_shape)
False
>>> shapesMatch([(0,0), (1,0), (1,1), (2,1), (2,2), (0,2)], l_shape)
True

如果要将测试形状与所有预定义形状进行比较,您可能只想计算一次测试形状的角度序列。如果您要针对预定义的形状测试许多形状,您可能只想预先计算预定义形状的序列一次。我将这些优化作为练习留给读者。

答案 1 :(得分:2)

您的形状不仅可以旋转,也可以翻译,甚至可以缩放。节点的方向也可以不同。例如,您的原始正方形的边长为1.0,并且是逆时针定义的,而您的钻石形状的边长为1.414,并且是顺时针定义的。

您需要找到一个比较好的参考。以下应该有效:

  • 找到每个形状的重心 C
  • 确定所有节点的径向坐标( r φ),其中径向坐标系的原点是重心 C
  • 规范化每个形状的半径,使形状中 r 的最大值为1.0
  • 确保节点逆时针定向,即增加角度φ

现在您有两个 n 径向坐标列表。 (已经排除了形状中节点数不匹配或节点数少于三个的情况。)

评估偏移的所有 n 配置,其中保留第一个数组,然后移动第二个数组。对于四元素数组,您可以比较:

{a1, a2, a3, a4} <=> {b1, b2, b3, b4}
{a1, a2, a3, a4} <=> {b2, b3, b4, b1}
{a1, a2, a3, a4} <=> {b3, b4, b1, b2}
{a1, a2, a3, a4} <=> {b4, b1, b2, b3}

径向坐标是浮点数。比较值时,应该允许一些纬度来满足浮点数学引入的不准确性。因为数字0≤ r ≤1且 - πφπ大致在相同的范围内,你可以使用固定的epsilon。

将半径与其归一化值进行比较。角度通过它们与列表中前一点的角度的差异进行比较。当这种差异为负时,我们已经绕着360°边界缠绕并且必须进行调整。 (我们必须强制执行正角度差异,因为我们比较的形状可能不会同等旋转,因此可能没有环绕间隙。)允许角度向前和向后,但必须最终完整圆。 / p>

代码必须检查 n 配置并测试每个 n 节点。实际上,早期会发现不匹配,因此代码应该具有良好的性能。如果要比较很多形状,可能需要事先为所有形状创建标准化的逆时针径向表示。

无论如何,这里是:

def radial(x, y, cx = 0.0, cy = 0.0):
    """Return radial coordinates from Cartesian ones"""

    x -= cx
    y -= cy

    return (math.sqrt(x*x + y*y), math.atan2(y, x))



def anticlockwise(a):
    """Reverse direction when a is clockwise"""

    phi0 = a[-1]
    pos = 0
    neg = 0

    for r, phi in a:
        if phi > phi0:
            pos += 1
        else:
            neg += 1

        phi0 = phi

    if neg > pos:
        a.reverse()



def similar_r(ar, br, eps = 0.001):
    """test two sets of radial coords for similarity"""

    _, aprev = ar[-1]
    _, bprev = br[-1]

    for aa, bb in zip(ar, br):
        # compare radii
        if abs(aa[0] - bb[0]) > eps:
            return False

        # compare angles
        da = aa[1] - aprev
        db = bb[1] - bprev

        if da < 0: da += 2 * math.pi
        if db < 0: db += 2 * math.pi

        if abs(da - db) > eps:
            return False

        aprev = aa[1]
        bprev = bb[1]

    return True



def similar(a, b):
    """Determine whether two shapes are similar"""

    # Only consider shapes with same number of points
    if len(a) != len(b) or len(a) < 3:
        return False        

    # find centre of gravity
    ax, ay = [1.0 * sum(x) / len(x) for x in zip(*a)]
    bx, by = [1.0 * sum(x) / len(x) for x in zip(*b)]

    # convert Cartesian coords into radial coords
    ar = [radial(x, y, ax, ay) for x, y in a]
    br = [radial(x, y, bx, by) for x, y in b]

    # find maximum radius
    amax = max([r for r, phi in ar])
    bmax = max([r for r, phi in br])

    # and normalise the coordinates with it
    ar = [(r / amax, phi) for r, phi in ar]
    br = [(r / bmax, phi) for r, phi in br]

    # ensure both shapes are anticlockwise
    anticlockwise(ar)
    anticlockwise(br)

    # now match radius and angle difference in n cionfigurations
    n = len(a)
    while n:
        if similar_r(ar, br):
            return True                

        br.append(br.pop(0))      # rotate br by one
        n -= 1

    return False

编辑:虽然此解决方案有效,但它过于复杂。 Rob的答案本质上是相似的,但使用了一个简单的指标:边缘之间的内部角度,它自动处理平移和缩放。

答案 2 :(得分:1)

通过线性代数接近此点,每个点都是一个向量(x,y),可以将其乘以rotation matrix以获得指定角度的新坐标(x1,y1)。这里似乎没有LaTeX的支持,所以我无法清楚地写出来,但实质上是:

(cos(a) -sin(a);sin(a) cos(a))*(x y) = (x1 y1)

这导致坐标x1,y1按角度旋转&#34; a&#34;。

编辑:这可能是基础理论,但您可以设置算法将形状逐点移动到(0,0),然后计算相邻点之间角度的余弦以对对象进行分类。