Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplified Access to Coordinates and Items in Augmentables #495

Merged
merged 6 commits into from
Nov 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions changelogs/master/20191113_iterable_augmentables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Simplified Access to Coordinates and Items in Augmentables #495

* Added module `imgaug.augmentables.base`.
* Added interface `imgaug.augmentables.base.IAugmentable`, implemented by
`HeatmapsOnImage`, `SegmentationMapsOnImage`, `KeypointsOnImage`,
`BoundingBoxesOnImage`, `PolygonsOnImage` and `LineStringsOnImage`.
* Added ability to iterate over coordinate-based `*OnImage` instances
(keypoints, bounding boxes, polygons, line strings), e.g.
`bbsoi = BoundingBoxesOnImage(bbs, shape=...); for bb in bbsoi: ...`.
would iterate now over `bbs`.
* Added ability to iterate over coordinates of `BoundingBox` (top-left,
bottom-right), `Polygon` and `LineString` via `for xy in obj: ...`.
* Added ability to access coordinates of `BoundingBox`, `Polygon` and
`LineString` using indices or slices, e.g. `line_string[1:]` to get an
array of all coordinates except the first one.
* Added property `Keypoint.xy`.
* Added property `Keypoint.xy_int`.
16 changes: 16 additions & 0 deletions imgaug/augmentables/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Interfaces used by augmentable objects."""
from __future__ import print_function, division, absolute_import


class IAugmentable(object):
"""Interface of augmentable objects.

This interface is right now only used to "mark" augmentable objects.
It does not enforce any methods yet (but will probably in the future).

Currently, only ``*OnImage`` clases are marked as augmentable.
Non-OnImage objects are normalized to OnImage-objects.
Batches are not yet marked as augmentable, but might be in the future.

"""
pass
46 changes: 42 additions & 4 deletions imgaug/augmentables/bbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import skimage.measure

from .. import imgaug as ia
from .base import IAugmentable
from .utils import (normalize_shape, project_coords,
_remove_out_of_image_fraction)

Expand Down Expand Up @@ -861,6 +862,7 @@ def to_polygon(self):
(self.x1, self.y2)
], label=self.label)

# TODO also introduce similar area_almost_equals()
def coords_almost_equals(self, other, max_distance=1e-4):
"""Estimate if this and another BB have almost identical coordinates.

Expand All @@ -885,17 +887,18 @@ def coords_almost_equals(self, other, max_distance=1e-4):
coordinates.

"""
if ia.is_np_array(other):
if isinstance(other, BoundingBox):
coords_b = other.coords.flat
elif ia.is_np_array(other):
# we use flat here in case other is (N,2) instead of (4,)
coords_b = other.flat
elif ia.is_iterable(other):
coords_b = list(ia.flatten(other))
else:
assert isinstance(other, BoundingBox), (
raise ValueError(
"Expected 'other' to be an iterable containing two "
"(x,y)-coordinate pairs or a BoundingBox. "
"Got type %s." % (type(other),))
coords_b = other.coords.flat

coords_a = self.coords

Expand Down Expand Up @@ -1042,6 +1045,28 @@ def deepcopy(self, x1=None, y1=None, x2=None, y2=None, label=None):
# the deepcopy from copy()
return self.copy(x1=x1, y1=y1, x2=x2, y2=y2, label=label)

def __getitem__(self, indices):
"""Get the coordinate(s) with given indices.

Returns
-------
ndarray
xy-coordinate(s) as ``ndarray``.

"""
return self.coords[indices]

def __iter__(self):
"""Iterate over the coordinates of this instance.

Yields
------
ndarray
An ``(2,)`` ``ndarray`` denoting an xy-coordinate pair.

"""
return iter(self.coords)

def __repr__(self):
return self.__str__()

Expand All @@ -1050,7 +1075,7 @@ def __str__(self):
self.x1, self.y1, self.x2, self.y2, self.label)


class BoundingBoxesOnImage(object):
class BoundingBoxesOnImage(IAugmentable):
"""Container for the list of all bounding boxes on a single image.

Parameters
Expand Down Expand Up @@ -1600,6 +1625,19 @@ def deepcopy(self):
bbs = [bb.deepcopy() for bb in self.bounding_boxes]
return BoundingBoxesOnImage(bbs, tuple(self.shape))

def __iter__(self):
"""Iterate over the bounding boxes in this container.

Yields
------
Polygon
A bounding box in this container.
The order is identical to the order in the bounding box list
provided upon class initialization.

"""
return iter(self.bounding_boxes)

def __repr__(self):
return self.__str__()

Expand Down
3 changes: 2 additions & 1 deletion imgaug/augmentables/heatmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import six.moves as sm

from .. import imgaug as ia
from .base import IAugmentable


class HeatmapsOnImage(object):
class HeatmapsOnImage(IAugmentable):
"""Object representing heatmaps on a single image.

Parameters
Expand Down
40 changes: 39 additions & 1 deletion imgaug/augmentables/kps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import six.moves as sm

from .. import imgaug as ia
from .base import IAugmentable
from .utils import (normalize_shape, project_coords,
_remove_out_of_image_fraction)

Expand Down Expand Up @@ -122,6 +123,30 @@ def y_int(self):
"""
return int(np.round(self.y))

@property
def xy(self):
"""Get the keypoint's x- and y-coordinate as a single array.

Returns
-------
ndarray
A ``(2,)`` ``ndarray`` denoting the xy-coordinate pair.

"""
return self.coords[0, :]

@property
def xy_int(self):
"""Get the keypoint's xy-coord, rounded to closest integer.

Returns
-------
ndarray
A ``(2,)`` ``ndarray`` denoting the xy-coordinate pair.

"""
return np.round(self.xy).astype(np.int32)

def project(self, from_shape, to_shape):
"""Project the keypoint onto a new position on a new image.

Expand Down Expand Up @@ -508,7 +533,7 @@ def __str__(self):
return "Keypoint(x=%.8f, y=%.8f)" % (self.x, self.y)


class KeypointsOnImage(object):
class KeypointsOnImage(IAugmentable):
"""Container for all keypoints on a single image.

Parameters
Expand Down Expand Up @@ -1202,6 +1227,19 @@ def deepcopy(self, keypoints=None, shape=None):
shape = tuple(self.shape)
return KeypointsOnImage(keypoints, shape)

def __iter__(self):
"""Iterate over the keypoints in this container.

Yields
------
Keypoint
A keypoint in this container.
The order is identical to the order in the keypoint list
provided upon class initialization.

"""
return iter(self.items)

def __repr__(self):
return self.__str__()

Expand Down
38 changes: 37 additions & 1 deletion imgaug/augmentables/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import cv2

from .. import imgaug as ia
from .base import IAugmentable
from .utils import (normalize_shape, project_coords, interpolate_points,
_remove_out_of_image_fraction)

Expand Down Expand Up @@ -1530,6 +1531,28 @@ def deepcopy(self, coords=None, label=None):
coords=np.copy(self.coords) if coords is None else coords,
label=copylib.deepcopy(self.label) if label is None else label)

def __getitem__(self, indices):
"""Get the coordinate(s) with given indices.

Returns
-------
ndarray
xy-coordinate(s) as ``ndarray``.

"""
return self.coords[indices]

def __iter__(self):
"""Iterate over the coordinates of this instance.

Yields
------
ndarray
An ``(2,)`` ``ndarray`` denoting an xy-coordinate pair.

"""
return iter(self.coords)

def __repr__(self):
return self.__str__()

Expand All @@ -1553,7 +1576,7 @@ def __str__(self):
# concat(other)
# is_self_intersecting()
# remove_self_intersections()
class LineStringsOnImage(object):
class LineStringsOnImage(IAugmentable):
"""Object that represents all line strings on a single image.

Parameters
Expand Down Expand Up @@ -2055,6 +2078,19 @@ def deepcopy(self, line_strings=None, shape=None):
line_strings=[ls.deepcopy() for ls in lss],
shape=tuple(shape))

def __iter__(self):
"""Iterate over the line strings in this container.

Yields
------
LineString
A line string in this container.
The order is identical to the order in the line string list
provided upon class initialization.

"""
return iter(self.line_strings)

def __repr__(self):
return self.__str__()

Expand Down
12 changes: 11 additions & 1 deletion imgaug/augmentables/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from .. import imgaug as ia
from .. import dtypes as iadt
from .base import IAugmentable


def _preprocess_shapes(shapes):
Expand Down Expand Up @@ -1206,6 +1207,15 @@ def restore_dtype_and_merge(arr, input_dtype):
return arr


def _is_iterable(obj):
return (
ia.is_iterable(obj)
and not isinstance(obj, IAugmentable) # not e.g. KeypointsOnImage
and not hasattr(obj, "coords") # not BBs, Polys, LS
and not ia.is_string(obj)
)


def find_first_nonempty(attr, parents=None):
if parents is None:
parents = []
Expand All @@ -1214,7 +1224,7 @@ def find_first_nonempty(attr, parents=None):
return attr, True, parents
# we exclude strings here, as otherwise we would get the first
# character, while we want to get the whole string
elif ia.is_iterable(attr) and not ia.is_string(attr):
elif _is_iterable(attr):
if len(attr) == 0:
return None, False, parents

Expand Down
38 changes: 37 additions & 1 deletion imgaug/augmentables/polys.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from .. import imgaug as ia
from .. import random as iarandom
from .base import IAugmentable
from .utils import (normalize_shape, interpolate_points,
_remove_out_of_image_fraction)

Expand Down Expand Up @@ -1282,6 +1283,28 @@ def deepcopy(self, exterior=None, label=None):
exterior=np.copy(self.exterior) if exterior is None else exterior,
label=self.label if label is None else label)

def __getitem__(self, indices):
"""Get the coordinate(s) with given indices.

Returns
-------
ndarray
xy-coordinate(s) as ``ndarray``.

"""
return self.exterior[indices]

def __iter__(self):
"""Iterate over the coordinates of this instance.

Yields
------
ndarray
An ``(2,)`` ``ndarray`` denoting an xy-coordinate pair.

"""
return iter(self.exterior)

def __repr__(self):
return self.__str__()

Expand All @@ -1295,7 +1318,7 @@ def __str__(self):


# TODO add tests for this
class PolygonsOnImage(object):
class PolygonsOnImage(IAugmentable):
"""Container for all polygons on a single image.

Parameters
Expand Down Expand Up @@ -1758,6 +1781,19 @@ def deepcopy(self):
polys = [poly.deepcopy() for poly in self.polygons]
return PolygonsOnImage(polys, tuple(self.shape))

def __iter__(self):
"""Iterate over the polygons in this container.

Yields
------
Polygon
A polygon in this container.
The order is identical to the order in the polygon list
provided upon class initialization.

"""
return iter(self.polygons)

def __repr__(self):
return self.__str__()

Expand Down
3 changes: 2 additions & 1 deletion imgaug/augmentables/segmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from .. import imgaug as ia
from ..augmenters import blend as blendlib
from .base import IAugmentable


@ia.deprecated(alt_func="SegmentationMapsOnImage",
Expand All @@ -13,7 +14,7 @@ def SegmentationMapOnImage(*args, **kwargs):
return SegmentationMapsOnImage(*args, **kwargs)


class SegmentationMapsOnImage(object):
class SegmentationMapsOnImage(IAugmentable):
"""
Object representing a segmentation map associated with an image.

Expand Down
Loading