-
Notifications
You must be signed in to change notification settings - Fork 512
/
labels.py
1934 lines (1484 loc) · 59.2 KB
/
labels.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Labels stored in dataset samples.
| Copyright 2017-2023, Voxel51, Inc.
| `voxel51.com <https://voxel51.com/>`_
|
"""
import itertools
import warnings
from bson import ObjectId
import cv2
import numpy as np
import eta.core.frameutils as etaf
import eta.core.image as etai
import eta.core.utils as etau
from fiftyone.core.odm import DynamicEmbeddedDocument
import fiftyone.core.fields as fof
import fiftyone.core.metadata as fom
import fiftyone.core.utils as fou
foue = fou.lazy_import("fiftyone.utils.eta")
foug = fou.lazy_import("fiftyone.utils.geojson")
foui = fou.lazy_import("fiftyone.utils.image")
sg = fou.lazy_import(
"shapely.geometry", callback=lambda: fou.ensure_package("shapely")
)
class _NoDefault(object):
pass
no_default = _NoDefault()
class Label(DynamicEmbeddedDocument):
"""Base class for labels.
Label instances represent a logical collection of data associated with a
particular task for a sample or frame in a dataset.
"""
def iter_attributes(self):
"""Returns an iterator over the custom attributes of the label.
Returns:
a generator that emits ``(name, value)`` tuples
"""
# pylint: disable=no-member
custom_fields = set(self._fields_ordered) - set(self._fields.keys())
for field in custom_fields:
yield field, self.get_attribute_value(field)
def has_attribute(self, name):
"""Determines whether the label has an attribute with the given name.
Args:
name: the attribute name
Returns:
True/False
"""
return hasattr(self, name)
def get_attribute_value(self, name, default=no_default):
"""Gets the value of the attribute with the given name.
Args:
name: the attribute name
default (no_default): a default value to return if the attribute
does not exist. Can be ``None``
Returns:
the attribute value
Raises:
AttributeError: if the attribute does not exist and no default
value was provided
"""
try:
return getattr(self, name)
except AttributeError:
pass
if default is not no_default:
return default
raise AttributeError(
"%s has no attribute '%s'" % (self.__class__.__name__, name)
)
def set_attribute_value(self, name, value):
"""Sets the value of the attribute with the given name.
The attribute will be declared if it does not exist.
Args:
name: the attribute name
value: the value
"""
setattr(self, name, value)
def delete_attribute(self, name):
"""Deletes the attribute with the given name.
Args:
name: the attribute name
Raises:
AttributeError: if the attribute does not exist
"""
try:
delattr(self, name)
except AttributeError:
raise AttributeError(
"%s has no attribute '%s'" % (self.__class__.__name__, name)
)
# @todo remove this in favor of dynamic-only attributes
class Attribute(DynamicEmbeddedDocument):
"""Base class for attributes.
Attribute instances represent an atomic piece of information, its
``value``, usually embedded with a ``name`` within a dict field of another
:class:`Label` instance.
Args:
value (None): the attribute value
"""
value = fof.Field()
# @todo remove this in favor of dynamic-only attributes
class BooleanAttribute(Attribute):
"""A boolean attribute.
Args:
value (None): the attribute value
"""
value = fof.BooleanField()
# @todo remove this in favor of dynamic-only attributes
class CategoricalAttribute(Attribute):
"""A categorical attribute.
Args:
value (None): the attribute value
confidence (None): a confidence in ``[0, 1]`` for the value
logits (None): logits associated with the attribute
"""
value = fof.StringField()
confidence = fof.FloatField()
logits = fof.VectorField()
# @todo remove this in favor of dynamic-only attributes
class NumericAttribute(Attribute):
"""A numeric attribute.
Args:
value (None): the attribute value
"""
value = fof.FloatField()
# @todo remove this in favor of dynamic-only attributes
class ListAttribute(Attribute):
"""A list attribute.
The list can store arbitrary JSON-serialiable values.
Args:
value (None): the attribute value
"""
value = fof.ListField()
# @todo remove this in favor of dynamic-only attributes
class _HasAttributesDict(Label):
"""Mixin for :class:`Label` classes that have an :attr:`attributes` field
that contains a dict of :class:`Attribute` instances.
"""
attributes = fof.DictField(fof.EmbeddedDocumentField(Attribute))
def iter_attributes(self):
"""Returns an iterator over the custom attributes of the label.
Attribute may either exist in the :attr:`attributes` dict or as dynamic
attributes.
Returns:
a generator that emits ``(name, value)`` tuples
"""
# pylint: disable=no-member
custom_fields = set(self._fields_ordered) - set(self._fields.keys())
custom_fields.update(self.attributes.keys())
for field in custom_fields:
yield field, self.get_attribute_value(field)
def has_attribute(self, name):
"""Determines whether the label has an attribute with the given name.
The specified attribute may either exist in the :attr:`attributes` dict
or as a dynamic attribute.
Args:
name: the attribute name
Returns:
True/False
"""
# pylint: disable=unsupported-membership-test
return name in self.attributes or hasattr(self, name)
def get_attribute_value(self, name, default=no_default):
"""Gets the value of the attribute with the given name.
The specified attribute may either exist in the :attr:`attributes` dict
or as a dynamic attribute.
Args:
name: the attribute name
default (no_default): a default value to return if the attribute
does not exist. Can be ``None``
Returns:
the attribute value
Raises:
AttributeError: if the attribute does not exist and no default
value was provided
"""
try:
return getattr(self, name)
except AttributeError:
pass
try:
# pylint: disable=unsubscriptable-object
return self.attributes[name].value
except KeyError:
pass
if default is not no_default:
return default
raise AttributeError(
"%s has no attribute '%s'" % (self.__class__.__name__, name)
)
def set_attribute_value(self, name, value):
"""Sets the value of the attribute with the given name.
If the specified attribute already exists in the :attr:`attributes`
dict, its value is updated there. Otherwise, the attribute is
set (or created) as a dynamic attribute.
Args:
name: the attribute name
value: the value
"""
# pylint: disable=unsupported-membership-test
if name in self.attributes:
# pylint: disable=unsubscriptable-object
self.attributes[name].value = value
else:
setattr(self, name, value)
def delete_attribute(self, name):
"""Deletes the attribute with the given name.
The specified attribute may either exist in the :attr:`attributes` dict
or as a dynamic attribute.
Args:
name: the attribute name
Raises:
AttributeError: if the attribute does not exist
"""
# pylint: disable=unsupported-membership-test
if name in self.attributes:
# pylint: disable=unsupported-delete-operation
try:
del self.attributes[name]
except KeyError:
raise AttributeError(
"%s has no attribute '%s'"
% (self.__class__.__name__, name)
)
else:
try:
delattr(self, name)
except AttributeError:
raise AttributeError(
"%s has no attribute '%s'"
% (self.__class__.__name__, name)
)
class _HasID(Label):
"""Mixin for :class:`Label` classes that expose a UUID via an ``id``
property, as well as a ``tags`` attribute.
"""
id = fof.ObjectIdField(
required=True,
unique=True,
default=lambda: str(ObjectId()),
db_field="_id",
)
tags = fof.ListField(fof.StringField())
@property
def _id(self):
return ObjectId(self.id)
@_id.setter
def _id(self, value):
self.id = str(value)
class _HasLabelList(object):
"""Mixin for :class:`Label` classes that contain a list of :class:`Label`
instances.
The ``_LABEL_LIST_FIELD`` attribute must be defined to specify the name of
the field that contains the :class:`Label` elements.
"""
_LABEL_LIST_FIELD = None
class Regression(_HasID, Label):
"""A regression value.
Args:
value (None): the regression value
confidence (None): a confidence in ``[0, 1]`` for the regression
"""
value = fof.FloatField()
confidence = fof.FloatField()
class Classification(_HasID, Label):
"""A classification label.
Args:
label (None): the label string
confidence (None): a confidence in ``[0, 1]`` for the classification
logits (None): logits associated with the labels
"""
label = fof.StringField()
confidence = fof.FloatField()
logits = fof.VectorField()
class Classifications(_HasLabelList, Label):
"""A list of classifications for an image.
Args:
classifications (None): a list of :class:`Classification` instances
logits (None): logits associated with the labels
"""
_LABEL_LIST_FIELD = "classifications"
classifications = fof.ListField(fof.EmbeddedDocumentField(Classification))
logits = fof.VectorField()
class Detection(_HasAttributesDict, _HasID, Label):
"""An object detection.
Args:
label (None): the label string
bounding_box (None): a list of relative bounding box coordinates in
``[0, 1]`` in the following format::
[<top-left-x>, <top-left-y>, <width>, <height>]
mask (None): an instance segmentation mask for the detection within
its bounding box, which should be a 2D binary or 0/1 integer numpy
array
confidence (None): a confidence in ``[0, 1]`` for the detection
index (None): an index for the object
attributes ({}): a dict mapping attribute names to :class:`Attribute`
instances
"""
label = fof.StringField()
bounding_box = fof.ListField(fof.FloatField())
mask = fof.ArrayField()
confidence = fof.FloatField()
index = fof.IntField()
def to_polyline(self, tolerance=2, filled=True):
"""Returns a :class:`Polyline` representation of this instance.
If the detection has a mask, the returned polyline will trace the
boundary of the mask; otherwise, the polyline will trace the bounding
box itself.
Args:
tolerance (2): a tolerance, in pixels, when generating an
approximate polyline for the instance mask. Typical values are
1-3 pixels
filled (True): whether the polyline should be filled
Returns:
a :class:`Polyline`
"""
dobj = foue.to_detected_object(self, extra_attrs=False)
polyline = etai.convert_object_to_polygon(
dobj, tolerance=tolerance, filled=filled
)
attributes = dict(self.iter_attributes())
return Polyline(
label=self.label,
points=polyline.points,
confidence=self.confidence,
index=self.index,
closed=polyline.closed,
filled=polyline.filled,
tags=self.tags,
**attributes,
)
def to_segmentation(self, mask=None, frame_size=None, target=255):
"""Returns a :class:`Segmentation` representation of this instance.
The detection must have an instance mask, i.e., its :attr:`mask`
attribute must be populated.
You must provide either ``mask`` or ``frame_size`` to use this method.
Args:
mask (None): an optional numpy array to use as an initial mask to
which to add this object
frame_size (None): the ``(width, height)`` of the segmentation
mask to render. This parameter has no effect if a ``mask`` is
provided
target (255): the pixel value or RGB hex string to use to render
the object
Returns:
a :class:`Segmentation`
"""
if self.mask is None:
raise ValueError(
"Only detections with their `mask` attributes populated can "
"be converted to segmentations"
)
mask, target = _parse_segmentation_target(mask, frame_size, target)
_render_instance(mask, self, target)
return Segmentation(mask=mask)
def to_shapely(self, frame_size=None):
"""Returns a Shapely representation of this instance.
Args:
frame_size (None): the ``(width, height)`` of the image. If
provided, the returned geometry will use absolute coordinates
Returns:
a ``shapely.geometry.polygon.Polygon``
"""
# pylint: disable=unpacking-non-sequence
x, y, w, h = self.bounding_box
if frame_size is not None:
width, height = frame_size
x *= width
y *= height
w *= width
h *= height
return sg.box(x, y, x + w, y + h)
@classmethod
def from_mask(cls, mask, label=None, **attributes):
"""Creates a :class:`Detection` instance with its ``mask`` attribute
populated from the given full image mask.
The instance mask for the object is extracted by computing the bounding
rectangle of the non-zero values in the image mask.
Args:
mask: a boolean or 0/1 numpy array
label (None): the label string
**attributes: additional attributes for the :class:`Detection`
Returns:
a :class:`Detection`
"""
if mask.ndim > 2:
mask = mask[:, :, 0]
bbox, mask = _parse_stuff_instance(mask.astype(bool))
return cls(label=label, bounding_box=bbox, mask=mask, **attributes)
class Detections(_HasLabelList, Label):
"""A list of object detections in an image.
Args:
detections (None): a list of :class:`Detection` instances
"""
_LABEL_LIST_FIELD = "detections"
detections = fof.ListField(fof.EmbeddedDocumentField(Detection))
def to_polylines(self, tolerance=2, filled=True):
"""Returns a :class:`Polylines` representation of this instance.
For detections with masks, the returned polylines will trace the
boundaries of the masks; otherwise, the polylines will trace the
bounding boxes themselves.
Args:
tolerance (2): a tolerance, in pixels, when generating approximate
polylines for the instance masks
filled (True): whether the polylines should be filled
Returns:
a :class:`Polylines`
"""
# pylint: disable=not-an-iterable
return Polylines(
polylines=[
d.to_polyline(tolerance=tolerance, filled=filled)
for d in self.detections
]
)
def to_segmentation(self, mask=None, frame_size=None, mask_targets=None):
"""Returns a :class:`Segmentation` representation of this instance.
Only detections with instance masks (i.e., their :attr:`mask`
attributes populated) will be rendered.
You must provide either ``mask`` or ``frame_size`` to use this method.
Args:
mask (None): an optional array to use as an initial mask to which
to add objects
frame_size (None): the ``(width, height)`` of the segmentation
mask to render. This parameter has no effect if a ``mask`` is
provided
mask_targets (None): a dict mapping integer pixel values (2D masks)
or RGB hex strings (3D masks) to label strings defining which
object classes to render and which pixel values to use for each
class. If omitted, all objects are rendered with pixel value
255
Returns:
a :class:`Segmentation`
"""
mask, labels_to_targets = _parse_segmentation_mask_targets(
mask, frame_size, mask_targets
)
# pylint: disable=not-an-iterable
for detection in self.detections:
if detection.mask is None:
msg = "Skipping detection(s) with no instance mask"
warnings.warn(msg)
continue
if labels_to_targets is not None:
target = labels_to_targets.get(detection.label, None)
if target is None:
continue # skip unknown target
else:
target = 255
_render_instance(mask, detection, target)
return Segmentation(mask=mask)
class Polyline(_HasAttributesDict, _HasID, Label):
"""A set of semantically related polylines or polygons.
Args:
label (None): a label for the polyline
points (None): a list of lists of ``(x, y)`` points in
``[0, 1] x [0, 1]`` describing the vertices of each shape in the
polyline
confidence (None): a confidence in ``[0, 1]`` for the polyline
index (None): an index for the polyline
closed (False): whether the shapes are closed, i.e., and edge should
be drawn from the last vertex to the first vertex of each shape
filled (False): whether the polyline represents polygons, i.e., shapes
that should be filled when rendering them
attributes ({}): a dict mapping attribute names to :class:`Attribute`
instances for the polyline
"""
label = fof.StringField()
points = fof.PolylinePointsField()
confidence = fof.FloatField()
index = fof.IntField()
closed = fof.BooleanField(default=False)
filled = fof.BooleanField(default=False)
def to_detection(self, mask_size=None, frame_size=None):
"""Returns a :class:`Detection` representation of this instance whose
bounding box tightly encloses the polyline.
If a ``mask_size`` is provided, an instance mask of the specified size
encoding the polyline's shape is included.
Alternatively, if a ``frame_size`` is provided, the required mask size
is then computed based off of the polyline points and ``frame_size``.
Args:
mask_size (None): an optional ``(width, height)`` at which to
render an instance mask for the polyline
frame_size (None): used when no ``mask_size`` is provided.
an optional ``(width, height)`` of the frame containing this
polyline that is used to compute the required ``mask_size``
Returns:
a :class:`Detection`
"""
polyline = foue.to_polyline(self, extra_attrs=False)
if mask_size is not None:
bbox, mask = etai.render_bounding_box_and_mask(polyline, mask_size)
else:
bbox = etai.render_bounding_box(polyline)
mask = None
xtl, ytl, xbr, ybr = bbox.to_coords()
bounding_box = [xtl, ytl, (xbr - xtl), (ybr - ytl)]
if mask_size is None and frame_size:
w, h = frame_size
rel_mask_w = bounding_box[2]
rel_mask_h = bounding_box[3]
abs_mask_w = int(round(rel_mask_w * w))
abs_mask_h = int(round(rel_mask_h * h))
mask_size = (abs_mask_w, abs_mask_h)
_, mask = etai.render_bounding_box_and_mask(polyline, mask_size)
attributes = dict(self.iter_attributes())
return Detection(
label=self.label,
bounding_box=bounding_box,
confidence=self.confidence,
mask=mask,
index=self.index,
tags=self.tags,
**attributes,
)
def to_segmentation(
self, mask=None, frame_size=None, target=255, thickness=1
):
"""Returns a :class:`Segmentation` representation of this instance.
You must provide either ``mask`` or ``frame_size`` to use this method.
Args:
mask (None): an optional numpy array to use as an initial mask to
which to add objects
frame_size (None): the ``(width, height)`` of the segmentation
mask to render. This parameter has no effect if a ``mask`` is
provided
target (255): the pixel value or RGB hex string to use to render
the object
thickness (1): the thickness, in pixels, at which to render
(non-filled) polylines
Returns:
a :class:`Segmentation`
"""
mask, target = _parse_segmentation_target(mask, frame_size, target)
_render_polyline(mask, self, target, thickness)
return Segmentation(mask=mask)
def to_shapely(self, frame_size=None, filled=None):
"""Returns a Shapely representation of this instance.
The type of geometry returned depends on the number of shapes
(:attr:`points`) and whether they are polygons or lines
(:attr:`filled`).
Args:
frame_size (None): the ``(width, height)`` of the image. If
provided, the returned geometry will use absolute coordinates
filled (None): whether to treat the shape as filled (True) or
hollow (False) regardless of its :attr:`filled` attribute
Returns:
one of the following:
- ``shapely.geometry.polygon.Polygon``: if :attr:`filled` is True
and :attr:`points` contains a single shape
- ``shapely.geometry.multipolygon.MultiPolygon``: if
:attr:`filled` is True and :attr:`points` contains multiple
shapes
- ``shapely.geometry.linestring.LineString``: if :attr:`filled`
is False and :attr:`points` contains a single shape
- ``shapely.geometry.multilinestring.MultiLineString``: if
:attr:`filled` is False and :attr:`points` contains multiple
shapes
"""
if filled is not None:
_filled = filled
else:
_filled = self.filled
if self.closed:
points = []
for shape in self.points: # pylint: disable=not-an-iterable
if shape:
shape = list(shape) + [shape[0]]
points.append(shape)
else:
points = self.points
if frame_size is not None:
w, h = frame_size
points = [[(x * w, y * h) for x, y in shape] for shape in points]
if len(points) == 1:
if _filled:
return sg.Polygon(points[0])
return sg.LineString(points[0])
if _filled:
return sg.MultiPolygon(list(zip(points, itertools.repeat(None))))
return sg.MultiLineString(points)
@classmethod
def from_mask(cls, mask, label=None, tolerance=2, **attributes):
"""Creates a :class:`Polyline` instance with polygons describing the
non-zero region(s) of the given full image mask.
Args:
mask: a boolean or 0/1 numpy array
label (None): the label string
tolerance (2): a tolerance, in pixels, when generating approximate
polygons for each region. Typical values are 1-3 pixels
**attributes: additional attributes for the :class:`Polyline`
Returns:
a :class:`Polyline`
"""
if mask.ndim > 2:
mask = mask[:, :, 0]
points = _get_polygons(mask.astype(bool), tolerance)
return cls(
label=label, points=points, filled=True, closed=True, **attributes
)
@classmethod
def from_cuboid(cls, vertices, frame_size=None, label=None, **attributes):
"""Constructs a cuboid from its 8 vertices in the format below::
7--------6
/| /|
/ | / |
3--------2 |
| 4-----|--5
| / | /
|/ |/
0--------1
If a ``frame_size`` is provided, ``vertices`` must be absolute pixel
coordinates; otherwise ``vertices`` should be normalized coordinates in
``[0, 1] x [0, 1]``.
Args:
vertices: a list of 8 ``(x, y)`` vertices in the above format
frame_size (None): the ``(width, height)`` of the frame
label (None): the label string
**attributes: additional arguments for the :class:`Polyline`
Returns:
a :class:`Polyline`
"""
vertices = np.asarray(vertices)
if frame_size is not None:
vertices /= np.asarray(frame_size)[np.newaxis, :]
front = vertices[:4]
back = vertices[4:]
top = vertices[[3, 2, 6, 7], :]
bottom = vertices[[0, 1, 5, 4], :]
faces = [front.tolist(), back.tolist(), top.tolist(), bottom.tolist()]
return cls(label=label, points=faces, closed=True, **attributes)
@classmethod
def from_rotated_box(
cls, xc, yc, w, h, theta, frame_size=None, label=None, **attributes
):
"""Constructs a rotated bounding box from its center, dimensions, and
rotation.
If a ``frame_size`` is provided, the provided box coordinates must be
absolute pixel coordinates; otherwise they should be normalized
coordinates in ``[0, 1]``. Note that rotations in normalized
coordinates only make sense when the source aspect ratio is square.
Args:
xc: the x-center coordinate
yc: the y-center coorindate
w: the box width
y: the box height
theta: the counter-clockwise rotation of the box in radians
frame_size (None): the ``(width, height)`` of the frame
label (None): the label string
**attributes: additional arguments for the :class:`Polyline`
Returns:
a :class:`Polyline`
"""
R = _rotation_matrix(theta)
x = 0.5 * w * np.array([1, -1, -1, 1])
y = 0.5 * h * np.array([1, 1, -1, -1])
points = R.dot(np.stack((x, y))).T + np.array((xc, yc))
if frame_size is not None:
points /= np.asarray(frame_size)[np.newaxis, :]
points = points.tolist()
return cls(label=label, points=[points], closed=True, **attributes)
def _rotation_matrix(theta):
return np.array(
[[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]
)
class Polylines(_HasLabelList, Label):
"""A list of polylines or polygons in an image.
Args:
polylines (None): a list of :class:`Polyline` instances
"""
_LABEL_LIST_FIELD = "polylines"
polylines = fof.ListField(fof.EmbeddedDocumentField(Polyline))
def to_detections(self, mask_size=None, frame_size=None):
"""Returns a :class:`Detections` representation of this instance whose
bounding boxes tightly enclose the polylines.
If a ``mask_size`` is provided, instance masks of the specified size
encoding the polyline's shape are included in each :class:`Detection`.
Alternatively, if a ``frame_size`` is provided, the required mask size
is then computed based off of the polyline points and ``frame_size``.
Args:
mask_size (None): an optional ``(width, height)`` at which to
render instance masks for the polylines
frame_size (None): used when no ``mask_size`` is provided.
an optional ``(width, height)`` of the frame containing these
polylines that is used to compute the required ``mask_size``
Returns:
a :class:`Detections`
"""
# pylint: disable=not-an-iterable
return Detections(
detections=[
p.to_detection(mask_size=mask_size, frame_size=frame_size)
for p in self.polylines
]
)
def to_segmentation(
self, mask=None, frame_size=None, mask_targets=None, thickness=1
):
"""Returns a :class:`Segmentation` representation of this instance.
You must provide either ``mask`` or ``frame_size`` to use this method.
Args:
mask (None): an optional numpy array to use as an initial mask to
which to add objects
frame_size (None): the ``(width, height)`` of the segmentation
mask to render. This parameter has no effect if a ``mask`` is
provided
mask_targets (None): a dict mapping integer pixel values (2D masks)
or RGB hex strings (3D masks) to label strings defining which
object classes to render and which pixel values to use for each
class. If omitted, all objects are rendered with pixel value
255
thickness (1): the thickness, in pixels, at which to render
(non-filled) polylines
Returns:
a :class:`Segmentation`
"""
mask, labels_to_targets = _parse_segmentation_mask_targets(
mask, frame_size, mask_targets
)
# pylint: disable=not-an-iterable
for polyline in self.polylines:
if labels_to_targets is not None:
target = labels_to_targets.get(polyline.label, None)
if target is None:
continue # skip unknown target
else:
target = 255
_render_polyline(mask, polyline, target, thickness)
return Segmentation(mask=mask)
class Keypoint(_HasAttributesDict, _HasID, Label):
"""A list of keypoints in an image.
Args:
label (None): a label for the points
points (None): a list of ``(x, y)`` keypoints in ``[0, 1] x [0, 1]``
confidence (None): a list of confidences in ``[0, 1]`` for each point
index (None): an index for the keypoints
attributes ({}): a dict mapping attribute names to :class:`Attribute`
instances
"""
label = fof.StringField()
points = fof.KeypointsField()
confidence = fof.ListField(fof.FloatField(), null=True)
index = fof.IntField()
def to_shapely(self, frame_size=None):
"""Returns a Shapely representation of this instance.
Args:
frame_size (None): the ``(width, height)`` of the image. If
provided, the returned geometry will use absolute coordinates
Returns:
a ``shapely.geometry.multipoint.MultiPoint``
"""
# pylint: disable=not-an-iterable
points = self.points
if frame_size is not None:
w, h = frame_size
points = [(x * w, y * h) for x, y in points]
return sg.MultiPoint(points)
class Keypoints(_HasLabelList, Label):
"""A list of :class:`Keypoint` instances in an image.
Args:
keypoints (None): a list of :class:`Keypoint` instances
"""
_LABEL_LIST_FIELD = "keypoints"
keypoints = fof.ListField(fof.EmbeddedDocumentField(Keypoint))
class _HasMedia(object):
"""Mixin for :class:`Label` classes that contain a media field."""
_MEDIA_FIELD = None
class Segmentation(_HasID, _HasMedia, Label):
"""A semantic segmentation for an image.