This is a python version to translate W. Randolph Franklin's work. And recorded for further usage.

class PNPoly(object):
    """
    PNPOLY - Point Inclusion in Polygon Test
    W. Randolph Franklin (WRF)
 
    See: https://wrfranklin.org/Research/Short_Notes/pnpoly.html
    """
 
    def __init__(self, vertices) -> None:
        self.vertices = vertices
        assert len(vertices) >= 3, "vertices must be >= 3"
        self.min_x, self.max_x, self.min_y, self.max_y = self._make_border()
 
    def _make_border(self):
        vertices = self.vertices
        min_x, min_y = vertices[0]
        max_x, max_y = vertices[0]
        for i in range(1, len(vertices)):
            x, y = vertices[i]
            min_x = min(min_x, x)
            min_y = min(min_y, y)
            max_x = max(max_x, x)
            max_y = max(max_y, y)
        return min_x, max_x, min_y, max_y
 
    def __call__(self, x, y):
        return self.is_inside(x, y)
 
    def is_inside(self, x, y):
        """given a polygon and a point p, returns True if p is inside the polygon, False otherwise
 
        Args:
            x (int): x coordinate of the point
            y (int): y coordinate of the point
        """
        if x < self.min_x or x > self.max_x or y < self.min_y or y > self.max_y:
            return False
        vert = self.vertices
        nvert = len(vert)
        i = 0
        j = nvert - 1
        c = False
        for i in range(nvert):
            if ((vert[i][1] > y) != (vert[j][1] > y)) and (x < (vert[j][0] - vert[i][0]) * (y - vert[i][1]) / (vert[j][1] - vert[i][1]) + vert[i][0]):
                c = not c
            j = i
        return c

Simple Test:

def test():
    vertices = [(0,0), (2,0), (2,1), (1,1), (1,2), (2,2), (2,3), (0,3)]
    pnpoly = PNPoly(vertices)
    cases = [
        (0,0, True),
        (1.5, 0.5, True),
        (1.5, 1.5, False),
        (1.5, 2.5, True),
        (1.5, 3.5, False),
    ]
    for x, y, expected in cases:
        assert pnpoly(x, y) == expected, "x: {}, y: {}, expected: {}".format(x, y, expected)
        print("OK: x: {}, y: {}, expected: {}".format(x, y, expected))
 
test()

Let's try to optimize it using numpy

import numpy as np

class PNPoly2(object):
    def __init__(self, vertices) -> None:
        assert len(vertices) >= 3, "vertices must be >= 3"
        self.vert = np.array(vertices)
        self.min_x, self.max_x, self.min_y, self.max_y = self._make_border()

    def _make_border(self):
        min_x, min_y = np.min(self.vert, axis=0)
        max_x, max_y = np.max(self.vert, axis=0)
        return min_x, max_x, min_y, max_y

    def __call__(self, x, y):
        return self.is_inside(x, y)

    def is_inside(self, x, y):
        """given a polygon and a point p, returns True if p is inside the polygon, False otherwise

        Args:
            x (int): x coordinate of the point
            y (int): y coordinate of the point
        """
        if x < self.min_x or x > self.max_x or y < self.min_y or y > self.max_y:
            return False
        # vert = np.array(self.vertices)
        vert = self.vert
        nvert = len(vert)
        i = np.arange(nvert)
        j = np.concatenate((i[1:], [0]))
        c = np.zeros(nvert, dtype=int)
        c[((vert[i, 1] > y) != (vert[j, 1] > y)) & (x < (vert[j, 0] - vert[i, 0]) * (y - vert[i, 1]) / (vert[j, 1] - vert[i, 1]) + vert[i, 0])] = 1
        return c.sum() % 2 == 1

According to the test, in the small size of the vertices, the overhead is too much ( np.arrange ... blah operation). I have to generate much more vertices to let the average performance in numpy get better.

here is the test code:


def make_random_cases(seed):
    import random

    p = PNPoly(seed)
    min_x, max_x, min_y, max_y = p._make_border()
    rand_min_x = min_x - (max_x - min_x) * 0.5
    rand_max_x = max_x + (max_x - min_x) * 0.5
    rand_min_y = min_y - (max_y - min_y) * 0.5
    rand_max_y = max_y + (max_y - min_y) * 0.5
    for i in range(1000):
        x = random.uniform(rand_min_x, rand_max_x)
        y = random.uniform(rand_min_y, rand_max_y)
        expected = p.is_inside(x, y)
        yield x, y, expected


def add_mid_point(vertices):
    new_vertices = []
    for i in range(len(vertices)):
        vi = vertices[i]
        new_vertices.append(vi)
        if i == len(vertices) - 1:
            break
        vj = vertices[i + 1]
        if vi[0] == vj[0]:
            new_vertices.append((vi[0], (vi[1] + vj[1]) / 2))
        elif vi[1] == vj[1]:
            new_vertices.append(((vi[0] + vj[0]) / 2, vi[1]))
        else:
            new_vertices.append(((vi[0] + vj[0]) / 2, (vi[1] + vj[1]) / 2))
    return new_vertices


def test(n_more_verts):
    vertices = [(0, 0), (2, 0), (2, 1), (1, 1), (1, 2), (2, 2), (2, 3), (0, 3)]
    for i in range(n_more_verts):
        vertices = add_mid_point(vertices)
    print("len(vertices): {}".format(len(vertices)))
    test_cases = []
    for x, y, expected in make_random_cases(vertices):
        test_cases.append((x, y, expected))

    def make_test(pnpoly):
        for x, y, expected in test_cases:
            result = pnpoly(x, y)
            assert result == expected, "x: {}, y: {}, expected: {}, actually: {}".format(x, y, expected, result)

    time_in = time.time()
    for i in range(1000):
        pnpoly = PNPoly(vertices)
        make_test(pnpoly)
    time_out = time.time()
    print("PNPoly: {}s".format(time_out - time_in))

    time_in = time.time()
    for i in range(1000):
        pnpoly = PNPoly2(vertices)
        make_test(pnpoly)
    time_out = time.time()
    print("PNPoly2: {}s".format(time_out - time_in))


test(5)
test(9)

here is the test output

len(vertices): 225
PNPoly: 7.810054063796997s
/data/yu/aigc/pipe/monet/monet/mask_process/pnploy.py:85: RuntimeWarning: divide by zero encountered in divide
  c[((vert[i, 1] > y) != (vert[j, 1] > y)) & (x < (vert[j, 0] - vert[i, 0]) * (y - vert[i, 1]) / (vert[j, 1] - vert[i, 1]) + vert[i, 0])] = 1
PNPoly2: 10.531755924224854s
len(vertices): 3585
PNPoly: 127.7502794265747s
/data/yu/aigc/pipe/monet/monet/mask_process/pnploy.py:85: RuntimeWarning: divide by zero encountered in divide
  c[((vert[i, 1] > y) != (vert[j, 1] > y)) & (x < (vert[j, 0] - vert[i, 0]) * (y - vert[i, 1]) / (vert[j, 1] - vert[i, 1]) + vert[i, 0])] = 1
PNPoly2: 34.64983057975769s
Categories: Code

Yu

Ideals are like the stars: we never reach them, but like the mariners of the sea, we chart our course by them.

Leave a Reply

Your email address will not be published. Required fields are marked *