from dataclasses import dataclass
from typing import Any
import matplotlib.pyplot as plt
import numpy as np
from skimage import measure
[docs]@dataclass
class Polygon:
# List of coordinates describing a polygon.
points: np.ndarray
def __len__(self):
return len(self.points)
[docs] def find_point(self) -> np.ndarray:
"""Use rejection sampling to find point in polygon.
Returns
-------
point : numpy.ndarray
Coordinate of point in the polygon
"""
# start with guess in center of polygon
point = self.points.mean(axis=0)
while not self.contains_point(point):
xmin, ymin = self.points.min(axis=0)
xmax, ymax = self.points.max(axis=0)
point = np.random.uniform(xmin,
xmax), np.random.uniform(ymin, ymax)
return point
[docs] def close_corner(self, shape: tuple) -> 'Polygon':
"""Check if polygons are in the corner, and close them if needed.
Polygons which cover a corner cannot be closed by joining the first
and last element, because some of the area is missed. This algorithm
adds the corner point to close the polygons.
Parameters
----------
shape : tuple
Shape of the source image. Used to check which corners the
polygon touches.
Returns
-------
polygon : Polygon
Return a polygon with a corner point added if needed (n+1,2),
otherwise return the input polygon (n,2)
"""
xmin, ymin = self.points.min(axis=0)
xmax, ymax = self.points.max(axis=0)
xdim, ydim = np.array(shape) - 1
left = (xmin == 0)
right = (xmax == xdim)
bottom = (ymin == 0)
top = (ymax == ydim)
if bottom and left:
extra_point = (0, 0)
elif top and left:
extra_point = (0, ydim)
elif top and right:
extra_point = (xdim, ydim)
elif bottom and right:
extra_point = (xdim, 0)
else:
# all good
return self
points = np.vstack([self.points, extra_point])
return Polygon(points)
[docs] def subdivide(self, max_dist: int = 10, plot: bool = False) -> 'Polygon':
"""This algorithm looks for long edges in the polygon and subdivides
them so they are no longer than `max_dist`
Parameters
----------
max_dist : int, optional
Maximum distance between neighbouring coordinates.
plot : bool, optional
Show plot of the generated points.
Returns
-------
Polygon
Polygon with updated coordinate array.
"""
points = self.points
new_points: Any = []
rolled = np.roll(points, shift=-1, axis=0)
diffs = rolled - points
# ignore last point, do not wrap around
dist = np.linalg.norm(diffs[:-1], axis=1)
last_i = 0
for i in np.argwhere(dist > max_dist).reshape(-1, ):
new_points.append(points[last_i:i])
start = points[i]
stop = rolled[i]
to_add = int(dist[i] // max_dist)
interpolated = np.linspace(start, stop, to_add, endpoint=False)
new_points.append(interpolated)
last_i = i + 1
new_points.append(points[last_i:])
new_points = np.vstack(new_points)
if plot:
plt.scatter(*points.T[::-1], color='red', s=100, marker='x')
plt.plot(*points.T[::-1], color='red')
plt.scatter(*new_points.T[::-1], color='green', s=100, marker='+')
plt.plot(*new_points.T[::-1], color='green')
plt.axis('equal')
plt.show()
return Polygon(new_points)
[docs] def remove_duplicate_points(self) -> 'Polygon':
"""Remove duplicate points from polygon.
For a polygon it is implied that the last point connects to the
first point. In case the first point equals the last point, this
results in errors down the line.
Returns
-------
Polygon
"""
points = self.points
first = points[0]
last = points[-1]
if np.all(first == last):
points = points[:-1]
return Polygon(points)
[docs] def approximate(self, *args, **kwargs) -> 'Polygon':
"""Approximate polygon.
Parameters
----------
*args : list
Extra arguments padded to `skimage.measure.approximate_polygon`.
**kwargs
These parameters are passed to
`skimage.measure.approximate_polygon`.
Returns
-------
new_polygon : Polygon
"""
new_points = measure.approximate_polygon(self.points, *args, **kwargs)
return Polygon(new_points)
[docs] def contains_point(self, point: np.ndarray) -> bool:
"""Test whether point lies inside polygon.
Parameters
----------
point : (2,) numpy.ndarray
Point coordinates
Returns
-------
bool
True if corresponding point is inside the polygon
"""
return measure.points_in_poly([point], self.points)
[docs] def contains_points(self, points: np.ndarray) -> np.ndarray:
"""Test whether points lie inside polygon.
Parameters
----------
points : (n,2) numpy.ndarray
List of points
Returns
-------
mask : (n,) numpy.ndarray[bool]
Boolean array where true corresponds to points
lying inside the polygon
"""
return measure.points_in_poly(points, self.points)