1
1
import json
2
2
from dataclasses import dataclass , field
3
3
from enum import Enum
4
- from typing import Dict , List , Optional , Sequence , Union
4
+ from typing import Dict , List , Optional , Sequence , Type , Union
5
5
from urllib .parse import urlparse
6
6
7
7
from .constants import (
16
16
INDEX_KEY ,
17
17
LABEL_KEY ,
18
18
LABELS_KEY ,
19
+ LINE_TYPE ,
19
20
MASK_TYPE ,
20
21
MASK_URL_KEY ,
21
22
METADATA_KEY ,
@@ -46,18 +47,17 @@ class Annotation:
46
47
@classmethod
47
48
def from_json (cls , payload : dict ):
48
49
"""Instantiates annotation object from schematized JSON dict payload."""
49
- if payload .get (TYPE_KEY , None ) == BOX_TYPE :
50
- return BoxAnnotation .from_json (payload )
51
- elif payload .get (TYPE_KEY , None ) == POLYGON_TYPE :
52
- return PolygonAnnotation .from_json (payload )
53
- elif payload .get (TYPE_KEY , None ) == CUBOID_TYPE :
54
- return CuboidAnnotation .from_json (payload )
55
- elif payload .get (TYPE_KEY , None ) == CATEGORY_TYPE :
56
- return CategoryAnnotation .from_json (payload )
57
- elif payload .get (TYPE_KEY , None ) == MULTICATEGORY_TYPE :
58
- return MultiCategoryAnnotation .from_json (payload )
59
- else :
60
- return SegmentationAnnotation .from_json (payload )
50
+ type_key_to_type : Dict [str , Type [Annotation ]] = {
51
+ BOX_TYPE : BoxAnnotation ,
52
+ LINE_TYPE : LineAnnotation ,
53
+ POLYGON_TYPE : PolygonAnnotation ,
54
+ CUBOID_TYPE : CuboidAnnotation ,
55
+ CATEGORY_TYPE : CategoryAnnotation ,
56
+ MULTICATEGORY_TYPE : MultiCategoryAnnotation ,
57
+ }
58
+ type_key = payload .get (TYPE_KEY , None )
59
+ AnnotationCls = type_key_to_type .get (type_key , SegmentationAnnotation )
60
+ return AnnotationCls .from_json (payload )
61
61
62
62
def to_payload (self ) -> dict :
63
63
"""Serializes annotation object to schematized JSON dict."""
@@ -177,6 +177,88 @@ def to_payload(self) -> dict:
177
177
return {X_KEY : self .x , Y_KEY : self .y }
178
178
179
179
180
+ @dataclass
181
+ class LineAnnotation (Annotation ):
182
+ """A polyline annotation consisting of an ordered list of 2D points.
183
+ A LineAnnotation differs from a PolygonAnnotation by not forming a closed
184
+ loop, and by having zero area.
185
+
186
+ ::
187
+
188
+ from nucleus import LineAnnotation
189
+
190
+ line = LineAnnotation(
191
+ label="face",
192
+ vertices=[Point(100, 100), Point(200, 300), Point(300, 200)],
193
+ reference_id="person_image_1",
194
+ annotation_id="person_image_1_line_1",
195
+ metadata={"camera_mode": "portrait"},
196
+ )
197
+
198
+ Parameters:
199
+ label (str): The label for this annotation.
200
+ vertices (List[:class:`Point`]): The list of points making up the line.
201
+ reference_id (str): User-defined ID of the image to which to apply this
202
+ annotation.
203
+ annotation_id (Optional[str]): The annotation ID that uniquely identifies
204
+ this annotation within its target dataset item. Upon ingest, a matching
205
+ annotation id will be ignored by default, and updated if update=True
206
+ for dataset.annotate.
207
+ metadata (Optional[Dict]): Arbitrary key/value dictionary of info to
208
+ attach to this annotation. Strings, floats and ints are supported best
209
+ by querying and insights features within Nucleus. For more details see
210
+ our `metadata guide <https://nucleus.scale.com/docs/upload-metadata>`_.
211
+ """
212
+
213
+ label : str
214
+ vertices : List [Point ]
215
+ reference_id : str
216
+ annotation_id : Optional [str ] = None
217
+ metadata : Optional [Dict ] = None
218
+
219
+ def __post_init__ (self ):
220
+ self .metadata = self .metadata if self .metadata else {}
221
+ if len (self .vertices ) > 0 :
222
+ if not hasattr (self .vertices [0 ], X_KEY ) or not hasattr (
223
+ self .vertices [0 ], "to_payload"
224
+ ):
225
+ try :
226
+ self .vertices = [
227
+ Point (x = vertex [X_KEY ], y = vertex [Y_KEY ])
228
+ for vertex in self .vertices
229
+ ]
230
+ except KeyError as ke :
231
+ raise ValueError (
232
+ "Use a point object to pass in vertices. For example, vertices=[nucleus.Point(x=1, y=2)]"
233
+ ) from ke
234
+
235
+ @classmethod
236
+ def from_json (cls , payload : dict ):
237
+ geometry = payload .get (GEOMETRY_KEY , {})
238
+ return cls (
239
+ label = payload .get (LABEL_KEY , 0 ),
240
+ vertices = [
241
+ Point .from_json (_ ) for _ in geometry .get (VERTICES_KEY , [])
242
+ ],
243
+ reference_id = payload [REFERENCE_ID_KEY ],
244
+ annotation_id = payload .get (ANNOTATION_ID_KEY , None ),
245
+ metadata = payload .get (METADATA_KEY , {}),
246
+ )
247
+
248
+ def to_payload (self ) -> dict :
249
+ payload = {
250
+ LABEL_KEY : self .label ,
251
+ TYPE_KEY : LINE_TYPE ,
252
+ GEOMETRY_KEY : {
253
+ VERTICES_KEY : [_ .to_payload () for _ in self .vertices ]
254
+ },
255
+ REFERENCE_ID_KEY : self .reference_id ,
256
+ ANNOTATION_ID_KEY : self .annotation_id ,
257
+ METADATA_KEY : self .metadata ,
258
+ }
259
+ return payload
260
+
261
+
180
262
@dataclass
181
263
class PolygonAnnotation (Annotation ):
182
264
"""A polygon annotation consisting of an ordered list of 2D points.
@@ -499,6 +581,7 @@ def to_payload(self) -> dict:
499
581
500
582
class AnnotationTypes (Enum ):
501
583
BOX = BOX_TYPE
584
+ LINE = LINE_TYPE
502
585
POLYGON = POLYGON_TYPE
503
586
CUBOID = CUBOID_TYPE
504
587
CATEGORY = CATEGORY_TYPE
@@ -600,6 +683,7 @@ class AnnotationList:
600
683
"""Wrapper class separating a list of annotations by type."""
601
684
602
685
box_annotations : List [BoxAnnotation ] = field (default_factory = list )
686
+ line_annotations : List [LineAnnotation ] = field (default_factory = list )
603
687
polygon_annotations : List [PolygonAnnotation ] = field (default_factory = list )
604
688
cuboid_annotations : List [CuboidAnnotation ] = field (default_factory = list )
605
689
category_annotations : List [CategoryAnnotation ] = field (
@@ -620,6 +704,8 @@ def add_annotations(self, annotations: List[Annotation]):
620
704
621
705
if isinstance (annotation , BoxAnnotation ):
622
706
self .box_annotations .append (annotation )
707
+ elif isinstance (annotation , LineAnnotation ):
708
+ self .line_annotations .append (annotation )
623
709
elif isinstance (annotation , PolygonAnnotation ):
624
710
self .polygon_annotations .append (annotation )
625
711
elif isinstance (annotation , CuboidAnnotation ):
@@ -637,6 +723,7 @@ def add_annotations(self, annotations: List[Annotation]):
637
723
def __len__ (self ):
638
724
return (
639
725
len (self .box_annotations )
726
+ + len (self .line_annotations )
640
727
+ len (self .polygon_annotations )
641
728
+ len (self .cuboid_annotations )
642
729
+ len (self .category_annotations )
0 commit comments