diff --git a/tensorbay/geometry/polygon.py b/tensorbay/geometry/polygon.py index fe81d689e..0d3f511aa 100644 --- a/tensorbay/geometry/polygon.py +++ b/tensorbay/geometry/polygon.py @@ -94,7 +94,7 @@ class MultiPolygon(MultiPointList2D[Polygon]): # pylint: disable=too-many-ances Examples: >>> MultiPolygon([[[1.0, 4.0], [2.0, 3.7], [7.0, 4.0]], - [[5.0, 7.0], [6.0, 7.0], [9.0, 8.0]]]) + ... [[5.0, 7.0], [6.0, 7.0], [9.0, 8.0]]]) MultiPolygon [ Polygon [...] Polygon [...] @@ -122,7 +122,7 @@ def loads(cls: Type[_P], contents: List[List[Dict[str, float]]]) -> _P: Examples: >>> contents = [[{'x': 1.0, 'y': 4.0}, {'x': 2.0, 'y': 3.7}, {'x': 7.0, 'y': 4.0}], - [{'x': 5.0, 'y': 7.0}, {'x': 6.0, 'y': 7.0}, {'x': 9.0, 'y': 8.0}]] + ... [{'x': 5.0, 'y': 7.0}, {'x': 6.0, 'y': 7.0}, {'x': 9.0, 'y': 8.0}]] >>> multipolygon = MultiPolygon.loads(contents) >>> multipolygon MultiPolygon [ @@ -142,7 +142,7 @@ def dumps(self) -> List[List[Dict[str, float]]]: Examples: >>> multipolygon = MultiPolygon([[[1.0, 4.0], [2.0, 3.7], [7.0, 4.0]], - >>> [[5.0, 7.0], [6.0, 7.0], [9.0, 8.0]]]) + ... [[5.0, 7.0], [6.0, 7.0], [9.0, 8.0]]]) >>> multipolygon.dumps() [ [{'x': 1.0, 'y': 4.0}, {'x': 2.0, 'y': 3.7}, {'x': 7.0, 'y': 4.0}], diff --git a/tensorbay/label/__init__.py b/tensorbay/label/__init__.py index c2d544cb8..6bc9d6aba 100644 --- a/tensorbay/label/__init__.py +++ b/tensorbay/label/__init__.py @@ -12,7 +12,12 @@ from .label_box import Box2DSubcatalog, Box3DSubcatalog, LabeledBox2D, LabeledBox3D from .label_classification import Classification, ClassificationSubcatalog from .label_keypoints import Keypoints2DSubcatalog, LabeledKeypoints2D -from .label_polygon import LabeledPolygon, PolygonSubcatalog +from .label_polygon import ( + LabeledMultiPolygon, + LabeledPolygon, + MultiPolygonSubcatalog, + PolygonSubcatalog, +) from .label_polyline import ( LabeledMultiPolyline2D, LabeledPolyline2D, @@ -30,7 +35,6 @@ "CategoryInfo", "Classification", "ClassificationSubcatalog", - "Items", "Keypoints2DSubcatalog", "KeypointsInfo", "Label", @@ -43,6 +47,9 @@ "LabeledPolyline2D", "LabeledSentence", "MultiPolyline2DSubcatalog", + "LabeledMultiPolygon", + "Items", + "MultiPolygonSubcatalog", "PolygonSubcatalog", "Polyline2DSubcatalog", "SentenceSubcatalog", diff --git a/tensorbay/label/basic.py b/tensorbay/label/basic.py index 0b5e33fd3..25dee37aa 100644 --- a/tensorbay/label/basic.py +++ b/tensorbay/label/basic.py @@ -44,6 +44,7 @@ class LabelType(TypeEnum): POLYLINE2D = "polyline2d" MULTI_POLYLINE2D = "multi_polyline2d" KEYPOINTS2D = "keypoints2d" + MULTI_POLYGON = "multi_polygon" SENTENCE = "sentence" @property diff --git a/tensorbay/label/catalog.py b/tensorbay/label/catalog.py index 3f21f40f5..2284e7e31 100644 --- a/tensorbay/label/catalog.py +++ b/tensorbay/label/catalog.py @@ -24,6 +24,7 @@ :class:`.Keypoints2DSubcatalog` subcatalog for 2D keypoints type of label :class:`.PolygonSubcatalog` subcatalog for polygon type of label :class:`.Polyline2DSubcatalog` subcatalog for 2D polyline type of label + :class:`.MultiPolygonSubcatalog` subcatalog for multiple polygon type of label :class:`.MultiPolyline2DSubcatalog` subcatalog for 2D multiple polyline type of label :class:`.SentenceSubcatalog` subcatalog for transcripted sentence type of label =================================== ================================================== @@ -38,7 +39,7 @@ from .label_box import Box2DSubcatalog, Box3DSubcatalog from .label_classification import ClassificationSubcatalog from .label_keypoints import Keypoints2DSubcatalog -from .label_polygon import PolygonSubcatalog +from .label_polygon import MultiPolygonSubcatalog, PolygonSubcatalog from .label_polyline import MultiPolyline2DSubcatalog, Polyline2DSubcatalog from .label_sentence import SentenceSubcatalog @@ -50,6 +51,7 @@ Polyline2DSubcatalog, MultiPolyline2DSubcatalog, Keypoints2DSubcatalog, + MultiPolygonSubcatalog, SentenceSubcatalog, ] @@ -101,6 +103,7 @@ class Catalog(ReprMixin, AttrsMixin): polyline2d: Polyline2DSubcatalog = _attr() multi_polyline2d: MultiPolyline2DSubcatalog = _attr() keypoints2d: Keypoints2DSubcatalog = _attr() + multi_polygon: MultiPolygonSubcatalog = _attr() sentence: SentenceSubcatalog = _attr() def __bool__(self) -> bool: diff --git a/tensorbay/label/label.py b/tensorbay/label/label.py index 9b39ffcac..600e09dc2 100644 --- a/tensorbay/label/label.py +++ b/tensorbay/label/label.py @@ -35,7 +35,7 @@ from .label_box import LabeledBox2D, LabeledBox3D from .label_classification import Classification from .label_keypoints import LabeledKeypoints2D -from .label_polygon import LabeledPolygon +from .label_polygon import LabeledMultiPolygon, LabeledPolygon from .label_polyline import LabeledMultiPolyline2D, LabeledPolyline2D from .label_sentence import LabeledSentence @@ -75,6 +75,7 @@ class Label(ReprMixin, AttrsMixin): polyline2d: List[LabeledPolyline2D] = _attr() multi_polyline2d: List[LabeledMultiPolyline2D] = _attr() keypoints2d: List[LabeledKeypoints2D] = _attr() + multi_polygon: List[LabeledMultiPolygon] = _attr() sentence: List[LabeledSentence] = _attr() def __bool__(self) -> bool: diff --git a/tensorbay/label/label_polygon.py b/tensorbay/label/label_polygon.py index edb57bf0b..ea77f770f 100644 --- a/tensorbay/label/label_polygon.py +++ b/tensorbay/label/label_polygon.py @@ -14,7 +14,7 @@ from typing import Any, Dict, Iterable, Optional, Type, TypeVar -from ..geometry import Polygon +from ..geometry import MultiPolygon, Polygon from ..utility import ReprType, SubcatalogTypeRegister, TypeRegister, attr_base, common_loads from .basic import LabelType, SubcatalogBase, _LabelBase from .supports import AttributesMixin, CategoriesMixin, IsTrackingMixin @@ -86,6 +86,68 @@ def __init__(self, is_tracking: bool = False) -> None: IsTrackingMixin.__init__(self, is_tracking) +@SubcatalogTypeRegister(LabelType.MULTI_POLYGON) +class MultiPolygonSubcatalog( # pylint: disable=too-many-ancestors + SubcatalogBase, IsTrackingMixin, CategoriesMixin, AttributesMixin +): + """This class defines the subcatalog for multiple polygon type of labels. + + Arguments: + is_tracking: A boolean value indicates whether the corresponding + subcatalog contains tracking information. + + Attributes: + description: The description of the entire multiple polygon subcatalog. + categories: All the possible categories in the corresponding dataset + stored in a :class:`~tensorbay.utility.name.NameList` + with the category names as keys + and the :class:`~tensorbay.label.supports.CategoryInfo` as values. + category_delimiter: The delimiter in category values indicating parent-child relationship. + attributes: All the possible attributes in the corresponding dataset + stored in a :class:`~tensorbay.utility.name.NameList` + with the attribute names as keys + and the :class:`~tensorbay.label.attribute.AttributeInfo` as values. + is_tracking: Whether the Subcatalog contains tracking information. + + Examples: + *Initialization Method 1:* Init from ``MultiPolygonSubcatalog.loads()`` method. + + >>> catalog = { + ... "MULTI_POLYGON": { + ... "isTracking": True, + ... "categories": [{"name": "0"}, {"name": "1"}], + ... "attributes": [{"name": "gender", "enum": ["male", "female"]}], + ... } + ... } + >>> MultiPolygonSubcatalog.loads(catalog["MULTI_POLYGON"]) + MultiPolygonSubcatalog( + (is_tracking): True, + (categories): NameList [...], + (attributes): NameList [...] + ) + + *Initialization Method 2:* Init an empty MultiPolygonSubcatalog + and then add the attributes. + + >>> from tensorbay.label import CategoryInfo, AttributeInfo + >>> multi_polygon_subcatalog = MultiPolygonSubcatalog() + >>> multi_polygon_subcatalog.is_tracking = True + >>> multi_polygon_subcatalog.add_category("a") + >>> multi_polygon_subcatalog.add_attribute("gender", enum=["female", "male"]) + >>> multi_polygon_subcatalog + MultiPolyline2DSubcatalog( + (is_tracking): True, + (categories): NameList [...], + (attributes): NameList [...] + ) + + """ + + def __init__(self, is_tracking: bool = False) -> None: + SubcatalogBase.__init__(self) + IsTrackingMixin.__init__(self, is_tracking) + + @TypeRegister(LabelType.POLYGON) class LabeledPolygon(_LabelBase, Polygon): # pylint: disable=too-many-ancestors """This class defines the concept of polygon label. @@ -198,3 +260,122 @@ def dumps(self) -> Dict[str, Any]: # type: ignore[override] """ return self._dumps() + + +@TypeRegister(LabelType.MULTI_POLYGON) +class LabeledMultiPolygon( # type: ignore[misc] + _LabelBase, MultiPolygon +): # pylint: disable=too-many-ancestors + """This class defines the concept of multiple polygon label. + + :class:`LabeledMultiPolygon` is the multipolygon type of label, + which is often used for CV tasks such as semantic segmentation. + + Arguments: + points: A list of 2D points representing the vertices of the polygon. + category: The category of the label. + attributes: The attributs of the label. + instance: The instance id of the label. + + Attributes: + category: The category of the label. + attributes: The attributes of the label. + instance: The instance id of the label. + + Examples: + >>> LabeledMultiPolygon( + ... [[(1.0, 2.0), (2.0, 3.0), (1.0, 3.0)], [(1.0, 4.0), (2.0, 3.0), (1.0, 8.0)]], + ... category = "example", + ... attributes = {"key": "value"}, + ... instance = "12345", + ... ) + LabeledMultiPolygon [ + Polygon [...], + Polygon [...] + ]( + (category): 'example', + (attributes): {...}, + (instance): '12345' + ) + + """ + + _T = TypeVar("_T", bound="LabeledMultiPolygon") + _repr_type = ReprType.SEQUENCE + _repr_attrs = _LabelBase._repr_attrs + _attrs_base: MultiPolygon = attr_base(key="multiPolygon") + + def __init__( + self, + polygons: Optional[Iterable[Iterable[Iterable[float]]]] = None, + *, + category: Optional[str] = None, + attributes: Optional[Dict[str, Any]] = None, + instance: Optional[str] = None, + ): + MultiPolygon.__init__(self, polygons=polygons) + _LabelBase.__init__(self, category, attributes, instance) + + @classmethod + def loads(cls: Type[_T], contents: Dict[str, Any]) -> _T: # type: ignore[override] + """Loads a LabeledMultiPolygon from a list of dict containing the information of the label. + + Arguments: + contents: A dict containing the information of the multipolygon label. + + Returns: + The loaded :class:`LabeledMultiPolygon` object. + + Examples: + >>> contents = { + ... "multiPolygon": [ + ... [ + ... {"x": 1.0, "y": 2.0}, + ... {"x": 2.0, "y": 3.0}, + ... {"x": 1.0, "y": 3.0}, + ... ], + ... [{"x": 1.0, "y": 4.0}, {"x": 2.0, "y": 3.0}, {"x": 1.0, "y": 8.0}], + ... ], + ... "category": "example", + ... "attributes": {"key": "value"}, + ... "instance": "12345", + ... } + >>> LabeledMultiPolygon.loads(contents) + LabeledMultiPolygon [ + Polygon [...], + Polygon [...] + ]( + (category): 'example', + (attributes): {...}, + (instance): '12345' + ) + + """ + return common_loads(cls, contents) + + def dumps(self) -> Dict[str, Any]: # type: ignore[override] + """Dumps the current multipolygon label into a dict. + + Returns: + A dict containing all the information of the multipolygon label. + + Examples: + >>> labeledmultipolygon = LabeledMultiPolygon( + ... [[(1, 2), (2, 3), (1, 3)],[(1, 2), (2, 3), (1, 3)]], + ... category = "example", + ... attributes = {"key": "value"}, + ... instance = "123", + ... ) + >>> labeledmultipolygon.dumps() + { + 'category': 'example', + 'attributes': {'key': 'value'}, + 'instance': '123', + 'multiPolygon': [ + [{'x': 1, 'y': 2}, {'x': 2, 'y': 3}, {'x': 1, 'y': 3}], + [{"x": 1.0, "y": 4.0}, {"x": 2.0, "y": 3.0}, {"x": 1.0, "y": 8.0}] + ] + } + + """ + return self._dumps() diff --git a/tensorbay/label/tests/test_label_polygon.py b/tensorbay/label/tests/test_label_polygon.py index 93a3edbb7..5da42e643 100644 --- a/tensorbay/label/tests/test_label_polygon.py +++ b/tensorbay/label/tests/test_label_polygon.py @@ -5,8 +5,22 @@ import pytest -from ...geometry import Vector2D -from .. import LabeledPolygon, PolygonSubcatalog +from ...geometry import Polygon, Vector2D +from .. import LabeledMultiPolygon, LabeledPolygon, MultiPolygonSubcatalog, PolygonSubcatalog + +_DATA_LABELEDMULTIPOLYGON = { + "multiPolygon": [ + [ + {"x": 1.0, "y": 2.0}, + {"x": 2.0, "y": 3.0}, + {"x": 1.0, "y": 3.0}, + ], + [{"x": 1.0, "y": 4.0}, {"x": 2.0, "y": 3.0}, {"x": 1.0, "y": 8.0}], + ], + "category": "example", + "attributes": {"key": "value"}, + "instance": "123", +} @pytest.fixture @@ -109,3 +123,78 @@ def test_dumps(self, categories, attributes, subcatalog_polygon): del subcatalog_polygon["isTracking"] assert subcatalog.dumps() == subcatalog_polygon + + +class TestLabeledMultiPolygon: + def test_init(self): + labeledmultipolygon = LabeledMultiPolygon( + [[(1.0, 2.0), (2.0, 3.0), (1.0, 3.0)], [(1.0, 4.0), (2.0, 3.0), (1.0, 8.0)]], + category="example", + attributes={"key": "value"}, + instance="123", + ) + assert labeledmultipolygon[0] == Polygon([(1.0, 2.0), (2.0, 3.0), (1.0, 3.0)]) + assert labeledmultipolygon.category == "example" + assert labeledmultipolygon.attributes == {"key": "value"} + assert labeledmultipolygon.instance == "123" + + def test_loads(self): + labeledmultipolygon = LabeledMultiPolygon.loads(_DATA_LABELEDMULTIPOLYGON) + assert labeledmultipolygon == LabeledMultiPolygon( + [[(1.0, 2.0), (2.0, 3.0), (1.0, 3.0)], [(1.0, 4.0), (2.0, 3.0), (1.0, 8.0)]], + category="example", + attributes={"key": "value"}, + instance="123", + ) + + def test_dumps(self): + labeledmultipolygon = LabeledMultiPolygon( + [[(1.0, 2.0), (2.0, 3.0), (1.0, 3.0)], [(1.0, 4.0), (2.0, 3.0), (1.0, 8.0)]], + category="example", + attributes={"key": "value"}, + instance="123", + ) + assert labeledmultipolygon.dumps() == _DATA_LABELEDMULTIPOLYGON + + +class TestMultiPolygonSubcatalog: + def test_init(self, is_tracking_data): + multi_polygon_subcatalog = MultiPolygonSubcatalog(is_tracking_data) + assert multi_polygon_subcatalog.is_tracking == is_tracking_data + + def test_eq(self): + contents1 = { + "isTracking": True, + "categories": [{"name": "0"}, {"name": "1"}], + "attributes": [{"name": "gender", "enum": ["male", "female"]}], + } + contents2 = { + "isTracking": False, + "categories": [{"name": "0"}, {"name": "1"}], + "attributes": [{"name": "gender", "enum": ["male", "female"]}], + } + multi_polygon_subcatalog1 = MultiPolygonSubcatalog.loads(contents1) + multi_polygon_subcatalog2 = MultiPolygonSubcatalog.loads(contents1) + multi_polygon_subcatalog3 = MultiPolygonSubcatalog.loads(contents2) + + assert multi_polygon_subcatalog1 == multi_polygon_subcatalog2 + assert multi_polygon_subcatalog1 != multi_polygon_subcatalog3 + + def test_loads(self, categories, attributes, subcatalog_polygon): + subcatalog = MultiPolygonSubcatalog.loads(subcatalog_polygon) + + assert subcatalog.is_tracking == subcatalog_polygon["isTracking"] + assert subcatalog.categories == categories + assert subcatalog.attributes == attributes + + def test_dumps(self, categories, attributes, subcatalog_polygon): + subcatalog = MultiPolygonSubcatalog() + subcatalog.is_tracking = subcatalog_polygon["isTracking"] + subcatalog.categories = categories + subcatalog.attributes = attributes + + # isTracking will not dump out when isTracking is false + if not subcatalog_polygon["isTracking"]: + del subcatalog_polygon["isTracking"] + + assert subcatalog.dumps() == subcatalog_polygon diff --git a/tests/test_label.py b/tests/test_label.py index ba2cdd879..59574e434 100644 --- a/tests/test_label.py +++ b/tests/test_label.py @@ -26,8 +26,26 @@ {"name": "occluded", "type": "integer", "minimum": 1, "maximum": 5}, ], } +MULTI_POLYGON_CATALOG = { + "isTracking": True, + "categories": [ + { + "name": "123", + "description": "This is another exmaple of test", + }, + { + "name": "234", + "description": "This is anpther exmaple of test", + }, + ], + "attributes": [ + {"name": "gender", "enum": ["male", "female"]}, + {"name": "occluded", "type": "integer", "minimum": 2, "maximum": 8}, + ], +} CATALOG_CONTENTS = { "MULTI_POLYLINE2D": MULTI_POLYLINE2D_CATALOG, + "MULTI_POLYGON": MULTI_POLYGON_CATALOG, } MULTI_POLYLINE2D_LABEL = [ @@ -41,8 +59,24 @@ ], } ] +MULTI_POLYGON_LABEL = [ + { + "multiPolygon": [ + [ + {"x": 1.0, "y": 2.0}, + {"x": 2.0, "y": 3.0}, + {"x": 1.0, "y": 3.0}, + ], + [{"x": 1.0, "y": 4.0}, {"x": 2.0, "y": 3.0}, {"x": 1.0, "y": 8.0}], + ], + "category": "example", + "attributes": {"key": "value"}, + "instance": "123", + } +] LABEL = { "MULTI_POLYLINE2D": MULTI_POLYLINE2D_LABEL, + "MULTI_POLYGON": MULTI_POLYGON_LABEL, }