# Created on Wed Sep 24 15:05:28 2014
# Author: XiaoTao Wang
# Organization: HuaZhong Agricultural University
from __future__ import division
import numpy as np
from scipy.spatial import ConvexHull
[docs]class Polygon(ConvexHull):
"""Main class for polygon creation, manipulation and calulation.
The API is inherited from :class:`scipy.spatial.ConvexHull`, which
constructs convex hull from a point set. So you should input coordinates
of a point set to initiate an instance.
More general, not all points are required. Vertices alone are enough
to construct the convex hull.
Also, this class is designed to operate on 2-D space, although
**ConvexHull** is okay for higher dimensions.
Parameters
----------
points : array_like
Coordinates of points to construct a convex hull from. All objects
that can be converted to a ndarray shaped (npoints, 2) are okay.
Attributes
----------
Common attributes from **ConvexHull**:
points : ndarray of double, shape (npoints, 2)
Coordinates of input points. Converted ndarray.
vertices : ndarray of ints, shape (nvertices,)
Indices of points forming the vertices of the convex hull. The
vertices are counter-clockwise ordered.
simplices : ndarray of ints, shape (nfacet, 2)
Indices of points forming the simplical facets of the convex hull.
Customized attributes:
anchors : ndarray of double, shape (nvertices,)
Vertices of the convex hull, counter-clockwise ordered.
area : float
Area of the polygon.
Examples
--------
Convex hull of a random set of points:
>>> import numpy as np
>>> from tadlib.calfea.polygon import Polygon
>>> points = np.random.rand(20, 2) # 20 random points in 2-D space
>>> P = Polygon(points)
>>> print P.anchors
[[ 0.71119071 0.216714 ]
[ 0.92908323 0.77903127]
[ 0.79530032 0.93279735]
[ 0.02160083 0.98758203]
[ 0.09094998 0.4423167 ]
[ 0.16924963 0.05900692]
[ 0.71119071 0.216714 ]]
"""
def __init__(self, points):
"""A customized constructor.
"""
ConvexHull.__init__(self, points)
indices = self.vertices
points = self.points
# Customize
self.anchors = points[indices]
[docs] def calarea(self):
"""Calculate the polygon area.
An attribute called **area** is assigned.
See Also
--------
tadlib.calfea.polygon.shoelace : Twice the area of polygon
"""
# Our Calculation
# Call external function
self.area = abs(shoelace(self.anchors)) / 2.0
[docs] def close(self):
"""
Close the polygon, i.e., the first point is also the last one.
See Also
--------
tadlib.calfea.polygon.isinside : judge if points are inside a polygon
or not.
Notes
-----
Must be called before :py:meth:`tadlib.calfea.polygon.Polygon.isinside`.
"""
first = self.anchors[0]
last = self.anchors[-1]
if not np.all(first==last):
# Concatenate along first axis, dim>=2
self.anchors = np.r_['0,2', self.anchors, self.anchors[0]]
[docs] def isinside(self, query, zerolike=1e-12):
"""Compute point location relative to a polygon.
Parameters
----------
query : tuple or array_like
A tuple indicates one point. (The length must be 2)
Or you can input an array-like sequence. Any object that can be
converted to a ndarray shaped (npoints, 2) is okay.
zerolike : float
A number used to approximate 0.
Returns
-------
mindst : scalar or array_like
If mindst < 0, point is outside the polygon.
If mindst = 0, point in on a side of the polygon.
If mindst > 0, point is inside the polygon.
Notes
-----
Sloan's improved version of the Nordbeck and Rydstedt algorithm.
Examples
--------
We start with :class:`tadlib.calfea.polygon.Polygon` construction:
>>> import numpy as np
>>> from tadlib.calfea.polygon import Polygon
>>> points = np.random.rand(20, 2) # Used for constructing Polygon
>>> P = Polygon(points)
>>> check = np.random.rand(3, 2) # Another 3 random points
>>> P.isinside(check)
array([ 0.21145311, 0.09807244, -0.15341914])
"""
# Indicator
label = type(query)==tuple
# Unify the interface
query = np.array(query, ndmin=2)
# Close the polygon
self.close()
# If snear = True, dist to nearest side < nearest vertex
# If snear = False, dist to nearest vertex < nearest side
snear = np.ma.masked_all(query.shape[0], dtype=bool)
# Initialization
mindst = np.ones(query.shape[0], dtype=float) * np.inf
j = np.ma.masked_all(query.shape[0], dtype=int)
# Number of sides/vertices defining the polygon
n = len(self.anchors) - 1
# Loop over each side of the polygon
for i in range(n):
d = np.ones(query.shape[0], dtype=float) * np.inf
# Vertex (x1, y1), start of side
start = self.anchors[i]
# Vertex (x2, y2), end of side
sec = self.anchors[i + 1] - start
# Query has coordinates (qx, qy)
devs = self.anchors[i] - query
# Points on infinite line defined by
# x = x1 + t * (x1 - x2)
# y = y1 + t * (y1 - y2)
# Find where normal passing through (qx, qy) intersects
# infinite line
t = -(devs[:,0] * sec[0] + devs[:,1] * sec[1]) / \
(sec[0] ** 2 + sec[1] ** 2)
tlt0 = t < 0
tle1 = (0 <= t) & (t <= 1)
# Normal intersects side
d[tle1] = ((devs[:,0][tle1] + t[tle1] * sec[0]) ** 2 +
(devs[:,1][tle1] + t[tle1] * sec[1]) ** 2)
# Normal does not intersects side
# Point is closest to vertex (x1, y1)
# Compute square of distance to this vertex
d[tlt0] = devs[:,0][tlt0] ** 2 + devs[:,1][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 when 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 = shoelace([self.anchors[j + 1], self.anchors[j],
self.anchors[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 = shoelace([self.anchors[j], self.anchors[j + 1], query])
mindst[snear] = np.copysign(mindst, area)[snear]
# Point is on side of polygon
mindst[np.fabs(mindst) < zerolike] = 0
# If input values were scalar then the output should be too
if label:
mindst = float(mindst)
return mindst
[docs]def shoelace(vertices):
"""
Calculate twice the area of polygon using Shoelace formula.
Polygon is defined by vertices.
Parameters
----------
vertices : array_like
Vertex coordinates in a 2-D space.
Coordinates must be placed along the last axis. And data points are
along the first axis.
Returns
-------
area : float
You can deduce the order of input vertices from the sign:
area is positive if vertices are in counter-clockwise order.
area is negative if vertices are in clockwise order.
area is zero if all points are colinear.
Notes
-----
This function can be also used to judge if all points in a data set are
collinear. Collinear points as input for initializing Polygon instance
will raise a QhullError.
Examples
--------
Vertices of a square:
Clockwise:
>>> from tadlib.calfea.polygon import shoelace
>>> sq = [(0,0), (0,1), (1,1), (1,0)]
>>> shoelace(sq)
-2.0
Counter-clockwise:
>>> sq = [(0,0), (1,0), (1,1), (0,1)]
>>> shoelace(sq)
2.0
"""
vertices = np.asfarray(vertices)
# Rule for stacking multiple comma separated arrays
rule = '0,' + str(len(vertices.shape))
# Slip the array along the first axis
slip_v = np.r_[rule, vertices[-1], vertices[:-1]]
# Extract coordinates
x = np.take(vertices, [0], axis=-1).reshape(vertices.shape[:-1])
y = np.take(vertices, [1], axis=-1).reshape(vertices.shape[:-1])
slip_x = np.take(slip_v, [0], axis=-1).reshape(vertices.shape[:-1])
slip_y = np.take(slip_v, [1], axis=-1).reshape(vertices.shape[:-1])
# Sholelace Foluma
area = np.sum(y * slip_x - x * slip_y, axis=0)
return area
[docs]def collinear(points):
"""Test whether all given points are collinear.
Collinear points will trigger an error called **QhullError** when used
to initialize a :class:`tadlib.calfea.polygon.Polygon` instance. However,
other conditions may also trigger **QhullError**. Doing this test in
advance can avoid this error and make things clearer.
Parameters
----------
points : array_like
Coordinates of points to construct a polygon. Any object that can
be converted to a ndarray shaped (npoints, 2) is okay.
Returns
-------
judge : bool
True if the input points are collinear else False.
See Also
--------
tadlib.calfea.polygon.shoelace
Notes
-----
To judge if all points are collinear, we use a simple heuristic
algorithm. We sort the points at first. Then test whether consecutive
triples are collinear.
Examples
--------
Trival but still effective:
>>> from tadlib.calfea.polygon import collinear
>>> line = [(2, 0.4), (2, 0.8), (2, 4), (2, 100)]
>>> collinear(line)
True
"""
if len(points)<3:
judge = True # Points are always collinear if N<3
else:
points = np.asfarray(points)
# Indices for sorting
idx = np.argsort(points, axis=0)
mask = points[:,0]==points[0][0]
if not np.all(mask):
asort = points[idx[:,0]]
else:
asort = points[idx[:,1]]
# The first point of the triples
first = np.arange(len(points)-2)
triples = [asort[first], asort[first + 1], asort[first + 2]]
# Calculate triple areas
areas = shoelace(triples)
# Triples are collinear if area=0
# If all sorted triples are collinear, then all points are collinear
judge = np.all(areas==0)
return judge