import ubelt as ub
import cv2
import numpy as np
import skimage
import numbers
from . import _generic
try:
from xdev import profile
except Exception:
from ubelt import identity as profile
[docs]class _PolyArrayBackend:
[docs] def is_numpy(self):
return self._impl.is_numpy
[docs] def is_tensor(self):
return self._impl.is_tensor
[docs] def tensor(self, device=ub.NoParam):
"""
Example:
>>> # xdoctest: +REQUIRES(module:torch)
>>> from kwimage.structs.polygon import *
>>> self = Polygon.random()
>>> self.tensor()
"""
impl = self._impl
if True:
newdata = {}
for k, v in self.data.items():
if hasattr(v, 'tensor'):
v2 = v.tensor(device)
elif isinstance(v, list):
v2 = [x.tensor(device) for x in v]
else:
v2 = impl.tensor(v, device)
newdata[k] = v2
else:
newdata = {k: v.tensor(device) if hasattr(v, 'tensor')
else impl.tensor(v, device)
for k, v in self.data.items()}
new = self.__class__(newdata, self.meta)
return new
[docs] def numpy(self):
"""
Example:
>>> # xdoctest: +REQUIRES(module:torch)
>>> from kwimage.structs.polygon import *
>>> self = Polygon.random()
>>> self.tensor().numpy().tensor().numpy()
"""
impl = self._impl
if True:
newdata = {}
for k, v in self.data.items():
if hasattr(v, 'numpy'):
v2 = v.numpy()
elif isinstance(v, list):
v2 = [x.numpy() for x in v]
else:
v2 = impl.numpy(v)
newdata[k] = v2
else:
# newdata = {k: v.tensor(device) if hasattr(v, 'tensor')
newdata = {k: v.numpy() if hasattr(v, 'numpy') else impl.numpy(v)
for k, v in self.data.items()}
new = self.__class__(newdata, self.meta)
return new
[docs]class _PolyWarpMixin:
# @profile
[docs] def _warp_imgaug(self, augmenter, input_dims, inplace=False):
"""
Warps by applying an augmenter from the imgaug library
Args:
augmenter (imgaug.augmenters.Augmenter):
input_dims (Tuple): h/w of the input image
inplace (bool, default=False): if True, modifies data inplace
Example:
>>> # xdoctest: +REQUIRES(module:imgaug)
>>> from kwimage.structs.polygon import * # NOQA
>>> import imgaug
>>> input_dims = np.array((10, 10))
>>> self = Polygon.random(10, n_holes=1, rng=0).scale(input_dims)
>>> augmenter = imgaug.augmenters.Fliplr(p=1)
>>> new = self._warp_imgaug(augmenter, input_dims)
>>> assert np.allclose(self.data['exterior'].data[:, 1], new.data['exterior'].data[:, 1])
>>> assert np.allclose(input_dims[0] - self.data['exterior'].data[:, 0], new.data['exterior'].data[:, 0])
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> kwplot.figure(fnum=1, doclf=True)
>>> from matplotlib import pyplot as pl
>>> ax = plt.gca()
>>> ax.set_xlim(0, 10)
>>> ax.set_ylim(0, 10)
>>> self.draw(color='red', alpha=.4)
>>> new.draw(color='blue', alpha=.4)
"""
import kwimage
new = self if inplace else self.__class__(self.data.copy())
# current version of imgaug doesnt fully support polygons
# coerce to and from points instead
dtype = self.data['exterior'].data.dtype
parts = [self.data['exterior']] + self.data.get('interiors', [])
parts = [p.data for p in parts]
cs = [0] + np.cumsum(np.array(list(map(len, parts)))).tolist()
flat_kps = np.concatenate(parts, axis=0)
flat_coords = kwimage.Coords(flat_kps)
flat_coords = flat_coords._warp_imgaug(augmenter, input_dims, inplace=True)
flat_parts = flat_coords.data
new_parts = []
for a, b in ub.iter_window(cs, 2):
new_part = np.array(flat_parts[a:b], dtype=dtype)
new_parts.append(new_part)
new_exterior = kwimage.Coords(new_parts[0])
new_interiors = [kwimage.Coords(p) for p in new_parts[1:]]
new.data['exterior'] = new_exterior
new.data['interiors'] = new_interiors
return new
# @profile
[docs] def to_imgaug(self, shape):
import imgaug
ia_exterior = imgaug.Polygon(self.data['exterior'])
ia_interiors = [imgaug.Polygon(p) for p in self.data.get('interiors', [])]
iamp = imgaug.MultiPolygon([ia_exterior] + ia_interiors)
return iamp
# @profile
[docs] def warp(self, transform, input_dims=None, output_dims=None, inplace=False):
"""
Generalized coordinate transform.
Args:
transform (GeometricTransform | ArrayLike | Augmenter | callable):
scikit-image tranform, a 3x3 transformation matrix,
an imgaug Augmenter, or generic callable which transforms
an NxD ndarray.
input_dims (Tuple): shape of the image these objects correspond to
(only needed / used when transform is an imgaug augmenter)
output_dims (Tuple): unused, only exists for compatibility
inplace (bool, default=False): if True, modifies data inplace
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random()
>>> transform = skimage.transform.AffineTransform(scale=(2, 2))
>>> new = self.warp(transform)
Doctest:
>>> # xdoctest: +REQUIRES(module:imgaug)
>>> self = Polygon.random()
>>> import imgaug
>>> augmenter = imgaug.augmenters.Fliplr(p=1)
>>> new = self.warp(augmenter, input_dims=(1, 1))
>>> print('new = {!r}'.format(new.data))
>>> print('self = {!r}'.format(self.data))
>>> #assert np.all(self.warp(np.eye(3)).exterior == self.exterior)
>>> #assert np.all(self.warp(np.eye(2)).exterior == self.exterior)
"""
new = self if inplace else self.__class__(self.data.copy())
# print('WARP new = {!r}'.format(new))
if transform is None:
return new
elif not isinstance(transform, (np.ndarray, skimage.transform._geometric.GeometricTransform)):
try:
import imgaug
except ImportError:
pass
# import warnings
# warnings.warn('imgaug is not installed')
# raise TypeError(type(transform))
else:
if isinstance(transform, imgaug.augmenters.Augmenter):
return new._warp_imgaug(transform, input_dims, inplace=True)
# else:
# raise TypeError(type(transform))
new.data['exterior'] = new.data['exterior'].warp(transform, input_dims,
output_dims, inplace)
new.data['interiors'] = [
p.warp(transform, input_dims, output_dims, inplace)
for p in new.data['interiors']
]
return new
@profile
[docs] def scale(self, factor, about=None, output_dims=None, inplace=False):
"""
Scale a polygon by a factor
Args:
factor (float or Tuple[float, float]):
scale factor as either a scalar or a (sf_x, sf_y) tuple.
about (Tuple | None):
if unspecified scales about the origin (0, 0), otherwise the
rotation is about this point.
output_dims (Tuple): unused in non-raster spatial structures
inplace (bool, default=False): if True, modifies data inplace
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(10, rng=0)
>>> new = self.scale(10)
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(10, rng=0).translate((0.5))
>>> new = self.scale(1.5, about='center')
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.figure(fnum=1, doclf=True)
>>> kwplot.autompl()
>>> self.draw(color='red', alpha=0.5)
>>> new.draw(color='blue', alpha=0.5, setlim=True)
"""
new = self if inplace else self.__class__(self.data.copy())
about = self._rectify_about(about)
new.data['exterior'] = new.data['exterior'].scale(
factor, about=about, output_dims=output_dims, inplace=inplace)
new.data['interiors'] = [
p.scale(factor, about=about, output_dims=output_dims,
inplace=inplace)
for p in new.data['interiors']]
return new
@profile
[docs] def translate(self, offset, output_dims=None, inplace=False):
"""
Shift the polygon up/down left/right
Args:
factor (float or Tuple[float]):
transation amount as either a scalar or a (t_x, t_y) tuple.
output_dims (Tuple): unused in non-raster spatial structures
inplace (bool, default=False): if True, modifies data inplace
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(10, rng=0)
>>> new = self.translate(10)
"""
new = self if inplace else self.__class__(self.data.copy())
new.data['exterior'] = new.data['exterior'].translate(
offset, output_dims, inplace)
new.data['interiors'] = [p.translate(offset, output_dims, inplace)
for p in new.data['interiors']]
return new
@profile
[docs] def rotate(self, theta, about=None, output_dims=None, inplace=False):
"""
Rotate the polygon
Args:
theta (float):
rotation angle in radians
about (Tuple | None | str):
if unspecified rotates about the origin (0, 0). If "center"
then rotate around the center of this polygon. Otherwise the
rotation is about a custom specified point.
output_dims (Tuple): unused in non-raster spatial structures
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(10, rng=0)
>>> new = self.rotate(np.pi / 2, about='center')
>>> new2 = self.rotate(np.pi / 2)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.figure(fnum=1, doclf=True)
>>> kwplot.autompl()
>>> self.draw(color='red', alpha=0.5)
>>> new.draw(color='blue', alpha=0.5)
"""
new = self if inplace else self.__class__(self.data.copy())
about = self._rectify_about(about)
new.data['exterior'] = new.data['exterior'].rotate(
theta, about, output_dims, inplace)
new.data['interiors'] = [p.rotate(theta, about, output_dims, inplace)
for p in new.data['interiors']]
return new
[docs] def _rectify_about(self, about):
"""
Ensures that about returns a specified point. Allows for special keys
like center to be used.
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(10, rng=0)
>>> self._rectify_about('center')
"""
if about is None:
about_ = None
else:
if isinstance(about, str):
if about == 'origin':
about_ = (0., 0.)
elif about == 'center':
centroid = self.to_shapely().centroid
about_ = (centroid.x, centroid.y)
else:
raise KeyError(about)
else:
about_ = about if ub.iterable(about) else [about] * 2
return about_
[docs] def swap_axes(self, inplace=False):
"""
Swap the x and y coordinate axes
Args:
inplace (bool, default=False): if True, modifies data inplace
Returns:
Polygon: modified polygon
"""
new = self if inplace else self.__class__(self.data.copy())
new.data['exterior'] = new.data['exterior'].reorder_axes(
(1, 0), inplace=inplace)
new.data['interiors'] = [
p.reorder_axes((1, 0), inplace=inplace)
for p in new.data['interiors']]
return new
[docs]class Polygon(_generic.Spatial, _PolyArrayBackend, _PolyWarpMixin, ub.NiceRepr):
"""
Represents a single polygon as set of exterior boundary points and a list
of internal polygons representing holes.
By convention exterior boundaries should be counterclockwise and interior
holes should be clockwise.
Example:
>>> import kwimage
>>> data = {
>>> 'exterior': np.array([[13, 1], [13, 19], [25, 19], [25, 1]]),
>>> 'interiors': [
>>> np.array([[13, 13], [14, 12], [24, 12], [25, 13], [25, 18],
>>> [24, 19], [14, 19], [13, 18]]),
>>> np.array([[13, 2], [14, 1], [24, 1], [25, 2], [25, 11],
>>> [24, 12], [14, 12], [13, 11]])]
>>> }
>>> self = kwimage.Polygon(**data)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> self.draw(setlim=True)
Example:
>>> import kwimage
>>> self = kwimage.Polygon.random(
>>> n=5, n_holes=1, convex=False, rng=0)
>>> print('self = {}'.format(self))
self = <Polygon({
'exterior': <Coords(data=
array([[0.30371392, 0.97195856],
[0.24372304, 0.60568445],
[0.21408694, 0.34884262],
[0.5799477 , 0.44020379],
[0.83720288, 0.78367234]]))>,
'interiors': [<Coords(data=
array([[0.50164209, 0.83520279],
[0.25835064, 0.40313428],
[0.28778562, 0.74758761],
[0.30341266, 0.93748088]]))>],
})>
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> self.draw(setlim=True)
"""
[docs] __datakeys__ = ['exterior', 'interiors']
def __init__(self, data=None, meta=None, datakeys=None, metakeys=None, **kwargs):
if kwargs:
if data or meta:
raise ValueError('Cannot specify kwargs AND data/meta dicts')
_datakeys = self.__datakeys__
_metakeys = self.__metakeys__
# Allow the user to specify custom data and meta keys
if datakeys is not None:
_datakeys = _datakeys + list(datakeys)
if metakeys is not None:
_metakeys = _metakeys + list(metakeys)
# Perform input checks whenever kwargs is given
data = {key: kwargs.pop(key) for key in _datakeys if key in kwargs}
meta = {key: kwargs.pop(key) for key in _metakeys if key in kwargs}
if kwargs:
raise ValueError(
'Unknown kwargs: {}'.format(sorted(kwargs.keys())))
import kwimage
if 'exterior' in data:
if isinstance(data['exterior'], (list, tuple)):
data['exterior'] = kwimage.Coords(np.array(data['exterior']))
elif isinstance(data['exterior'], _generic.ARRAY_TYPES):
data['exterior'] = kwimage.Coords(data['exterior'])
if 'interiors' in data:
holes = []
for hole in data['interiors']:
if isinstance(hole, (list, tuple)):
hole = kwimage.Coords(np.array(hole))
elif isinstance(hole, _generic.ARRAY_TYPES):
hole = kwimage.Coords(hole)
holes.append(hole)
data['interiors'] = holes
else:
data['interiors'] = []
elif isinstance(data, self.__class__):
# Avoid runtime checks and assume the user is doing the right thing
# if data is explicitly specified
meta = data.meta
data = data.data
if meta is None:
meta = {}
self.data = data
self.meta = meta
@property
[docs] def exterior(self):
return self.data['exterior']
@property
[docs] def interiors(self):
return self.data['interiors']
[docs] def __nice__(self):
return ub.repr2(self.data, nl=1)
@classmethod
[docs] def circle(cls, xy, r, resolution=64):
"""
Create a circular polygon
Example:
>>> xy = (0.5, 0.5)
>>> r = .3
>>> poly = Polygon.circle(xy, r)
"""
tau = 2 * np.pi
theta = np.linspace(0, tau, resolution)
y_offset = np.sin(theta) * r
x_offset = np.cos(theta) * r
center = np.array(xy)
xcoords = center[0] + x_offset
ycoords = center[1] + y_offset
exterior = np.hstack([
xcoords.ravel()[:, None],
ycoords.ravel()[:, None],
])
self = cls(exterior=exterior)
return self
@classmethod
[docs] def random(cls, n=6, n_holes=0, convex=True, tight=False, rng=None):
"""
Args:
n (int): number of points in the polygon (must be 3 or more)
n_holes (int): number of holes
tight (bool, default=False): fits the minimum and maximum points
between 0 and 1
convex (bool, default=True): force resulting polygon will be convex
(may remove exterior points)
CommandLine:
xdoctest -m kwimage.structs.polygon Polygon.random
Example:
>>> rng = None
>>> n = 4
>>> n_holes = 1
>>> cls = Polygon
>>> self = Polygon.random(n=n, rng=rng, n_holes=n_holes, convex=1)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.figure(fnum=1, doclf=True)
>>> kwplot.autompl()
>>> self.draw()
References:
https://gis.stackexchange.com/questions/207731/random-multipolygon
https://stackoverflow.com/questions/8997099/random-polygon
https://stackoverflow.com/questions/27548363/from-voronoi-tessellation-to-shapely-polygons
https://stackoverflow.com/questions/8997099/algorithm-to-generate-random-2d-polygon
"""
import kwarray
import scipy
rng = kwarray.ensure_rng(rng)
def _gen_polygon2(n, irregularity, spikeyness):
"""
Creates the polygon by sampling points on a circle around the centre.
Random noise is added by varying the angular spacing between sequential points,
and by varying the radial distance of each point from the centre.
Based on original code by Mike Ounsworth
Args:
n (int): number of vertices
irregularity (float): [0,1] indicating how much variance there
is in the angular spacing of vertices. [0,1] will map to
[0, 2pi/numberOfVerts]
spikeyness (float): [0,1] indicating how much variance there is
in each vertex from the circle of radius aveRadius. [0,1] will
map to [0, aveRadius]
Returns:
a list of vertices, in CCW order.
Example:
n = 4
irregularity = 0
spikeyness = 0
"""
# Generate around the unit circle
cx, cy = (0.0, 0.0)
radius = 1
tau = np.pi * 2
irregularity = np.clip(irregularity, 0, 1) * 2 * np.pi / n
spikeyness = np.clip(spikeyness, 1e-9, 1)
# generate n angle steps
lower = (tau / n) - irregularity
upper = (tau / n) + irregularity
angle_steps = rng.uniform(lower, upper, n)
# normalize the steps so that point 0 and point n+1 are the same
k = angle_steps.sum() / (2 * np.pi)
angles = (angle_steps / k).cumsum() + rng.uniform(0, tau)
from kwarray import distributions
tnorm = distributions.TruncNormal(radius, spikeyness,
low=0, high=2 * radius, rng=rng)
# now generate the points
radii = tnorm.sample(n)
x_pts = cx + radii * np.cos(angles)
y_pts = cy + radii * np.sin(angles)
points = np.hstack([x_pts[:, None], y_pts[:, None]])
# Scale to 0-1 space
points = points - points.min(axis=0)
points = points / points.max(axis=0)
# Randomly place within 0-1 space
points = points * (rng.rand() * .8 + .2)
min_pt = points.min(axis=0)
max_pt = points.max(axis=0)
high = (1 - max_pt)
low = (0 - min_pt)
offset = (rng.rand(2) * (high - low)) + low
points = points + offset
return points
# points = rng.rand(n, 2)
points = _gen_polygon2(n, 0.9, 0.1 if convex else 0.9)
if convex:
points = _order_vertices(points)
hull = scipy.spatial.ConvexHull(points)
exterior = hull.points[hull.vertices]
# hack
if len(exterior) != n:
points = _gen_polygon2(n, 1.0, 0)
points = _order_vertices(points)
hull = scipy.spatial.ConvexHull(points)
exterior = hull.points[hull.vertices]
else:
exterior = points
exterior = _order_vertices(exterior)
def generate_random(number, polygon, rng):
# FIXME: needs to be inside a convex portion of the polygon
list_of_points = []
minx, miny, maxx, maxy = polygon.bounds
counter = 0
while counter < number:
xy = (rng.uniform(minx, maxx), rng.uniform(miny, maxy))
pnt = Point(*xy)
if polygon.contains(pnt):
list_of_points.append(xy)
counter += 1
return list_of_points
interiors = []
if n_holes:
try:
import shapely
from shapely.geometry import Point
except Exception:
print('FAILED TO IMPORT SHAPELY')
raise
polygon = shapely.geometry.Polygon(shell=exterior)
for _ in range(n_holes):
polygon = shapely.geometry.Polygon(shell=exterior, holes=interiors)
in_pts = generate_random(4, polygon, rng)
interior = _order_vertices(np.array(in_pts))[::-1]
interiors.append(interior)
self = cls(exterior=exterior, interiors=interiors)
if tight:
min_xy = self.data['exterior'].data.min(axis=0)
max_xy = self.data['exterior'].data.max(axis=0)
extent = max_xy - min_xy
self = self.translate(-min_xy).scale(1 / extent)
return self
@ub.memoize_property
[docs] def _impl(self):
return self.data['exterior']._impl
[docs] def to_mask(self, dims=None):
"""
Convert this polygon to a mask
TODO:
- [ ] currently not efficient
Args:
dims (Tuple): height and width of the output mask
Returns:
kwimage.Mask
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(n_holes=1).scale(128)
>>> mask = self.to_mask((128, 128))
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> kwplot.figure(fnum=1, doclf=True)
>>> mask.draw(color='blue')
>>> mask.to_multi_polygon().draw(color='red', alpha=.5)
"""
import kwimage
if dims is None:
raise ValueError('Must specify output raster dimensions')
c_mask = np.zeros(dims, dtype=np.uint8)
value = 1
self.fill(c_mask, value)
mask = kwimage.Mask(c_mask, 'c_mask')
return mask
[docs] def to_relative_mask(self):
"""
Returns a translated mask such the mask dimensions are minimal.
In other words, we move the polygon all the way to the top-left and
return a mask just big enough to fit the polygon.
Returns:
kwimage.Mask
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random().scale(8).translate(100, 100)
>>> mask = self.to_relative_mask()
>>> assert mask.shape <= (8, 8)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> kwplot.figure(fnum=1, doclf=True)
>>> mask.draw(color='blue')
>>> mask.to_multi_polygon().draw(color='red', alpha=.5)
"""
x, y, w, h = self.to_boxes().quantize().to_xywh().data[0]
mask = self.translate((-x, -y)).to_mask(dims=(h, w))
return mask
[docs] def fill(self, image, value=1):
"""
Inplace fill in an image based on this polyon.
Args:
image (ndarray): image to draw on
value (int | Tuple[int], default=1): value fill in with
Returns:
ndarray: the image that has been modified in place
"""
# line_type = cv2.LINE_AA
cv_contours = self._to_cv_countours()
line_type = cv2.LINE_8
# Modification happens inplace
cv2.fillPoly(image, cv_contours, value, line_type, shift=0)
return image
[docs] def _to_cv_countours(self):
"""
OpenCV polygon representation, which is a list of points. Holes are
implicitly represented. When another polygon is drawn over an existing
polyon via cv2.fillPoly
Returns:
List[ndarray]: where each ndarray is of shape [N, 1, 2],
where N is the number of points on the boundary, the middle
dimension is always 1, and the trailing dimension represents
x and y coordinates respectively.
"""
data = self.data
coords = [data['exterior']] + data['interiors']
cv_contours = [np.expand_dims(c.data.astype(int), axis=1)
for c in coords]
return cv_contours
@classmethod
[docs] def coerce(Polygon, data):
"""
Try to autodetermine format of input polygon and coerce it into a
kwimage.Polygon.
Args:
data (object): some type of data that can be interpreted as a
polygon.
Returns:
kwimage.Polygon
Example:
>>> import kwimage
>>> self = kwimage.Polygon.random()
>>> self.coerce(self)
>>> self.coerce(self.exterior)
>>> self.coerce(self.exterior.data)
>>> self.coerce(self.data)
>>> self.coerce(self.to_geojson())
"""
import kwimage
if isinstance(data, Polygon):
return data
if isinstance(data, (np.ndarray, kwimage.Coords)):
return Polygon(exterior=data) # TODO accept torch
if isinstance(data, str):
return Polygon.from_wkt(data)
if isinstance(data, dict):
if 'coordinates' in data:
return Polygon.from_geojson(data)
if 'exterior' in data:
return Polygon(data)
import shapely
import shapely.geometry
if isinstance(data, shapely.geometry.Polygon):
return Polygon.from_shapely(data)
raise TypeError(
'coerce data into a polygon not implemented for this case: {}'.format(
type(data)))
@classmethod
[docs] def from_shapely(Polygon, geom):
"""
Convert a shapely polygon to a kwimage.Polygon
Args:
geom (shapely.geometry.polygon.Polygon): a shapely polygon
Returns:
kwimage.Polygon
"""
exterior = np.array(geom.exterior.coords.xy).T
interiors = [np.array(g.coords.xy).T for g in geom.interiors]
self = Polygon(exterior=exterior, interiors=interiors)
return self
@classmethod
[docs] def from_wkt(Polygon, data):
"""
Convert a WKT string to a kwimage.Polygon
Args:
data (str): a WKT polygon string
Returns:
kwimage.Polygon
Example:
>>> import kwimage
>>> data = 'POLYGON ((0.11 0.61, 0.07 0.588, 0.015 0.50, 0.11 0.61))'
>>> self = kwimage.Polygon.from_wkt(data)
>>> assert len(self.exterior) == 4
"""
from shapely import wkt
geom = wkt.loads(data)
self = Polygon.from_shapely(geom)
return self
@classmethod
[docs] def from_geojson(Polygon, data_geojson):
"""
Convert a geojson polygon to a kwimage.Polygon
Args:
data_geojson (dict): geojson data
References:
https://geojson.org/geojson-spec.html
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(n_holes=2)
>>> data_geojson = self.to_geojson()
>>> new = Polygon.from_geojson(data_geojson)
"""
# By the spec a Polygon should have 3 levels of nesting, but lets
# handle the common case (mistake?) where there are only two
geojson_type = data_geojson.get('type', 'Polygon').lower()
if geojson_type != 'polygon':
raise ValueError('Type is {}, not Polygon'.format(geojson_type))
# TODO: better method for checking nest depth
coords = data_geojson['coordinates']
def check_leftmost_depth(data):
# quick check leftmost depth of a nested struct
item = data
depth = 0
while isinstance(item, (list, tuple)):
if len(item) == 0:
raise Exception('no child node')
item = item[0]
depth += 1
return depth
depth = check_leftmost_depth(coords)
if depth == 2:
raise Exception(ub.codeblock(
'''
The GEOJSON spec has a depth of 3!
coodinates should be:
'coordinates': [
[ [x_1, y_1], ... , [x_n, y_n] ], # exterior
[ [x_1, y_1], ... , [x_n, y_n] ], # hole 1
[ [x_1, y_1], ... , [x_n, y_n] ], # hole 2
]
'''))
# exterior = np.array(coords)
# interiors = []
elif depth == 3:
exterior = np.array(coords[0])
interiors = [np.array(h) for h in coords[1:]]
else:
raise Exception('Unknown geojson format')
self = Polygon(exterior=exterior, interiors=interiors)
return self
[docs] def to_shapely(self):
"""
Example:
>>> # xdoc: +REQUIRES(module:kwplot)
>>> # xdoc: +REQUIRES(module:shapely)
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(n_holes=1)
>>> self = self.scale(100)
>>> geom = self.to_shapely()
>>> print('geom = {!r}'.format(geom))
"""
import shapely
import shapely.geometry
geom = shapely.geometry.Polygon(
shell=self.data['exterior'].data,
holes=[c.data for c in self.data['interiors']]
)
return geom
[docs] def to_geojson(self):
"""
Converts polygon to a geojson structure
Returns:
Dict[str, object]
Example:
>>> import kwimage
>>> self = kwimage.Polygon.random()
>>> print(self.to_geojson())
"""
coords = [self.data['exterior'].data.tolist()]
holes = [interior.data.tolist() for interior in self.data['interiors']]
if holes:
coords.extend(holes)
geojson = {
'type': 'Polygon',
'coordinates': coords,
}
return geojson
[docs] def to_wkt(self):
"""
Convert a kwimage.Polygon to WKT string
Example:
>>> import kwimage
>>> self = kwimage.Polygon.random()
>>> print(self.to_wkt())
"""
shp = self.to_shapely()
if hasattr(shp, 'wkt'):
return shp.wkt # new version (~1.8a1)
else:
return shp.to_wkt()
@classmethod
[docs] def from_coco(cls, data, dims=None):
"""
Accepts either new-style or old-style coco polygons
"""
if isinstance(data, list):
if len(data) > 0:
assert isinstance(ub.peek(data), numbers.Number)
exterior = np.array(data).reshape(-1, 2)
self = cls(exterior=exterior)
else:
self = cls(exterior=[])
elif isinstance(data, dict):
if 'exterior' not in data:
raise ValueError('dict requires exterior key')
self = cls(**data)
else:
raise TypeError(type(data))
return self
[docs] def _to_coco(self, style='orig'):
return self.to_coco(style=style)
[docs] def to_coco(self, style='orig'):
"""
Returns:
List | Dict : coco-style polygons
"""
interiors = self.data.get('interiors', [])
if style == 'orig':
if interiors:
raise ValueError('Original coco does not support holes')
return self.data['exterior'].data.ravel().tolist()
elif style == 'new':
_new = {
'exterior': self.data['exterior'].data.tolist(),
'interiors': [item.data.tolist() for item in interiors]
}
return _new
else:
raise KeyError(style)
[docs] def to_multi_polygon(self):
return MultiPolygon([self])
[docs] def to_boxes(self):
"""
Deprecated: lossy conversion use 'bounding_box' instead
"""
return self.bounding_box()
@property
[docs] def centroid(self):
shp_centroid = self.to_shapely().centroid
xy = (shp_centroid.x, shp_centroid.y)
return xy
[docs] def bounding_box(self):
"""
Returns an axis-aligned bounding box for the segmentation
Returns:
kwimage.Boxes
"""
import kwimage
xys = self.data['exterior'].data
lt = xys.min(axis=0)
rb = xys.max(axis=0)
ltrb = np.hstack([lt, rb])[None, :]
boxes = kwimage.Boxes(ltrb, 'ltrb')
return boxes
[docs] def bounding_box_polygon(self):
"""
Returns an axis-aligned bounding polygon for the segmentation.
Notes:
This Polygon will be a Box, not a convex hull! Use shapely for
convex hulls.
Returns:
kwimage.Polygon
"""
new = self.bounding_box().to_polygons()[0]
return new
[docs] def copy(self):
self2 = Polygon(self.data, self.meta)
self2.data['exterior'] = self2.data['exterior'].copy()
self2.data['interiors'] = [x.copy() for x in self2.data['interiors']]
return self2
[docs] def clip(self, x_min, y_min, x_max, y_max, inplace=False):
"""
Clip polygon to image boundaries.
Example:
>>> from kwimage.structs.polygon import *
>>> self = Polygon.random().scale(10).translate(-1)
>>> self2 = self.clip(1, 1, 3, 3)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> self2.draw(setlim=True)
"""
if inplace:
self2 = self
else:
self2 = self.copy()
impl = self._impl
xs, ys = impl.T(self2.data['exterior'].data)
np.clip(xs, x_min, x_max, out=xs)
np.clip(ys, y_min, y_max, out=ys)
return self2
[docs] def draw_on(self, image, color='blue', fill=True, border=False, alpha=1.0,
copy=False):
"""
Rasterizes a polygon on an image. See `draw` for a vectorized
matplotlib version.
Args:
image (ndarray): image to raster polygon on.
color (str | tuple): data coercable to a color
fill (bool, default=True): draw the center mass of the polygon
border (bool, default=False): draw the border of the polygon
alpha (float, default=1.0): polygon transparency (setting alpha < 1
makes this function much slower).
copy (bool, default=False): if False only copies if necessary
Example:
>>> # xdoc: +REQUIRES(module:kwplot)
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(n_holes=1).scale(128)
>>> image = np.zeros((128, 128), dtype=np.float32)
>>> image = self.draw_on(image)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> kwplot.imshow(image, fnum=1)
Example:
>>> import kwimage
>>> color = 'blue'
>>> self = kwimage.Polygon.random(n_holes=1).scale(128)
>>> image = np.zeros((128, 128), dtype=np.float32)
>>> # Test drawong on all channel + dtype combinations
>>> im3 = np.random.rand(128, 128, 3)
>>> im_chans = {
>>> 'im3': im3,
>>> 'im1': kwimage.convert_colorspace(im3, 'rgb', 'gray'),
>>> 'im4': kwimage.convert_colorspace(im3, 'rgb', 'rgba'),
>>> }
>>> inputs = {}
>>> for k, im in im_chans.items():
>>> inputs[k + '_01'] = (kwimage.ensure_float01(im.copy()), {'alpha': None})
>>> inputs[k + '_255'] = (kwimage.ensure_uint255(im.copy()), {'alpha': None})
>>> inputs[k + '_01_a'] = (kwimage.ensure_float01(im.copy()), {'alpha': 0.5})
>>> inputs[k + '_255_a'] = (kwimage.ensure_uint255(im.copy()), {'alpha': 0.5})
>>> outputs = {}
>>> for k, v in inputs.items():
>>> im, kw = v
>>> outputs[k] = self.draw_on(im, color=color, **kw)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.figure(fnum=2, doclf=True)
>>> kwplot.autompl()
>>> pnum_ = kwplot.PlotNums(nCols=2, nRows=len(inputs))
>>> for k in inputs.keys():
>>> kwplot.imshow(inputs[k][0], fnum=2, pnum=pnum_(), title=k)
>>> kwplot.imshow(outputs[k], fnum=2, pnum=pnum_(), title=k)
>>> kwplot.show_if_requested()
"""
import kwimage
# return shape of contours to openCV contours
dtype_fixer = _generic._consistent_dtype_fixer(image)
# print('--- A')
# print('image.dtype = {!r}'.format(image.dtype))
# print('image.max() = {!r}'.format(image.max()))
# line_type = cv2.LINE_AA
line_type = cv2.LINE_8
cv_contours = self._to_cv_countours()
if alpha is None or alpha == 1.0:
# image = kwimage.ensure_uint255(image)
image = kwimage.atleast_3channels(image, copy=copy)
rgba = kwimage.Color(color)._forimage(image)
else:
image = kwimage.ensure_float01(image)
image = kwimage.ensure_alpha_channel(image)
rgba = kwimage.Color(color, alpha=alpha)._forimage(image)
# print('--- B')
# print('image.dtype = {!r}'.format(image.dtype))
# print('image.max() = {!r}'.format(image.max()))
# print('rgba = {!r}'.format(rgba))
if fill:
if alpha is None or alpha == 1.0:
# Modification happens inplace
image = cv2.fillPoly(image, cv_contours, rgba, line_type, shift=0)
else:
orig = image.copy()
mask = np.zeros_like(orig)
mask = cv2.fillPoly(mask, cv_contours, rgba, line_type, shift=0)
# TODO: could use add weighted
image = kwimage.overlay_alpha_images(mask, orig)
rgba = kwimage.Color(rgba)._forimage(image)
# print('--- C')
# print('image.dtype = {!r}'.format(image.dtype))
# print('image.max() = {!r}'.format(image.max()))
# print('rgba = {!r}'.format(rgba))
if border or True:
thickness = 4
contour_idx = -1
image = cv2.drawContours(image, cv_contours, contour_idx, rgba,
thickness, line_type)
# image = kwimage.ensure_float01(image)[..., 0:3]
# print('--- D')
# print('image.dtype = {!r}'.format(image.dtype))
# print('image.max() = {!r}'.format(image.max()))
image = dtype_fixer(image, copy=False)
return image
[docs] def draw(self, color='blue', ax=None, alpha=1.0, radius=1, setlim=False,
border=False, linewidth=2):
"""
Draws polygon in a matplotlib axes. See `draw_on` for in-memory image
modification.
Args:
setlim (bool): if True ensures the limits of the axes contains the
polygon
color (str | Tuple): coercable color
alpha (float): fill transparency
setlim (bool): if True, modify the x and y limits of the matplotlib
axes such that the polygon is can be seen.
border (bool, default=False):
if True, draws an edge border on the polygon.
linewidth (bool):
width of the border
TODO:
- [ ] Rework arguments in favor of matplotlib standards
Example:
>>> # xdoc: +REQUIRES(module:kwplot)
>>> from kwimage.structs.polygon import * # NOQA
>>> self = Polygon.random(n_holes=1)
>>> self = self.scale(100)
>>> # xdoc: +REQUIRES(--show)
>>> self.draw()
>>> import kwplot
>>> kwplot.autompl()
>>> from matplotlib import pyplot as plt
>>> kwplot.figure(fnum=2)
>>> self.draw(setlim=True)
"""
import matplotlib as mpl
from matplotlib.patches import Path
from matplotlib import pyplot as plt
import kwimage
if ax is None:
ax = plt.gca()
color = list(kwimage.Color(color).as01())
data = self.data
exterior = data['exterior'].data.tolist()
exterior.append(exterior[0])
n = len(exterior)
verts = []
verts.extend(exterior)
codes = [Path.MOVETO] + ([Path.LINETO] * (n - 2)) + [Path.CLOSEPOLY]
interiors = data.get('interiors', [])
for hole in interiors:
hole = hole.data.tolist()
hole.append(hole[0])
n = len(hole)
verts.extend(hole)
codes += [Path.MOVETO] + ([Path.LINETO] * (n - 2)) + [Path.CLOSEPOLY]
verts = np.array(verts)
path = Path(verts, codes)
kw = {}
# TODO:
# depricate border kwarg in favor of standard matplotlib args
if border:
kw['linewidth'] = linewidth
try:
edgecolor = list(kwimage.Color(border).as01())
except Exception:
edgecolor = list(color)
# hack to darken
edgecolor[0] -= .1
edgecolor[1] -= .1
edgecolor[2] -= .1
edgecolor = [min(1, max(0, c)) for c in edgecolor]
kw['edgecolor'] = edgecolor
else:
kw['linewidth'] = 0
patch = mpl.patches.PathPatch(path, alpha=alpha, facecolor=color, **kw)
ax.add_patch(patch)
if setlim:
x1, y1, x2, y2 = self.to_boxes().to_ltrb().data[0]
if setlim == 'grow':
# only allow growth
x1_, x2_ = ax.get_xlim()
x1 = min(x1_, x1)
x2 = max(x2_, x2)
y1_, y2_ = ax.get_ylim()
y1 = min(y1_, y1)
y2 = max(y2_, y2)
ax.set_xlim(x1, x2)
ax.set_ylim(y1, y2)
return patch
[docs] def _ensure_vertex_order(self, inplace=False):
"""
Fixes vertex ordering so the exterior ring is CCW and the interior rings
are CW.
Example:
>>> import kwimage
>>> self = kwimage.Polygon.random(n=3, n_holes=2, rng=0)
>>> print('self = {!r}'.format(self))
>>> new = self._ensure_vertex_order()
>>> print('new = {!r}'.format(new))
>>> self = kwimage.Polygon.random(n=3, n_holes=2, rng=0).swap_axes()
>>> print('self = {!r}'.format(self))
>>> new = self._ensure_vertex_order()
>>> print('new = {!r}'.format(new))
"""
new = self if inplace else self.__class__(self.data.copy())
exterior = new.data['exterior']
if _is_clockwise(exterior.data):
# ensure exterior is CCW
exterior.data = exterior.data[::-1]
pass
for interior in new.data['interiors']:
if not _is_clockwise(interior.data):
interior.data = interior.data[::-1]
return new
[docs]def _is_clockwise(verts):
"""
References:
https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order
Ignore:
verts = poly.data['exterior'].data[::-1]
"""
x1 = verts[:-1][:, 0]
y1 = verts[:-1][:, 1]
x2 = verts[1:][:, 0]
y2 = verts[1:][:, 1]
is_clockwise = ((x2 - x1) * (y2 + y1)).sum() > 0
# cross_product = np.cross(verts[:-1], verts[1:])
# is_clockwise = cross_product.sum() > 0
return is_clockwise
[docs]def _order_vertices(verts):
"""
References:
https://stackoverflow.com/questions/1709283/how-can-i-sort-a-coordinate-list-for-a-rectangle-counterclockwise
Ignore:
verts = poly.data['exterior'].data[::-1]
"""
mean_x = verts.T[0].sum() / len(verts)
mean_y = verts.T[1].sum() / len(verts)
delta_x = mean_x - verts.T[0]
delta_y = verts.T[1] - mean_y
tau = np.pi * 2
angle = (np.arctan2(delta_x, delta_y) + tau) % tau
sortx = angle.argsort()
verts = verts.take(sortx, axis=0)
return verts
[docs]class MultiPolygon(_generic.ObjectList):
"""
Data structure for storing multiple polygons (typically related to the same
underlying but potentitally disjoing object)
Attributes:
data (List[Polygon])
"""
@classmethod
[docs] def random(self, n=3, n_holes=0, rng=None, tight=False):
"""
Create a random MultiPolygon
Returns:
MultiPolygon
"""
import kwarray
rng = kwarray.ensure_rng(rng)
data = [Polygon.random(rng=rng, n_holes=n_holes, tight=tight)
for _ in range(n)]
self = MultiPolygon(data)
return self
[docs] def fill(self, image, value=1):
"""
Inplace fill in an image based on this multi-polyon.
Args:
image (ndarray): image to draw on (inplace)
value (int | Tuple[int], default=1): value fill in with
Returns:
ndarray: the image that has been modified in place
"""
for p in self.data:
p.fill(image, value=value)
return image
[docs] def to_multi_polygon(self):
return self
[docs] def to_boxes(self):
"""
Deprecated: lossy conversion use 'bounding_box' instead
"""
return self.bounding_box()
[docs] def bounding_box(self):
"""
Return the bounding box of the multi polygon
Returns:
kwimage.Boxes: a Boxes object with one box that encloses all
polygons
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = MultiPolygon.random(rng=0, n=10)
>>> boxes = self.to_boxes()
>>> sub_boxes = [d.to_boxes() for d in self.data]
>>> areas1 = np.array([s.intersection(boxes).area[0] for s in sub_boxes])
>>> areas2 = np.array([s.area[0] for s in sub_boxes])
>>> assert np.allclose(areas1, areas2)
"""
import kwimage
lt = np.array([np.inf, np.inf])
rb = np.array([-np.inf, -np.inf])
for data in self.data:
xys = data.data['exterior'].data
lt = np.minimum(lt, xys.min(axis=0))
rb = np.maximum(rb, xys.max(axis=0))
ltrb = np.hstack([lt, rb])[None, :]
boxes = kwimage.Boxes(ltrb, 'ltrb')
return boxes
[docs] def to_mask(self, dims=None):
"""
Returns a mask object indication regions occupied by this multipolygon
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> s = 100
>>> self = MultiPolygon.random(rng=0).scale(s)
>>> dims = (s, s)
>>> mask = self.to_mask(dims)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> kwplot.figure(fnum=1, doclf=True)
>>> from matplotlib import pyplot as pl
>>> ax = plt.gca()
>>> ax.set_xlim(0, s)
>>> ax.set_ylim(0, s)
>>> self.draw(color='red', alpha=.4)
>>> mask.draw(color='blue', alpha=.4)
"""
import kwimage
if dims is None:
raise ValueError('Must specify output raster dimensions')
c_mask = np.zeros(dims, dtype=np.uint8)
for p in self.data:
if p is not None:
p.fill(c_mask, value=1)
mask = kwimage.Mask(c_mask, 'c_mask')
return mask
[docs] def to_relative_mask(self):
"""
Returns a translated mask such the mask dimensions are minimal.
In other words, we move the polygon all the way to the top-left and
return a mask just big enough to fit the polygon.
Returns:
Mask
"""
x, y, w, h = self.to_boxes().quantize().to_xywh().data[0]
mask = self.translate((-x, -y)).to_mask(dims=(h, w))
return mask
@classmethod
[docs] def coerce(cls, data, dims=None):
"""
Attempts to construct a MultiPolygon instance from the input data
See Mask.coerce
"""
from kwimage.structs.segmentation import _coerce_coco_segmentation
self = _coerce_coco_segmentation(data, dims=dims)
if self is not None:
self = self.to_multi_polygon()
return self
[docs] def to_shapely(self):
"""
Example:
>>> # xdoc: +REQUIRES(module:kwplot)
>>> # xdoc: +REQUIRES(module:shapely)
>>> from kwimage.structs.polygon import * # NOQA
>>> self = MultiPolygon.random(rng=0)
>>> geom = self.to_shapely()
>>> print('geom = {!r}'.format(geom))
"""
import shapely
import shapely.geometry
polys = [p.to_shapely() for p in self.data]
geom = shapely.geometry.MultiPolygon(polys)
return geom
@classmethod
[docs] def from_shapely(MultiPolygon, geom):
"""
Convert a shapely polygon or multipolygon to a kwimage.MultiPolygon
"""
if geom.type == 'Polygon':
polys = [Polygon.from_shapely(geom)]
else:
polys = [Polygon.from_shapely(g) for g in geom.geoms]
self = MultiPolygon(polys)
return self
@classmethod
[docs] def from_geojson(MultiPolygon, data_geojson):
"""
Convert a geojson polygon or multipolygon to a kwimage.MultiPolygon
Example:
>>> import kwimage
>>> orig = kwimage.MultiPolygon.random()
>>> data_geojson = orig.to_geojson()
>>> self = kwimage.MultiPolygon.from_geojson(data_geojson)
"""
if data_geojson['type'] == 'Polygon':
polys = [Polygon.from_geojson(data_geojson)]
else:
polys = [
Polygon.from_geojson(
{'type': 'Polygon', 'coordinates': coords})
for coords in data_geojson['coordinates']
]
self = MultiPolygon(polys)
return self
[docs] def to_geojson(self):
"""
Converts polygon to a geojson structure
"""
coords = [poly.to_geojson()['coordinates'] for poly in self.data]
data_geojson = {
'type': 'MultiPolygon',
'coordinates': coords,
}
return data_geojson
@classmethod
[docs] def from_coco(cls, data, dims=None):
"""
Accepts either new-style or old-style coco multi-polygons
"""
if isinstance(data, list):
poly_list = [Polygon.from_coco(item, dims=dims)
for item in data]
self = cls(poly_list)
else:
raise TypeError(type(data))
return self
[docs] def _to_coco(self, style='orig'):
return self.to_coco(style=style)
[docs] def to_coco(self, style='orig'):
"""
Example:
>>> from kwimage.structs.polygon import * # NOQA
>>> self = MultiPolygon.random(1, rng=0)
>>> self.to_coco()
"""
return [item.to_coco(style=style) for item in self.data]
[docs] def swap_axes(self, inplace=False):
return self.apply(lambda item: item.swap_axes(inplace=inplace))
# def draw_on(self, image, color='blue', fill=True, border=False, alpha=1.0):
# """
# Faster version
# """
# import kwimage
# dtype_fixer = _generic._consistent_dtype_fixer(image)
# if alpha == 1.0:
# image = kwimage.ensure_uint255(image)
# image = kwimage.atleast_3channels(image)
# rgba = kwimage.Color(color).as255()
# else:
# image = kwimage.ensure_float01(image)
# image = kwimage.ensure_alpha_channel(image)
# rgba = kwimage.Color(color, alpha=alpha).as01()
# kwargs = dict(color=color, fill=fill, border=border, alpha=alpha)
# for item in self.data:
# if item is not None:
# image = item.draw_on(image=image, **kwargs)
# image = dtype_fixer(image)
# return image
[docs]class PolygonList(_generic.ObjectList):
"""
Stores and allows manipluation of multiple polygons, usually within the
same image.
"""
[docs] def to_mask_list(self, dims=None):
"""
Converts all items to masks
"""
import kwimage
new = kwimage.MaskList([
None if item is None else item.to_mask(dims=dims)
for item in self
])
return new
[docs] def to_polygon_list(self):
return self
[docs] def to_segmentation_list(self):
"""
Converts all items to segmentation objects
"""
import kwimage
new = kwimage.SegmentationList([
None if item is None else kwimage.Segmentation.coerce(item)
for item in self
])
return new
[docs] def swap_axes(self, inplace=False):
return self.apply(lambda item: item.swap_axes(inplace=inplace))
[docs] def to_geojson(self, as_collection=False):
"""
Converts a list of polygons/multipolygons to a geojson structure
Args:
as_collection (bool): if True, wraps the polygon geojson items in a
geojson feature collection, otherwise just return a list of
items.
Returns:
List[Dict] | Dict: items or geojson data
Example:
>>> import kwimage
>>> data = [kwimage.Polygon.random(),
>>> kwimage.Polygon.random(n_holes=1),
>>> kwimage.MultiPolygon.random(n_holes=1),
>>> kwimage.MultiPolygon.random()]
>>> self = kwimage.PolygonList(data)
>>> geojson = self.to_geojson(as_collection=True)
>>> items = self.to_geojson(as_collection=False)
>>> print('geojson = {}'.format(ub.repr2(geojson, nl=-2, precision=1)))
>>> print('items = {}'.format(ub.repr2(items, nl=-2, precision=1)))
"""
items = [poly.to_geojson() for poly in self.data]
if as_collection:
geojson = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": item,
"properties": {}
}
for item in items
]
}
return geojson
else:
return items