Welcome, guest | Sign In | My Account | Store | Cart
#!/usr/bin/env python3

'''Class to compute if a point lies inside/outside/on-side of a polygon.

This is a Python 3 implementation of the Sloan's improved version of the
Nordbeck and Rystedt algorithm, published in the paper:

SLOAN, S.W. (1985): A point-in-polygon program.
    Adv. Eng. Software, Vol 7, No. 1, pp 45-47.

This class has 1 method (is_inside) that returns the minimum distance to the
nearest point of the polygon:

If is_inside < 0 then point is outside the polygon.
If is_inside = 0 then point in on a side of the polygon.
If is_inside > 0 then point is inside the polygon.

'''

import numpy as np


def _det(xvert, yvert):
    '''Compute twice the area of the triangle defined by points with using
    determinant formula.

    Input parameters:

    xvert -- A vector of nodal x-coords (array-like).
    yvert -- A vector of nodal y-coords (array-like).

    Output parameters:

    Twice the area of the triangle defined by the points.

    Notes:

    _det is positive if points define polygon in anticlockwise order.
    _det is negative if points define polygon in clockwise order.
    _det is zero if at least two of the points are concident or if
        all points are collinear.

    '''
    xvert = np.asfarray(xvert)
    yvert = np.asfarray(yvert)
    x_prev = np.concatenate(([xvert[-1]], xvert[:-1]))
    y_prev = np.concatenate(([yvert[-1]], yvert[:-1]))
    return np.sum(yvert * x_prev - xvert * y_prev, axis=0)


class Polygon:
    '''Polygon object.

    Input parameters:

    x -- A sequence of nodal x-coords.
    y -- A sequence of nodal y-coords.

    '''

    def __init__(self, x, y):
        if len(x) != len(y):
            raise IndexError('x and y must be equally sized.')
        self.x = np.asfarray(x)
        self.y = np.asfarray(y)
        # Closes the polygon if were open
        x1, y1 = x[0], y[0]
        xn, yn = x[-1], y[-1]
        if x1 != xn or y1 != yn:
            self.x = np.concatenate((self.x, [x1]))
            self.y = np.concatenate((self.y, [y1]))
        # Anti-clockwise coordinates
        if _det(self.x, self.y) < 0:
            self.x = self.x[::-1]
            self.y = self.y[::-1]

    def is_inside(self, xpoint, ypoint, smalld=1e-12):
        '''Check if point is inside a general polygon.

        Input parameters:

        xpoint -- The x-coord of the point to be tested.
        ypoint -- The y-coords of the point to be tested.
        smalld -- A small float number.

        xpoint and ypoint could be scalars or array-like sequences.

        Output parameters:

        mindst -- The distance from the point to the nearest point of the
                  polygon.
                  If mindst < 0 then point is outside the polygon.
                  If mindst = 0 then point in on a side of the polygon.
                  If mindst > 0 then point is inside the polygon.

        Notes:

        An improved version of the algorithm of Nordbeck and Rydstedt.

        REF: SLOAN, S.W. (1985): A point-in-polygon program. Adv. Eng.
             Software, Vol 7, No. 1, pp 45-47.

        '''
        xpoint = np.asfarray(xpoint)
        ypoint = np.asfarray(ypoint)
        # Scalar to array
        if xpoint.shape is tuple():
            xpoint = np.array([xpoint], dtype=float)
            ypoint = np.array([ypoint], dtype=float)
            scalar = True
        else:
            scalar = False
        # Check consistency
        if xpoint.shape != ypoint.shape:
            raise IndexError('x and y has different shapes')
        # If snear = True: Dist to nearest side < nearest vertex
        # If snear = False: Dist to nearest vertex < nearest side
        snear = np.ma.masked_all(xpoint.shape, dtype=bool)
        # Initialize arrays
        mindst = np.ones_like(xpoint, dtype=float) * np.inf
        j = np.ma.masked_all(xpoint.shape, dtype=int)
        x = self.x
        y = self.y
        n = len(x) - 1  # Number of sides/vertices defining the polygon
        # Loop over each side defining polygon
        for i in range(n):
            d = np.ones_like(xpoint, dtype=float) * np.inf
            # Start of side has coords (x1, y1)
            # End of side has coords (x2, y2)
            # Point has coords (xpoint, ypoint)
            x1 = x[i]
            y1 = y[i]
            x21 = x[i + 1] - x1
            y21 = y[i + 1] - y1
            x1p = x1 - xpoint
            y1p = y1 - ypoint
            # Points on infinite line defined by
            #     x = x1 + t * (x1 - x2)
            #     y = y1 + t * (y1 - y2)
            # where
            #     t = 0    at (x1, y1)
            #     t = 1    at (x2, y2)
            # Find where normal passing through (xpoint, ypoint) intersects
            # infinite line
            t = -(x1p * x21 + y1p * y21) / (x21 ** 2 + y21 ** 2)
            tlt0 = t < 0
            tle1 = (0 <= t) & (t <= 1)
            # Normal intersects side
            d[tle1] = ((x1p[tle1] + t[tle1] * x21) ** 2 +
                       (y1p[tle1] + t[tle1] * y21) ** 2)
            # Normal does not intersects side
            # Point is closest to vertex (x1, y1)
            # Compute square of distance to this vertex
            d[tlt0] = x1p[tlt0] ** 2 + y1p[tlt0] ** 2
            # Store distances
            mask = d < mindst
            mindst[mask] = d[mask]
            j[mask] = i
            # Point is closer to (x1, y1) than any other vertex or side
            snear[mask & tlt0] = False
            # Point is closer to this side than to any other side or vertex
            snear[mask & tle1] = True
        if np.ma.count(snear) != snear.size:
            raise IndexError('Error computing distances')
        mindst **= 0.5
        # Point is closer to its nearest vertex than its nearest side, check if
        # nearest vertex is concave.
        # If the nearest vertex is concave then point is inside the polygon,
        # else the point is outside the polygon.
        jo = j.copy()
        jo[j == 0] -= 1
        area = _det([x[j + 1], x[j], x[jo - 1]], [y[j + 1], y[j], y[jo - 1]])
        mindst[~snear] = np.copysign(mindst, area)[~snear]
        # Point is closer to its nearest side than to its nearest vertex, check
        # if point is to left or right of this side.
        # If point is to left of side it is inside polygon, else point is
        # outside polygon.
        area = _det([x[j], x[j + 1], xpoint], [y[j], y[j + 1], ypoint])
        mindst[snear] = np.copysign(mindst, area)[snear]
        # Point is on side of polygon
        mindst[np.fabs(mindst) < smalld] = 0
        # If input values were scalar then the output should be too
        if scalar:
            mindst = float(mindst)
        return mindst


# TEST

if __name__ == '__main__':
    # Define a triangle
    xvert = [10, 90, 90]
    yvert = [10, 90, 10]
    poly = Polygon(xvert, yvert)
    # Test
    x = np.arange(101)
    xx, yy = np.meshgrid(x, x)
    grid = poly.is_inside(xx, yy)
    # Plot
    try:
        import matplotlib.pyplot as plt
    except ImportError:
        print(grid)
    else:
        plt.contour(xx, yy, grid, levels=np.linspace(-100, 100, 51), colors='k')
        plt.pcolor(xx, yy, grid, vmin=-80, vmax=80, cmap=plt.get_cmap('RdBu'))
        plt.colorbar()
        plt.title('Distance to polygon border')
        plt.show(block=True)

Diff to Previous Revision

--- revision 1 2012-12-16 12:09:36
+++ revision 2 2014-04-24 18:13:34
@@ -8,8 +8,8 @@
 SLOAN, S.W. (1985): A point-in-polygon program.
     Adv. Eng. Software, Vol 7, No. 1, pp 45-47.
 
-This class has 1 public method (is_inside) that returns the minimum distance to
-the nearest point of the polygon:
+This class has 1 method (is_inside) that returns the minimum distance to the
+nearest point of the polygon:
 
 If is_inside < 0 then point is outside the polygon.
 If is_inside = 0 then point in on a side of the polygon.
@@ -17,9 +17,35 @@
 
 '''
 
-import math
-
 import numpy as np
+
+
+def _det(xvert, yvert):
+    '''Compute twice the area of the triangle defined by points with using
+    determinant formula.
+
+    Input parameters:
+
+    xvert -- A vector of nodal x-coords (array-like).
+    yvert -- A vector of nodal y-coords (array-like).
+
+    Output parameters:
+
+    Twice the area of the triangle defined by the points.
+
+    Notes:
+
+    _det is positive if points define polygon in anticlockwise order.
+    _det is negative if points define polygon in clockwise order.
+    _det is zero if at least two of the points are concident or if
+        all points are collinear.
+
+    '''
+    xvert = np.asfarray(xvert)
+    yvert = np.asfarray(yvert)
+    x_prev = np.concatenate(([xvert[-1]], xvert[:-1]))
+    y_prev = np.concatenate(([yvert[-1]], yvert[:-1]))
+    return np.sum(yvert * x_prev - xvert * y_prev, axis=0)
 
 
 class Polygon:
@@ -37,53 +63,27 @@
             raise IndexError('x and y must be equally sized.')
         self.x = np.asfarray(x)
         self.y = np.asfarray(y)
-        # Closes the polygon if needed
+        # Closes the polygon if were open
         x1, y1 = x[0], y[0]
         xn, yn = x[-1], y[-1]
         if x1 != xn or y1 != yn:
             self.x = np.concatenate((self.x, [x1]))
             self.y = np.concatenate((self.y, [y1]))
-        # Transform to anti-clockwise if needed
-        if self._det(self.x, self.y) < 0:
+        # Anti-clockwise coordinates
+        if _det(self.x, self.y) < 0:
             self.x = self.x[::-1]
             self.y = self.y[::-1]
 
-    @staticmethod
-    def _det(xvert, yvert):
-        '''Compute twice the area of the triangle defined by points with using
-        determinant formula.
-
-        Input parameters:
-
-        xvert -- A vector of nodal x-coords.
-        yvert -- A vector of nodal y-coords.
-
-        Output parameters:
-
-        Twice the area of the triangle defined by the points.
-
-        Notes:
-
-        _det is positive if points define polygon in anticlockwise order.
-        _det is negative if points define polygon in clockwise order.
-        _det is zero if at least two of the points are concident or if
-            all points are collinear.
-
-        '''
-        xvert = np.asfarray(xvert)
-        yvert = np.asfarray(yvert)
-        x_prev = np.concatenate(([xvert[-1]], xvert[:-1]))
-        y_prev = np.concatenate(([yvert[-1]], yvert[:-1]))
-        return np.sum(yvert * x_prev - xvert * y_prev)
-
     def is_inside(self, xpoint, ypoint, smalld=1e-12):
         '''Check if point is inside a general polygon.
 
         Input parameters:
 
         xpoint -- The x-coord of the point to be tested.
-        ypoint -- The y-coord of the point to be tested.
+        ypoint -- The y-coords of the point to be tested.
         smalld -- A small float number.
+
+        xpoint and ypoint could be scalars or array-like sequences.
 
         Output parameters:
 
@@ -101,14 +101,30 @@
              Software, Vol 7, No. 1, pp 45-47.
 
         '''
+        xpoint = np.asfarray(xpoint)
+        ypoint = np.asfarray(ypoint)
+        # Scalar to array
+        if xpoint.shape is tuple():
+            xpoint = np.array([xpoint], dtype=float)
+            ypoint = np.array([ypoint], dtype=float)
+            scalar = True
+        else:
+            scalar = False
+        # Check consistency
+        if xpoint.shape != ypoint.shape:
+            raise IndexError('x and y has different shapes')
         # If snear = True: Dist to nearest side < nearest vertex
         # If snear = False: Dist to nearest vertex < nearest side
+        snear = np.ma.masked_all(xpoint.shape, dtype=bool)
+        # Initialize arrays
+        mindst = np.ones_like(xpoint, dtype=float) * np.inf
+        j = np.ma.masked_all(xpoint.shape, dtype=int)
         x = self.x
         y = self.y
         n = len(x) - 1  # Number of sides/vertices defining the polygon
-        mindst = None
         # Loop over each side defining polygon
         for i in range(n):
+            d = np.ones_like(xpoint, dtype=float) * np.inf
             # Start of side has coords (x1, y1)
             # End of side has coords (x2, y2)
             # Point has coords (xpoint, ypoint)
@@ -127,50 +143,45 @@
             # Find where normal passing through (xpoint, ypoint) intersects
             # infinite line
             t = -(x1p * x21 + y1p * y21) / (x21 ** 2 + y21 ** 2)
-            if t < 0:
-                # Normal does not intersects side
-                # Point is closest to vertex (x1, y1)
-                # Compute square of distance to this vertex
-                d = x1p ** 2 + y1p ** 2
-                if mindst is None or d < mindst:
-                    # Point is closer to (x1, y1) than any other vertex or side
-                    snear = False
-                    mindst = d
-                    j = i
-            elif t <= 1:
-                # Normal intersects side
-                dx = x1p + t * x21
-                dy = y1p + t * y21
-                d = dx ** 2 + dy ** 2
-                if mindst is None or d < mindst:
-                    # Point is closer to this side than to any other side or
-                    # vertex
-                    snear = True
-                    mindst = d
-                    j = i
+            tlt0 = t < 0
+            tle1 = (0 <= t) & (t <= 1)
+            # Normal intersects side
+            d[tle1] = ((x1p[tle1] + t[tle1] * x21) ** 2 +
+                       (y1p[tle1] + t[tle1] * y21) ** 2)
+            # Normal does not intersects side
+            # Point is closest to vertex (x1, y1)
+            # Compute square of distance to this vertex
+            d[tlt0] = x1p[tlt0] ** 2 + y1p[tlt0] ** 2
+            # Store distances
+            mask = d < mindst
+            mindst[mask] = d[mask]
+            j[mask] = i
+            # Point is closer to (x1, y1) than any other vertex or side
+            snear[mask & tlt0] = False
+            # Point is closer to this side than to any other side or vertex
+            snear[mask & tle1] = True
+        if np.ma.count(snear) != snear.size:
+            raise IndexError('Error computing distances')
         mindst **= 0.5
-        if mindst < smalld:
-            # Point is on side of polygon
-            mindst = 0
-        elif snear:
-            # Point is closer to its nearest side than to its nearest vertex,
-            # check if point is to left or right of this side.
-            # If point is to left of side it is inside polygon, else point is
-            # outside polygon.
-            area = self._det([x[j], x[j + 1], xpoint],
-                             [y[j], y[j + 1], ypoint])
-            mindst = math.copysign(mindst, area)
-        else:
-            # Point is closer to its nearest vertex than its nearest side,
-            # check if nearest vertex is concave.
-            # If the nearest vertex is concave then point is inside the
-            # polygon, else the point is outside the polygon.
-            if not j:
-                x = x[:-1]
-                y = y[:-1]
-            area = self._det([x[j + 1], x[j], x[j - 1]],
-                             [y[j + 1], y[j], y[j - 1]])
-            mindst = math.copysign(mindst, area)
+        # Point is closer to its nearest vertex than its nearest side, check if
+        # nearest vertex is concave.
+        # If the nearest vertex is concave then point is inside the polygon,
+        # else the point is outside the polygon.
+        jo = j.copy()
+        jo[j == 0] -= 1
+        area = _det([x[j + 1], x[j], x[jo - 1]], [y[j + 1], y[j], y[jo - 1]])
+        mindst[~snear] = np.copysign(mindst, area)[~snear]
+        # Point is closer to its nearest side than to its nearest vertex, check
+        # if point is to left or right of this side.
+        # If point is to left of side it is inside polygon, else point is
+        # outside polygon.
+        area = _det([x[j], x[j + 1], xpoint], [y[j], y[j + 1], ypoint])
+        mindst[snear] = np.copysign(mindst, area)[snear]
+        # Point is on side of polygon
+        mindst[np.fabs(mindst) < smalld] = 0
+        # If input values were scalar then the output should be too
+        if scalar:
+            mindst = float(mindst)
         return mindst
 
 
@@ -178,14 +189,21 @@
 
 if __name__ == '__main__':
     # Define a triangle
-    xvert = [1, 10, 10]
-    yvert = [1, 10, 1]
+    xvert = [10, 90, 90]
+    yvert = [10, 90, 10]
     poly = Polygon(xvert, yvert)
-    # Test the function in every point of a 12x12 grid
-    grid = np.zeros((12, 12), dtype=int)
-    for y in range(12):
-        for x in range(12):
-            if poly.is_inside(x, y) >= 0:
-                grid[y, x] = 1
-    # Print the result (0=outside)
-    print(grid)
+    # Test
+    x = np.arange(101)
+    xx, yy = np.meshgrid(x, x)
+    grid = poly.is_inside(xx, yy)
+    # Plot
+    try:
+        import matplotlib.pyplot as plt
+    except ImportError:
+        print(grid)
+    else:
+        plt.contour(xx, yy, grid, levels=np.linspace(-100, 100, 51), colors='k')
+        plt.pcolor(xx, yy, grid, vmin=-80, vmax=80, cmap=plt.get_cmap('RdBu'))
+        plt.colorbar()
+        plt.title('Distance to polygon border')
+        plt.show(block=True)

History