Intersection between a convex polygon and a line

概要

凸多角形 \(P = (v_1, v_2, ..., v_N)\) と 点\(p_0, p_1\) を通る直線の交差判定と交点を求める

具体的計算

\(h(i) = (v_{(i-1 \text{ mod } N)+1} - p_0) \times (p_1 - p_0)\) と定義する。

この関数は、頂点が直線の左右どちらかにあるかによって符号が変化し、直線と頂点の距離に比例する。

関数\(h\) の最大値をとる\(x\) を \(x_1\), 最小値をとる\(x\) を \(x_0\) とする時、 この関数は以下の性質を満たす

  • 区間\([x_0+1, x_0+N-1\)] について 関数\(h\) は上に凸な関数

  • 区間\([x_1+1, x_1+N-1\)] について 関数\(h\) は下に凸な関数

この最大値と最小値は2回の三分探索で求める。 1回目の三分探索では 区間\([1, N\)] で定義される以下の関数\(g\)上で最小値をとる \(x_0\) を求める。(\(h(1) = h(N)\) の場合や \(h(1)\) が極値の場合は特殊ケースとして処理)

\(\displaystyle g(x) = \min\left(h(x), \frac{h(N) - h(1)}{N-1}(x-1) + h(1)\right)\)

2回目の三分探索では 区間\([x_0+1, x_0+N-1\)] 上の関数\(h\)で \(x_1\) を求める。

この最大値、最小値を求めた後、交点を含む辺を見つけるために \(h(i)\) の符号が変化する箇所を二分探索で求める。

計算量

\(O(\log N)\)

実装

凸多角形の頂点は反時計回りのリストとして持つとする。

def find_zero(x0, x1, f):
    v0 = f(x0)
    if v0 == 0:
        return x0+1
    left = x0; right = x1+1
    while left+1 < right:
        mid = (left + right) >> 1
        if v0 * f(mid) >= 0:
            left = mid
        else:
            right = mid
    return right

def binary_search(f, L):
    left = 0; right = L
    while left+1 < right:
        mid = (left + right) >> 1
        if f(mid) < 0:
            left = mid
        else:
            right = mid
    return right

def line_polygon_intersection(p0, p1, qs):
    x0, y0 = p0; x1, y1 = p1
    dx = x1 - x0; dy = y1 - y0
    h = lambda p: (p[0] - x0)*dy - (p[1] - y0)*dx
    L = len(qs)

    i0 = i1 = -1

    v0 = h(qs[0])
    v1 = h(qs[L-1])
    if v0 == v1:
        v2 = h(qs[1])
        # assert v0 != v2
        if v0 < v2:
            i0 = L-1
        else:
            i1 = L-1
    else:
        v2 = h(qs[1])
        if v1 > v0 <= v2:
            i0 = 0
        elif v1 < v0 >= v2:
            i1 = 0
        else:
            g = lambda x: min((v1 - v0)*x/(L-1) + v0, h(qs[x]))
            i0 = binary_search(lambda x: g(x+1) - g(x), L-1)

    if i1 == -1:
        B = i0 - L
        k = binary_search(lambda x: h(qs[B+x]) - h(qs[B+x+1]), L)
        i1 = (i0 + k) % L
    else:
        B = i1 - L
        k = binary_search(lambda x: h(qs[B+x+1]) - h(qs[B+x]), L)
        i0 = (i1 + k) % L

    if h(qs[i0]) * h(qs[i1]) > 0:
        # a line and a polygon are disjoint
        return []

    # a vertex to the left side of a line: i0
    # a vertex to the right side of a line: i1

    f = lambda i: h(qs[i-L])
    k0 = find_zero(i1, i0 if i1 < i0 else i0+L, f) % L
    k1 = find_zero(i0, i1 if i0 < i1 else i1+L, f) % L
    # vertices to the left side of a line: k0, k0+1, ..., k1-2, k1-1
    # vertices to the right side of a line: k1, k1+1, ..., k0-2, k0-1
    if k0 == k1:
        return [k0]
    return [k0, k1]

# e_i is a line segment between v_{i-1} and v_i

p0, p1 = (0, 0), (1, 1)
qs = [(0, 2), (2, 0), (4, 0), (4, 2), (2, 4), (0, 4)]
print(line_polygon_intersection(p0, p1, qs))
# => "[4, 1]": cross points is on either e_4 or e_1

p0, p1 = (0, -1), (4, 0)
qs = [(0, 2), (2, 0), (4, 0), (4, 2), (2, 4), (0, 4)]
# => "[3]": cross point is on e_3

参考

  • Chazelle, Bernard, and David P. Dobkin. "Intersection of convex objects in two and three dimensions." Journal of the ACM (JACM) 34.1 (1987): 1-27.