diff --git a/docs/source/user_guide/dataset_creation/datasets.rst b/docs/source/user_guide/dataset_creation/datasets.rst index a5cd6231f0..b734c353c8 100644 --- a/docs/source/user_guide/dataset_creation/datasets.rst +++ b/docs/source/user_guide/dataset_creation/datasets.rst @@ -5164,14 +5164,20 @@ should implement is determined by the type of dataset that you are importing. importer = CustomLabeledImageDatasetImporter(...) label_field = ... + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: + label_key = lambda k: label_field + "_" + k + else: + label_field = "ground_truth" + label_key = lambda k: k + with importer: for image_path, image_metadata, label in importer: sample = fo.Sample(filepath=image_path, metadata=image_metadata) if isinstance(label, dict): - sample.update_fields( - {label_field + "_" + k: v for k, v in label.items()} - ) + sample.update_fields({label_key(k): v for k, v in label.items()}) elif label is not None: sample[label_field] = label @@ -5566,14 +5572,20 @@ should implement is determined by the type of dataset that you are importing. importer = CustomLabeledVideoDatasetImporter(...) label_field = ... + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: + label_key = lambda k: label_field + "_" + k + else: + label_field = "ground_truth" + label_key = lambda k: k + with importer: for video_path, video_metadata, label, frames in importer: sample = fo.Sample(filepath=video_path, metadata=video_metadata) if isinstance(label, dict): - sample.update_fields( - {label_field + "_" + k: v for k, v in label.items()} - ) + sample.update_fields({label_key(k): v for k, v in label.items()}) elif label is not None: sample[label_field] = label @@ -5583,8 +5595,7 @@ should implement is determined by the type of dataset that you are importing. for frame_number, _label in frames.items(): if isinstance(_label, dict): frame_labels[frame_number] = { - label_field + "_" + field_name: label - for field_name, label in _label.items() + label_key(k): v for k, v in _label.items() } elif _label is not None: frame_labels[frame_number] = {label_field: _label} diff --git a/docs/source/user_guide/dataset_creation/samples.rst b/docs/source/user_guide/dataset_creation/samples.rst index 4346fb00e1..cc9203a65a 100644 --- a/docs/source/user_guide/dataset_creation/samples.rst +++ b/docs/source/user_guide/dataset_creation/samples.rst @@ -932,6 +932,14 @@ classification or object detections) associated with the image. sample_parser = CustomLabeledImageSampleParser(...) label_field = ... + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: + label_key = lambda k: label_field + "_" + k + else: + label_field = "ground_truth" + label_key = lambda k: k + for sample in samples: sample_parser.with_sample(sample) @@ -947,9 +955,7 @@ classification or object detections) associated with the image. sample = fo.Sample(filepath=image_path, metadata=metadata) if isinstance(label, dict): - sample.update_fields( - {label_field + "_" + k: v for k, v in label.items()} - ) + sample.update_fields({label_key(k): v for k, v in label.items()}) elif label is not None: sample[label_field] = label @@ -1191,6 +1197,14 @@ classification or object detections) associated with the image. sample_parser = CustomLabeledVideoSampleParser(...) label_field = ... + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: + label_key = lambda k: label_field + "_" + k + else: + label_field = "ground_truth" + label_key = lambda k: k + for sample in samples: sample_parser.with_sample(sample) @@ -1207,9 +1221,7 @@ classification or object detections) associated with the image. sample = fo.Sample(filepath=video_path, metadata=metadata) if isinstance(label, dict): - sample.update_fields( - {label_field + "_" + k: v for k, v in label.items()} - ) + sample.update_fields({label_key(k): v for k, v in label.items()}) elif label is not None: sample[label_field] = label @@ -1219,8 +1231,7 @@ classification or object detections) associated with the image. for frame_number, _label in frames.items(): if isinstance(_label, dict): frame_labels[frame_number] = { - label_field + "_" + field_name: label - for field_name, label in _label.items() + label_key(k): v for k, v in _label.items() } elif _label is not None: frame_labels[frame_number] = {label_field: _label} diff --git a/docs/source/user_guide/model_zoo/index.rst b/docs/source/user_guide/model_zoo/index.rst index a80d8b81c3..2481bb8bc5 100644 --- a/docs/source/user_guide/model_zoo/index.rst +++ b/docs/source/user_guide/model_zoo/index.rst @@ -225,7 +225,7 @@ All models in the FiftyOne Model Zoo are instances of the |Model| class, which defines a common interface for loading models and generating predictions with defined input and output data formats. -.. note: +.. note:: The following sections describe the interface that all models in the Model Zoo implement. If you write a wrapper for your custom model that implements @@ -357,7 +357,7 @@ and the output ``labels`` can be any of the following: # Multiple sample-level labels for key, value in labels.items(): - sample[label_field + "_" + key] = value + sample[label_key(key)] = value - A dict mapping frame numbers to |Label| instances. In this case, the provided labels are interpreted as frame-level labels that should be added @@ -384,19 +384,29 @@ and the output ``labels`` can be any of the following: # Multiple per-frame labels sample.frames.merge( { - frame_number: { - label_field + "_" + name: label - for name, label in frame_dict.items() - } + frame_number: {label_key(k): v for k, v in frame_dict.items()} for frame_number, frame_dict in labels.items() } ) +In the above snippets, the ``label_key`` function maps label dict keys to field +names, and is defined from ``label_field`` as follows:: + +.. code-block:: python + :linenos: + + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: + label_key = lambda k: label_field + "_" + k + else: + label_key = lambda k: k + For models that support batching, the |Model| interface also provides a :meth:`predict_all() ` method that can provide an efficient implementation of predicting on a batch of data. -.. note: +.. note:: Builtin methods like :meth:`apply_model() ` @@ -404,7 +414,7 @@ provide an efficient implementation of predicting on a batch of data. size used when performing inference with models that support efficient batching. -.. note: +.. note:: PyTorch models can implement the |TorchModelMixin| mixin, in which case `DataLoaders `_ @@ -440,7 +450,7 @@ By convention, :meth:`Model.embed() ` should return a numpy array containing the embedding. -.. note: +.. note:: Sample embeddings are typically 1D vectors, but this is not strictly required. diff --git a/fiftyone/core/dataset.py b/fiftyone/core/dataset.py index fa4c343291..4cc125740e 100644 --- a/fiftyone/core/dataset.py +++ b/fiftyone/core/dataset.py @@ -2531,10 +2531,11 @@ def add_dir( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample expand_schema (True): whether to dynamically add new sample fields @@ -2688,10 +2689,11 @@ def merge_dir( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample key_field ("filepath"): the sample field to use to decide whether @@ -2841,10 +2843,11 @@ def add_archive( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample expand_schema (True): whether to dynamically add new sample fields @@ -2991,10 +2994,11 @@ def merge_archive( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample key_field ("filepath"): the sample field to use to decide whether @@ -3086,10 +3090,11 @@ def add_importer( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample expand_schema (True): whether to dynamically add new sample fields @@ -3174,10 +3179,11 @@ def merge_importer( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample key_field ("filepath"): the sample field to use to decide whether @@ -3293,10 +3299,10 @@ def add_labeled_images( :class:`fiftyone.core.labels.Label` instance per sample, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the parser produces a dictionary of - labels per sample, this argument specifies a string prefix to - prepend to each label key; the default in this case is to - directly use the keys of the imported label dictionaries as - field names + labels per sample, this argument can be either a string prefix + to prepend to each label key or a dict mapping label keys to + field names; the default in this case is to directly use the + keys of the imported label dictionaries as field names tags (None): an optional tag or iterable of tags to attach to each sample expand_schema (True): whether to dynamically add new sample fields @@ -3433,10 +3439,10 @@ def ingest_labeled_images( :class:`fiftyone.core.labels.Label` instance per sample, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the parser produces a dictionary of - labels per sample, this argument specifies a string prefix to - prepend to each label key; the default in this case is to - directly use the keys of the imported label dictionaries as - field names + labels per sample, this argument can be either a string prefix + to prepend to each label key or a dict mapping label keys to + field names; the default in this case is to directly use the + keys of the imported label dictionaries as field names tags (None): an optional tag or iterable of tags to attach to each sample expand_schema (True): whether to dynamically add new sample fields @@ -3524,10 +3530,11 @@ def add_labeled_videos( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the parser produces a - dictionary of labels per sample/frame, this argument specifies - a string prefix to prepend to each label key; the default in - this case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample/frame, this argument can be + either a string prefix to prepend to each label key or a dict + mapping label keys to field names; the default in this case is + to directly use the keys of the imported label dictionaries as + field names label_field ("ground_truth"): the name (or root name) of the frame field(s) to use for the labels tags (None): an optional tag or iterable of tags to attach to each @@ -3757,10 +3764,11 @@ def from_dir( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample **kwargs: optional keyword arguments to pass to the constructor of @@ -3865,10 +3873,11 @@ def from_archive( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample cleanup (True): whether to delete the archive after extracting it @@ -3917,10 +3926,11 @@ def from_importer( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a - string prefix to prepend to each label key; the default in this - case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping + label keys to field names; the default in this case is to + directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample @@ -3999,10 +4009,10 @@ def from_labeled_images( :class:`fiftyone.core.labels.Label` instance per sample, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the parser produces a dictionary of - labels per sample, this argument specifies a string prefix to - prepend to each label key; the default in this case is to - directly use the keys of the imported label dictionaries as - field names + labels per sample, this argument can be either a string prefix + to prepend to each label key or a dict mapping label keys to + field names; the default in this case is to directly use the + keys of the imported label dictionaries as field names tags (None): an optional tag or iterable of tags to attach to each sample @@ -4121,10 +4131,11 @@ def from_labeled_videos( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the parser produces a - dictionary of labels per sample/frame, this argument specifies - a string prefix to prepend to each label key; the default in - this case is to directly use the keys of the imported label - dictionaries as field names + dictionary of labels per sample/frame, this argument can be + either a string prefix to prepend to each label key or a dict + mapping label keys to field names; the default in this case is + to directly use the keys of the imported label dictionaries as + field names tags (None): an optional tag or iterable of tags to attach to each sample diff --git a/fiftyone/core/sample.py b/fiftyone/core/sample.py index ba77ba530c..50365e914b 100644 --- a/fiftyone/core/sample.py +++ b/fiftyone/core/sample.py @@ -115,7 +115,11 @@ def compute_metadata(self, overwrite=False, skip_failures=False): ) def add_labels( - self, labels, label_field, confidence_thresh=None, expand_schema=True + self, + labels, + label_field=None, + confidence_thresh=None, + expand_schema=True, ): """Adds the given labels to the sample. @@ -128,7 +132,7 @@ def add_labels( instances. In this case, the labels are added as follows:: for key, value in labels.items(): - sample[label_field + "_" + key] = value + sample[label_key(key)] = value - A dict mapping frame numbers to :class:`fiftyone.core.labels.Label` instances. In this case, the provided labels are interpreted as @@ -149,24 +153,37 @@ def add_labels( sample.frames.merge( { frame_number: { - label_field + "_" + name: label - for name, label in frame_dict.items() + label_key(key): value + for key, value in frame_dict.items() } for frame_number, frame_dict in labels.items() } ) + In the above, the ``label_key`` function maps label dict keys to field + names, and is defined from ``label_field`` as follows:: + + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: + label_key = lambda k: label_field + "_" + k + else: + label_key = lambda k: k + Args: labels: a :class:`fiftyone.core.labels.Label` or dict of labels per the description above - label_field: the sample field or prefix in which to save the labels + label_field (None): the sample field, prefix, or dict defining in + which field(s) to save the labels confidence_thresh (None): an optional confidence threshold to apply to any applicable labels before saving them expand_schema (True): whether to dynamically add new fields encountered to the dataset schema. If False, an error is raised if any fields are not in the dataset schema """ - if label_field: + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: label_key = lambda k: label_field + "_" + k else: label_key = lambda k: k @@ -192,6 +209,11 @@ def add_labels( }, expand_schema=expand_schema, ) + elif label_field is None: + raise ValueError( + "A `label_field` must be provided in order to add labels " + "to a single frame-level field" + ) else: # Single frame-level field self.frames.merge( @@ -209,10 +231,17 @@ def add_labels( expand_schema=expand_schema, ) elif labels is not None: + if label_field is None: + raise ValueError( + "A `label_field` must be provided in order to add labels " + "to a single sample field" + ) + # Single sample-level field self.set_field(label_field, labels, create=expand_schema) - self.save() + if self._in_db: + self.save() def merge( self, diff --git a/fiftyone/utils/data/importers.py b/fiftyone/utils/data/importers.py index d9f0c4edd7..39f82f9580 100644 --- a/fiftyone/utils/data/importers.py +++ b/fiftyone/utils/data/importers.py @@ -71,10 +71,10 @@ def import_samples( single :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a string - prefix to prepend to each label key; the default in this case is to - directly use the keys of the imported label dictionaries as field - names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping label + keys to field names; the default in this case is to directly use + the keys of the imported label dictionaries as field names tags (None): an optional tag or iterable of tags to attach to each sample expand_schema (True): whether to dynamically add new sample fields @@ -203,10 +203,10 @@ def merge_samples( single :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the importer produces a - dictionary of labels per sample, this argument specifies a string - prefix to prepend to each label key; the default in this case is to - directly use the keys of the imported label dictionaries as field - names + dictionary of labels per sample, this argument can be either a + string prefix to prepend to each label key or a dict mapping label + keys to field names; the default in this case is to directly use + the keys of the imported label dictionaries as field names tags (None): an optional tag or iterable of tags to attach to each sample key_field ("filepath"): the sample field to use to decide whether to @@ -391,7 +391,9 @@ def parse_sample(sample): elif isinstance(dataset_importer, LabeledImageDatasetImporter): # Labeled image dataset - if label_field: + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: label_key = lambda k: label_field + "_" + k else: label_field = "ground_truth" @@ -430,7 +432,9 @@ def parse_sample(sample): elif isinstance(dataset_importer, LabeledVideoDatasetImporter): # Labeled video dataset - if label_field: + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: label_key = lambda k: label_field + "_" + k else: label_field = "ground_truth" diff --git a/fiftyone/utils/data/parsers.py b/fiftyone/utils/data/parsers.py index 34d01f4370..4078d3ffc1 100644 --- a/fiftyone/utils/data/parsers.py +++ b/fiftyone/utils/data/parsers.py @@ -116,9 +116,10 @@ def add_labeled_images( :class:`fiftyone.core.labels.Label` instance per sample, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the parser produces a dictionary of labels - per sample, this argument specifies a string prefix to prepend to - each label key; the default in this case is to directly use the - keys of the imported label dictionaries as field names + per sample, this argument can be either a string prefix to prepend + to each label key or a dict mapping label keys to field names; the + default in this case is to directly use the keys of the imported + label dictionaries as field names tags (None): an optional tag or iterable of tags to attach to each sample expand_schema (True): whether to dynamically add new sample fields @@ -143,7 +144,9 @@ def add_labeled_images( ) ) - if label_field: + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: label_key = lambda k: label_field + "_" + k else: label_field = "ground_truth" @@ -283,9 +286,10 @@ def add_labeled_videos( :class:`fiftyone.core.labels.Label` instance per sample/frame, this argument specifies the name of the field to use; the default is ``"ground_truth"``. If the parser produces a dictionary of labels - per sample/frame, this argument specifies a string prefix to - prepend to each label key; the default in this case is to directly - use the keys of the imported label dictionaries as field names + per sample/frame, this argument can be either a string prefix to + prepend to each label key or a dict mapping label keys to field + names; the default in this case is to directly use the keys of the + imported label dictionaries as field names tags (None): an optional tag or iterable of tags to attach to each sample expand_schema (True): whether to dynamically add new sample fields @@ -304,7 +308,9 @@ def add_labeled_videos( ) ) - if label_field: + if isinstance(label_field, dict): + label_key = lambda k: label_field.get(k, k) + elif label_field is not None: label_key = lambda k: label_field + "_" + k else: label_field = "ground_truth"