kwimage.structs.polygon

Module Contents

Classes

_PolyArrayBackend

_PolyWarpMixin

Polygon

Represents a single polygon as set of exterior boundary points and a list

MultiPolygon

Data structure for storing multiple polygons (typically related to the same

PolygonList

Stores and allows manipluation of multiple polygons, usually within the

Functions

_is_clockwise(verts)

References

_order_vertices(verts)

References

class kwimage.structs.polygon._PolyArrayBackend[source]
is_numpy(self)[source]
is_tensor(self)[source]
tensor(self, device=ub.NoParam)[source]

Example

>>> # xdoctest: +REQUIRES(module:torch)
>>> from kwimage.structs.polygon import *
>>> self = Polygon.random()
>>> self.tensor()
numpy(self)[source]

Example

>>> # xdoctest: +REQUIRES(module:torch)
>>> from kwimage.structs.polygon import *
>>> self = Polygon.random()
>>> self.tensor().numpy().tensor().numpy()
class kwimage.structs.polygon._PolyWarpMixin[source]
_warp_imgaug(self, augmenter, input_dims, inplace=False)[source]

Warps by applying an augmenter from the imgaug library

Parameters
  • 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)
to_imgaug(self, shape)[source]
warp(self, transform, input_dims=None, output_dims=None, inplace=False)[source]

Generalized coordinate transform.

Parameters
  • 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)
scale(self, factor, about=None, output_dims=None, inplace=False)[source]

Scale a polygon by a factor

Parameters
  • 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)
translate(self, offset, output_dims=None, inplace=False)[source]

Shift the polygon up/down left/right

Parameters
  • 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)
rotate(self, theta, about=None, output_dims=None, inplace=False)[source]

Rotate the polygon

Parameters
  • 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)
_rectify_about(self, about)[source]

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')
swap_axes(self, inplace=False)[source]

Swap the x and y coordinate axes

Parameters

inplace (bool, default=False) – if True, modifies data inplace

Returns

modified polygon

Return type

Polygon

class kwimage.structs.polygon.Polygon(data=None, meta=None, datakeys=None, metakeys=None, **kwargs)[source]

Bases: kwimage.structs._generic.Spatial, _PolyArrayBackend, _PolyWarpMixin, ubelt.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)
__datakeys__ = ['exterior', 'interiors'][source]
__metakeys__ = ['classes'][source]
property exterior(self)[source]
property interiors(self)[source]
__nice__(self)[source]
classmethod circle(cls, xy, r, resolution=64)[source]

Create a circular polygon

Example

>>> xy = (0.5, 0.5)
>>> r = .3
>>> poly = Polygon.circle(xy, r)
classmethod random(cls, n=6, n_holes=0, convex=True, tight=False, rng=None)[source]
Parameters
  • 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

_impl(self)[source]
to_mask(self, dims=None)[source]

Convert this polygon to a mask

Todo

  • [ ] currently not efficient

Parameters

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)
to_relative_mask(self)[source]

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)
fill(self, image, value=1)[source]

Inplace fill in an image based on this polyon.

Parameters
  • image (ndarray) – image to draw on

  • value (int | Tuple[int], default=1) – value fill in with

Returns

the image that has been modified in place

Return type

ndarray

_to_cv_countours(self)[source]

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

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.

Return type

List[ndarray]

classmethod coerce(Polygon, data)[source]

Try to autodetermine format of input polygon and coerce it into a kwimage.Polygon.

Parameters

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())
classmethod from_shapely(Polygon, geom)[source]

Convert a shapely polygon to a kwimage.Polygon

Parameters

geom (shapely.geometry.polygon.Polygon) – a shapely polygon

Returns

kwimage.Polygon

classmethod from_wkt(Polygon, data)[source]

Convert a WKT string to a kwimage.Polygon

Parameters

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
classmethod from_geojson(Polygon, data_geojson)[source]

Convert a geojson polygon to a kwimage.Polygon

Parameters

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)
to_shapely(self)[source]

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))
to_geojson(self)[source]

Converts polygon to a geojson structure

Returns

Dict[str, object]

Example

>>> import kwimage
>>> self = kwimage.Polygon.random()
>>> print(self.to_geojson())
to_wkt(self)[source]

Convert a kwimage.Polygon to WKT string

Example

>>> import kwimage
>>> self = kwimage.Polygon.random()
>>> print(self.to_wkt())
classmethod from_coco(cls, data, dims=None)[source]

Accepts either new-style or old-style coco polygons

_to_coco(self, style='orig')[source]
to_coco(self, style='orig')[source]
Returns

coco-style polygons

Return type

List | Dict

to_multi_polygon(self)[source]
to_boxes(self)[source]

Deprecated: lossy conversion use ‘bounding_box’ instead

property centroid(self)[source]
bounding_box(self)[source]

Returns an axis-aligned bounding box for the segmentation

Returns

kwimage.Boxes

bounding_box_polygon(self)[source]

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

copy(self)[source]
clip(self, x_min, y_min, x_max, y_max, inplace=False)[source]

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)
draw_on(self, image, color='blue', fill=True, border=False, alpha=1.0, copy=False)[source]

Rasterizes a polygon on an image. See draw for a vectorized matplotlib version.

Parameters
  • 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()
draw(self, color='blue', ax=None, alpha=1.0, radius=1, setlim=False, border=False, linewidth=2)[source]

Draws polygon in a matplotlib axes. See draw_on for in-memory image modification.

Parameters
  • 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)
_ensure_vertex_order(self, inplace=False)[source]

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))
kwimage.structs.polygon._is_clockwise(verts)[source]

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]

kwimage.structs.polygon._order_vertices(verts)[source]

References

https://stackoverflow.com/questions/1709283/how-can-i-sort-a-coordinate-list-for-a-rectangle-counterclockwise

Ignore:

verts = poly.data[‘exterior’].data[::-1]

class kwimage.structs.polygon.MultiPolygon[source]

Bases: kwimage.structs._generic.ObjectList

Data structure for storing multiple polygons (typically related to the same underlying but potentitally disjoing object)

Variables

data (List[Polygon]) –

classmethod random(self, n=3, n_holes=0, rng=None, tight=False)[source]

Create a random MultiPolygon

Returns

MultiPolygon

fill(self, image, value=1)[source]

Inplace fill in an image based on this multi-polyon.

Parameters
  • image (ndarray) – image to draw on (inplace)

  • value (int | Tuple[int], default=1) – value fill in with

Returns

the image that has been modified in place

Return type

ndarray

to_multi_polygon(self)[source]
to_boxes(self)[source]

Deprecated: lossy conversion use ‘bounding_box’ instead

bounding_box(self)[source]

Return the bounding box of the multi polygon

Returns

a Boxes object with one box that encloses all

polygons

Return type

kwimage.Boxes

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)
to_mask(self, dims=None)[source]

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)
to_relative_mask(self)[source]

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

classmethod coerce(cls, data, dims=None)[source]

Attempts to construct a MultiPolygon instance from the input data

See Mask.coerce

to_shapely(self)[source]

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))
classmethod from_shapely(MultiPolygon, geom)[source]

Convert a shapely polygon or multipolygon to a kwimage.MultiPolygon

classmethod from_geojson(MultiPolygon, data_geojson)[source]

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)
to_geojson(self)[source]

Converts polygon to a geojson structure

classmethod from_coco(cls, data, dims=None)[source]

Accepts either new-style or old-style coco multi-polygons

_to_coco(self, style='orig')[source]
to_coco(self, style='orig')[source]

Example

>>> from kwimage.structs.polygon import *  # NOQA
>>> self = MultiPolygon.random(1, rng=0)
>>> self.to_coco()
swap_axes(self, inplace=False)[source]
class kwimage.structs.polygon.PolygonList[source]

Bases: kwimage.structs._generic.ObjectList

Stores and allows manipluation of multiple polygons, usually within the same image.

to_mask_list(self, dims=None)[source]

Converts all items to masks

to_polygon_list(self)[source]
to_segmentation_list(self)[source]

Converts all items to segmentation objects

swap_axes(self, inplace=False)[source]
to_geojson(self, as_collection=False)[source]

Converts a list of polygons/multipolygons to a geojson structure

Parameters

as_collection (bool) – if True, wraps the polygon geojson items in a geojson feature collection, otherwise just return a list of items.

Returns

items or geojson data

Return type

List[Dict] | Dict

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)))