diff --git a/fiftyone/core/labels.py b/fiftyone/core/labels.py index 81a2994820..db87423006 100644 --- a/fiftyone/core/labels.py +++ b/fiftyone/core/labels.py @@ -401,18 +401,75 @@ class Detection(_HasAttributesDict, _HasID, Label): 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 + mask_path (None): the absolute path to the instance segmentation image + on disk 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 """ + _MEDIA_FIELD = "mask_path" + label = fof.StringField() bounding_box = fof.ListField(fof.FloatField()) mask = fof.ArrayField() + mask_path = fof.StringField() confidence = fof.FloatField() index = fof.IntField() + @property + def has_mask(self): + """Whether this instance has a mask.""" + return self.mask is not None or self.mask_path is not None + + def get_mask(self): + """Returns the segmentation mask for this instance. + + Returns: + a numpy array, or ``None`` + """ + if self.mask is not None: + return self.mask + + if self.mask_path is not None: + return _read_mask(self.mask_path) + + return None + + def import_mask(self, update=False): + """Imports this instance's mask from disk to its :attr:`mask` + attribute. + + Args: + outpath: the path to write the map + update (False): whether to clear this instance's :attr:`mask_path` + attribute after importing + """ + if self.mask_path is not None: + self.mask = _read_mask(self.mask_path) + + if update: + self.mask_path = None + + def export_mask(self, outpath, update=False): + """Exports this instance's mask to the given path. + + Args: + outpath: the path to write the mask + update (False): whether to clear this instance's :attr:`mask` + attribute and set its :attr:`mask_path` attribute when + exporting in-database segmentations + """ + if self.mask_path is not None: + etau.copy_file(self.mask_path, outpath) + else: + _write_mask(self.mask, outpath) + + if update: + self.mask = None + self.mask_path = outpath + def to_polyline(self, tolerance=2, filled=True): """Returns a :class:`Polyline` representation of this instance. @@ -467,7 +524,8 @@ def to_segmentation(self, mask=None, frame_size=None, target=255): Returns: a :class:`Segmentation` """ - if self.mask is None: + mask = self.get_mask() + if mask is None: raise ValueError( "Only detections with their `mask` attributes populated can " "be converted to segmentations" diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py index 76a4fd494b..4d079db105 100644 --- a/fiftyone/utils/coco.py +++ b/fiftyone/utils/coco.py @@ -1299,7 +1299,7 @@ def from_label( x, y, w, h = label.bounding_box bbox = [x * width, y * height, w * width, h * height] - if label.mask is not None: + if label.has_mask() is not None: segmentation = _instance_to_coco_segmentation( label, frame_size, iscrowd=iscrowd, tolerance=tolerance ) @@ -2110,7 +2110,7 @@ def _coco_objects_to_detections( ) if detection is not None and ( - not load_segmentations or detection.mask is not None + not load_segmentations or detection.has_mask() is not None ): detections.append(detection) diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py index c30b6811dd..f546909543 100644 --- a/fiftyone/utils/cvat.py +++ b/fiftyone/utils/cvat.py @@ -6400,7 +6400,7 @@ def _create_detection_shapes( } ) elif label_type in ("instance", "instances"): - if det.mask is None: + if det.has_mask() is None: continue polygon = det.to_polyline() diff --git a/fiftyone/utils/eta.py b/fiftyone/utils/eta.py index a71ba42277..237001f503 100644 --- a/fiftyone/utils/eta.py +++ b/fiftyone/utils/eta.py @@ -596,7 +596,7 @@ def to_detected_object(detection, name=None, extra_attrs=True): bry = tly + h bounding_box = etag.BoundingBox.from_coords(tlx, tly, brx, bry) - mask = detection.mask + mask = detection.get_mask() confidence = detection.confidence attrs = _to_eta_attributes(detection, extra_attrs=extra_attrs)