From 7ca8e066c895643574bd49703ea044ab0ea224bc Mon Sep 17 00:00:00 2001 From: brimoor Date: Thu, 9 Jun 2022 15:57:02 -0400 Subject: [PATCH 1/2] stop silently passing class lists to API methods --- docs/source/integrations/coco.rst | 3 +- .../user_guide/dataset_creation/datasets.rst | 18 +++---- .../user_guide/dataset_creation/index.rst | 12 ++--- docs/source/user_guide/export_datasets.rst | 37 ++++--------- fiftyone/core/collections.py | 17 ++---- fiftyone/utils/coco.py | 51 ++++++------------ fiftyone/utils/data/exporters.py | 51 ++---------------- fiftyone/utils/eval/classification.py | 15 +----- fiftyone/utils/eval/detection.py | 14 +---- fiftyone/utils/eval/segmentation.py | 13 +---- fiftyone/utils/labels.py | 12 ----- fiftyone/utils/tf.py | 21 ++------ fiftyone/utils/yolo.py | 52 ++----------------- tests/unittests/import_export_tests.py | 12 ++--- 14 files changed, 66 insertions(+), 262 deletions(-) diff --git a/docs/source/integrations/coco.rst b/docs/source/integrations/coco.rst index 31b079642e..27ea938245 100644 --- a/docs/source/integrations/coco.rst +++ b/docs/source/integrations/coco.rst @@ -325,7 +325,8 @@ to add them to your dataset as follows: ] # Add COCO predictions to `predictions` field of dataset - fouc.add_coco_labels(coco_dataset, "predictions", predictions) + classes = coco_dataset.default_classes + fouc.add_coco_labels(coco_dataset, "predictions", predictions, classes) # Verify that predictions were added to two images print(coco_dataset.count("predictions")) # 2 diff --git a/docs/source/user_guide/dataset_creation/datasets.rst b/docs/source/user_guide/dataset_creation/datasets.rst index 463d9df8c5..a5cd6231f0 100644 --- a/docs/source/user_guide/dataset_creation/datasets.rst +++ b/docs/source/user_guide/dataset_creation/datasets.rst @@ -1553,16 +1553,16 @@ COCO format: dataset.export( export_dir="/tmp/coco", dataset_type=fo.types.COCODetectionDataset, - classes=classes, label_field="ground_truth", + classes=classes, ) # Export predictions dataset.export( dataset_type=fo.types.COCODetectionDataset, labels_path="/tmp/coco/predictions.json", - classes=classes, label_field="predictions", + classes=classes, ) # Now load ground truth labels into a new dataset @@ -1577,7 +1577,7 @@ COCO format: dataset2, "predictions", "/tmp/coco/predictions.json", - classes=classes, + classes, ) # Verify that ground truth and predictions were imported as expected @@ -2147,16 +2147,16 @@ images-and-labels and labels-only data in YOLO format: dataset.export( export_dir="/tmp/yolov4", dataset_type=fo.types.YOLOv4Dataset, - classes=classes, label_field="ground_truth", + classes=classes, ) # Export predictions dataset.export( dataset_type=fo.types.YOLOv4Dataset, labels_path="/tmp/yolov4/predictions", - classes=classes, label_field="predictions", + classes=classes, ) # Now load ground truth labels into a new dataset @@ -2171,7 +2171,7 @@ images-and-labels and labels-only data in YOLO format: dataset2, "predictions", "/tmp/yolov4/predictions", - classes=classes, + classes, ) # Verify that ground truth and predictions were imported as expected @@ -2327,16 +2327,16 @@ images-and-labels and labels-only data in YOLO format: export_dir="/tmp/yolov5", dataset_type=fo.types.YOLOv5Dataset, split="validation", - classes=classes, label_field="ground_truth", + classes=classes, ) # Export predictions view.export( dataset_type=fo.types.YOLOv5Dataset, labels_path="/tmp/yolov5/predictions/validation", - classes=classes, label_field="predictions", + classes=classes, ) # Now load ground truth labels into a new dataset @@ -2352,7 +2352,7 @@ images-and-labels and labels-only data in YOLO format: dataset2, "predictions", "/tmp/yolov5/predictions/validation", - classes=classes, + classes, ) # Verify that ground truth and predictions were imported as expected diff --git a/docs/source/user_guide/dataset_creation/index.rst b/docs/source/user_guide/dataset_creation/index.rst index d5fba3045d..626c6d2479 100644 --- a/docs/source/user_guide/dataset_creation/index.rst +++ b/docs/source/user_guide/dataset_creation/index.rst @@ -369,16 +369,16 @@ predictions to take advantage of FiftyOne's dataset.export( export_dir="/tmp/coco", dataset_type=fo.types.COCODetectionDataset, - classes=classes, label_field="ground_truth", + classes=classes, ) # Export predictions dataset.export( dataset_type=fo.types.COCODetectionDataset, labels_path="/tmp/coco/predictions.json", - classes=classes, label_field="predictions", + classes=classes, ) # Now load ground truth labels into a new dataset @@ -393,7 +393,7 @@ predictions to take advantage of FiftyOne's dataset2, "predictions", "/tmp/coco/predictions.json", - classes=classes, + classes, ) # Verify that ground truth and predictions were imported as expected @@ -432,16 +432,16 @@ predictions to take advantage of FiftyOne's dataset.export( export_dir="/tmp/yolov4", dataset_type=fo.types.YOLOv4Dataset, - classes=classes, label_field="ground_truth", + classes=classes, ) # Export predictions dataset.export( dataset_type=fo.types.YOLOv4Dataset, labels_path="/tmp/yolov4/predictions", - classes=classes, label_field="predictions", + classes=classes, ) # Now load ground truth labels into a new dataset @@ -456,7 +456,7 @@ predictions to take advantage of FiftyOne's dataset2, "predictions", "/tmp/yolov4/predictions", - classes=classes, + classes, ) # Verify that ground truth and predictions were imported as expected diff --git a/docs/source/user_guide/export_datasets.rst b/docs/source/user_guide/export_datasets.rst index c3182407ce..78b9e7442e 100644 --- a/docs/source/user_guide/export_datasets.rst +++ b/docs/source/user_guide/export_datasets.rst @@ -349,26 +349,16 @@ Class lists ----------- Certain labeled image/video export formats such as -:ref:`COCO ` or +:ref:`COCO ` and :ref:`YOLO ` store an explicit list of classes for the label field being exported. -In such cases, all exporters provided by FiftyOne use the following strategy -to retrieve the class list when exporting a given `label_field`: +By convention, all exporters provided by FiftyOne should provide a `classes` +parameter that allows for manually specifying the classes list to use. -1. If the exporter provides a parameter like `classes` that allows for - manually specifying the classes, this list is used -2. If the :meth:`classes ` dict of the - sample collection contains `label_field`, this class list is used -3. If the collection's - :meth:`default_classes ` - attribute is non-empty, this class list is used -4. If the collection's :meth:`info ` dict - contains a class list under the `classes` key, this list is used - -If no explicit class list is available via the above methods, the observed -classes in the collection being exported are used, which may be a subset of the -classes in the parent dataset when exporting a view. +If no explicit class list is provided, the observed classes in the collection +being exported are used, which may be a subset of the classes in the parent +dataset when exporting a view. .. note:: @@ -397,26 +387,17 @@ classes in the parent dataset when exporting a view. # Create a view that only contains cats and dogs view = dataset.filter_labels("ground_truth", F("label").is_in(["cat", "dog"])) - # By default, `default_classes` will be used to populate the COCO categories + # By default, only the observed classes will be stored as COCO categories view.export( labels_path="/tmp/coco1.json", dataset_type=fo.types.COCODetectionDataset, ) - # But, since we're only interested in exporting cats and dogs, we can override - # this by manually providing the `classes` argument + # However, if desired, we can explicitly provide a classes list view.export( labels_path="/tmp/coco2.json", dataset_type=fo.types.COCODetectionDataset, - classes=["cat", "dog"], - ) - - # Equivalently, we can clear `default_classes` first so that the observed - # labels (only cats and dogs in this case) will be used - dataset.default_classes = None - view.export( - labels_path="/tmp/coco3.json", - dataset_type=fo.types.COCODetectionDataset, + classes=dataset.default_classes, ) .. _supported-export-formats: diff --git a/fiftyone/core/collections.py b/fiftyone/core/collections.py index 0552db550a..e799ef953d 100644 --- a/fiftyone/core/collections.py +++ b/fiftyone/core/collections.py @@ -2010,11 +2010,7 @@ def evaluate_classifications( instances eval_key (None): a string key to use to refer to this evaluation classes (None): the list of possible classes. If not provided, - classes are loaded from - :meth:`fiftyone.core.dataset.Dataset.classes` or - :meth:`fiftyone.core.dataset.Dataset.default_classes` if - possible, or else the observed ground truth/predicted labels - are used + the observed ground truth/predicted labels are used missing (None): a missing label string. Any None-valued labels are given this label for results purposes method ("simple"): a string specifying the evaluation method to use. @@ -2116,11 +2112,7 @@ def evaluate_detections( or :class:`fiftyone.core.labels.TemporalDetections` eval_key (None): a string key to use to refer to this evaluation classes (None): the list of possible classes. If not provided, - classes are loaded from - :meth:`fiftyone.core.dataset.Dataset.classes` or - :meth:`fiftyone.core.dataset.Dataset.default_classes` if - possible, or else the observed ground truth/predicted labels - are used + the observed ground truth/predicted labels are used missing (None): a missing label string. Any unmatched objects are given this label for results purposes method (None): a string specifying the evaluation method to use. @@ -2211,10 +2203,7 @@ def evaluate_segmentations( instances eval_key (None): a string key to use to refer to this evaluation mask_targets (None): a dict mapping mask values to labels. If not - provided, mask targets are loaded from - :meth:`fiftyone.core.dataset.Dataset.mask_targets` or - :meth:`fiftyone.core.dataset.Dataset.default_mask_targets` if - possible, or else the observed pixel values are used + provided, the observed pixel values are used method ("simple"): a string specifying the evaluation method to use. Supported values are ``("simple")`` **kwargs: optional keyword arguments for the constructor of the diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py index a1db1e1706..0cc744b754 100644 --- a/fiftyone/utils/coco.py +++ b/fiftyone/utils/coco.py @@ -45,9 +45,9 @@ def add_coco_labels( sample_collection, label_field, labels_or_path, + classes, label_type="detections", coco_id_field=None, - classes=None, extra_attrs=True, use_polylines=False, tolerance=None, @@ -128,6 +128,7 @@ def add_coco_labels( will be created if necessary labels_or_path: a list of COCO annotations or the path to a JSON file containing such data on disk + classes: the list of class label strings label_type ("detections"): the type of labels to load. Supported values are ``("detections", "segmentations", "keypoints")`` coco_id_field (None): this parameter determines how to map the @@ -140,9 +141,6 @@ def add_coco_labels( - the name of a field of ``sample_collection`` containing the COCO IDs for the samples that correspond to the ``image_id`` of the predictions - classes (None): the list of class label strings. If not provided, these - must be available from - :meth:`fiftyone.core.collections.SampleCollection.get_classes` extra_attrs (True): whether to load extra annotation attributes onto the imported labels. Supported values are: @@ -155,14 +153,6 @@ def add_coco_labels( tolerance (None): a tolerance, in pixels, when generating approximate polylines for instance masks. Typical values are 1-3 pixels """ - if classes is None: - classes = sample_collection.get_classes(label_field) - - if not classes: - raise ValueError( - "You must provide `classes` in order to load COCO labels" - ) - if etau.is_str(labels_or_path): labels = etas.load_json(labels_or_path) if isinstance(labels, dict): @@ -646,9 +636,7 @@ class COCODetectionDatasetExporter( image_format (None): the image format to use when writing in-memory images to disk. By default, ``fiftyone.config.default_image_ext`` is used - classes (None): the list of possible class labels. If not provided, - this list will be extracted when :meth:`log_collection` is called, - if possible + classes (None): the list of possible class labels info (None): a dict of info as returned by :meth:`load_coco_detection_annotations` to include in the exported JSON. If not provided, this info will be extracted when @@ -709,12 +697,13 @@ def __init__( self.num_decimals = num_decimals self.tolerance = tolerance - self._labels_map_rev = None self._image_id = None self._anno_id = None self._images = None self._annotations = None self._classes = None + self._dynamic_classes = classes is None + self._labels_map_rev = None self._has_labels = None self._media_exporter = None @@ -731,7 +720,6 @@ def setup(self): self._anno_id = 0 self._images = [] self._annotations = [] - self._classes = set() self._has_labels = False self._parse_classes() @@ -744,17 +732,6 @@ def setup(self): self._media_exporter.setup() def log_collection(self, sample_collection): - if self.classes is None: - if sample_collection.default_classes: - self.classes = sample_collection.default_classes - self._parse_classes() - elif sample_collection.classes: - self.classes = next(iter(sample_collection.classes.values())) - self._parse_classes() - elif "classes" in sample_collection.info: - self.classes = sample_collection.info["classes"] - self._parse_classes() - if self.info is None: self.info = sample_collection.info @@ -794,7 +771,10 @@ def export_sample(self, image_or_path, label, metadata=None): for label in labels: _label = label.label - if self._labels_map_rev is not None: + if self._dynamic_classes: + category_id = _label # will be converted to int later + self._classes.add(_label) + else: if _label not in self._labels_map_rev: msg = ( "Ignoring object with label '%s' not in provided " @@ -804,11 +784,8 @@ def export_sample(self, image_or_path, label, metadata=None): continue category_id = self._labels_map_rev[_label] - else: - category_id = _label # will be converted to int later self._anno_id += 1 - self._classes.add(_label) obj = COCOObject.from_label( label, @@ -825,13 +802,13 @@ def export_sample(self, image_or_path, label, metadata=None): self._annotations.append(obj.to_anno_dict()) def close(self, *args): - if self.classes is not None: - classes = self.classes - else: + if self._dynamic_classes: classes = sorted(self._classes) labels_map_rev = _to_labels_map_rev(classes) for anno in self._annotations: anno["category_id"] = labels_map_rev[anno["category_id"]] + else: + classes = self.classes date_created = datetime.now().replace(microsecond=0).isoformat() info = { @@ -874,7 +851,9 @@ def close(self, *args): self._media_exporter.close() def _parse_classes(self): - if self.classes is not None: + if self._dynamic_classes: + self._classes = set() + else: self._labels_map_rev = _to_labels_map_rev(self.classes) diff --git a/fiftyone/utils/data/exporters.py b/fiftyone/utils/data/exporters.py index 4a835ba078..44c5c337ab 100644 --- a/fiftyone/utils/data/exporters.py +++ b/fiftyone/utils/data/exporters.py @@ -1175,8 +1175,7 @@ def log_collection(self, sample_collection): Subclasses can optionally implement this method if their export format can record information such as the - :meth:`fiftyone.core.collections.SampleCollection.info` or - :meth:`fiftyone.core.collections.SampleCollection.classes` of the + :meth:`fiftyone.core.collections.SampleCollection.info` of the collection being exported. By convention, this method must be optional; i.e., if it is not called @@ -1989,9 +1988,7 @@ class FiftyOneImageClassificationDatasetExporter( - ``True``: always include a (possibly empty) attributes dict - ``None``: include attributes only if they exist - a name or iterable of names of specific attributes to include - classes (None): the list of possible class labels. If not provided, - this list will be extracted when :meth:`log_collection` is called, - if possible + classes (None): the list of possible class labels image_format (None): the image format to use when writing in-memory images to disk. By default, ``fiftyone.config.default_image_ext`` is used @@ -2059,18 +2056,6 @@ def setup(self): ) self._media_exporter.setup() - def log_collection(self, sample_collection): - if self.classes is None: - if sample_collection.default_classes: - self.classes = sample_collection.default_classes - self._parse_classes() - elif sample_collection.classes: - self.classes = next(iter(sample_collection.classes.values())) - self._parse_classes() - elif "classes" in sample_collection.info: - self.classes = sample_collection.info["classes"] - self._parse_classes() - def export_sample(self, image_or_path, label, metadata=None): _, uuid = self._media_exporter.export(image_or_path) self._labels_dict[uuid] = _parse_classifications( @@ -2339,9 +2324,7 @@ class FiftyOneImageDetectionDatasetExporter( If None, the default value of this parameter will be chosen based on the value of the ``data_path`` parameter - classes (None): the list of possible class labels. If not provided, - this list will be extracted when :meth:`log_collection` is called, - if possible + classes (None): the list of possible class labels include_confidence (None): whether to include detection confidences in the export. The supported values are: @@ -2422,18 +2405,6 @@ def setup(self): ) self._media_exporter.setup() - def log_collection(self, sample_collection): - if self.classes is None: - if sample_collection.default_classes: - self.classes = sample_collection.default_classes - self._parse_classes() - elif sample_collection.classes: - self.classes = next(iter(sample_collection.classes.values())) - self._parse_classes() - elif "classes" in sample_collection.info: - self.classes = sample_collection.info["classes"] - self._parse_classes() - def export_sample(self, image_or_path, detections, metadata=None): _, uuid = self._media_exporter.export(image_or_path) self._labels_dict[uuid] = _parse_detections( @@ -2520,9 +2491,7 @@ class FiftyOneTemporalDetectionDatasetExporter( on the value of the ``data_path`` parameter use_timestamps (False): whether to export the support of each temporal detection in seconds rather than frame numbers - classes (None): the list of possible class labels. If not provided, - this list will be extracted when :meth:`log_collection` is called, - if possible + classes (None): the list of possible class labels include_confidence (None): whether to include detection confidences in the export. The supported values are: @@ -2603,18 +2572,6 @@ def setup(self): ) self._media_exporter.setup() - def log_collection(self, sample_collection): - if self.classes is None: - if sample_collection.default_classes: - self.classes = sample_collection.default_classes - self._parse_classes() - elif sample_collection.classes: - self.classes = next(iter(sample_collection.classes.values())) - self._parse_classes() - elif "classes" in sample_collection.info: - self.classes = sample_collection.info["classes"] - self._parse_classes() - def export_sample(self, video_path, temporal_detections, _, metadata=None): _, uuid = self._media_exporter.export(video_path) self._labels_dict[uuid] = _parse_temporal_detections( diff --git a/fiftyone/utils/eval/classification.py b/fiftyone/utils/eval/classification.py index fa92da7ba9..7324847be4 100644 --- a/fiftyone/utils/eval/classification.py +++ b/fiftyone/utils/eval/classification.py @@ -67,11 +67,8 @@ def evaluate_classifications( gt_field ("ground_truth"): the name of the field containing the ground truth :class:`fiftyone.core.labels.Classification` instances eval_key (None): an evaluation key to use to refer to this evaluation - classes (None): the list of possible classes. If not provided, classes - are loaded from :meth:`fiftyone.core.dataset.Dataset.classes` or - :meth:`fiftyone.core.dataset.Dataset.default_classes` if - possible, or else the observed ground truth/predicted labels are - used + classes (None): the list of possible classes. If not provided, the + observed ground truth/predicted labels are used missing (None): a missing label string. Any None-valued labels are given this label for results purposes method ("simple"): a string specifying the evaluation method to use. @@ -86,14 +83,6 @@ def evaluate_classifications( samples, (pred_field, gt_field), fol.Classification, same_type=True ) - if classes is None: - if pred_field in samples.classes: - classes = samples.classes[pred_field] - elif gt_field in samples.classes: - classes = samples.classes[gt_field] - elif samples.default_classes: - classes = samples.default_classes - config = _parse_config(pred_field, gt_field, method, **kwargs) eval_method = config.build() eval_method.ensure_requirements() diff --git a/fiftyone/utils/eval/detection.py b/fiftyone/utils/eval/detection.py index 690f521436..e1fdc4a5c3 100644 --- a/fiftyone/utils/eval/detection.py +++ b/fiftyone/utils/eval/detection.py @@ -101,10 +101,8 @@ def evaluate_detections( :class:`fiftyone.core.labels.Polylines`, or :class:`fiftyone.core.labels.TemporalDetections` eval_key (None): an evaluation key to use to refer to this evaluation - classes (None): the list of possible classes. If not provided, classes - are loaded from :meth:`fiftyone.core.dataset.Dataset.classes` or - :meth:`fiftyone.core.dataset.Dataset.default_classes` if possible, - or else the observed ground truth/predicted labels are used + classes (None): the list of possible classes. If not provided, the + observed ground truth/predicted labels are used missing (None): a missing label string. Any unmatched objects are given this label for results purposes method (None): a string specifying the evaluation method to use. @@ -152,14 +150,6 @@ def evaluate_detections( **kwargs, ) - if classes is None: - if pred_field in samples.classes: - classes = samples.classes[pred_field] - elif gt_field in samples.classes: - classes = samples.classes[gt_field] - elif samples.default_classes: - classes = samples.default_classes - eval_method = config.build() eval_method.ensure_requirements() diff --git a/fiftyone/utils/eval/segmentation.py b/fiftyone/utils/eval/segmentation.py index 91f6e6ab07..f32a0f301f 100644 --- a/fiftyone/utils/eval/segmentation.py +++ b/fiftyone/utils/eval/segmentation.py @@ -75,10 +75,7 @@ def evaluate_segmentations( truth :class:`fiftyone.core.labels.Segmentation` instances eval_key (None): an evaluation key to use to refer to this evaluation mask_targets (None): a dict mapping mask values to labels. If not - provided, mask targets are loaded from - :meth:`fiftyone.core.dataset.Dataset.mask_targets` or - :meth:`fiftyone.core.dataset.Dataset.default_mask_targets` if - possible, or else the observed pixel values are used + provided, the observed pixel values are used method ("simple"): a string specifying the evaluation method to use. Supported values are ``("simple")`` **kwargs: optional keyword arguments for the constructor of the @@ -91,14 +88,6 @@ def evaluate_segmentations( samples, (pred_field, gt_field), fol.Segmentation, same_type=True ) - if mask_targets is None: - if pred_field in samples.mask_targets: - mask_targets = samples.mask_targets[pred_field] - elif gt_field in samples.mask_targets: - mask_targets = samples.mask_targets[gt_field] - elif samples.default_mask_targets: - mask_targets = samples.default_mask_targets - config = _parse_config(pred_field, gt_field, method, **kwargs) eval_method = config.build() eval_method.ensure_requirements() diff --git a/fiftyone/utils/labels.py b/fiftyone/utils/labels.py index 369b7c78ab..187792b65f 100644 --- a/fiftyone/utils/labels.py +++ b/fiftyone/utils/labels.py @@ -153,12 +153,6 @@ def segmentations_to_detections( fol.Segmentation, ) - if mask_targets is None: - if out_field in sample_collection.mask_targets: - mask_targets = sample_collection.mask_targets[out_field] - elif sample_collection.default_mask_targets: - mask_targets = sample_collection.default_mask_targets - samples = sample_collection.select_fields(in_field) in_field, processing_frames = samples._handle_frame_field(in_field) out_field, _ = samples._handle_frame_field(out_field) @@ -278,12 +272,6 @@ def segmentations_to_polylines( fol.Segmentation, ) - if mask_targets is None: - if out_field in sample_collection.mask_targets: - mask_targets = sample_collection.mask_targets[out_field] - elif sample_collection.default_mask_targets: - mask_targets = sample_collection.default_mask_targets - samples = sample_collection.select_fields(in_field) in_field, processing_frames = samples._handle_frame_field(in_field) out_field, _ = samples._handle_frame_field(out_field) diff --git a/fiftyone/utils/tf.py b/fiftyone/utils/tf.py index 13c55e644c..08c6a06a6d 100644 --- a/fiftyone/utils/tf.py +++ b/fiftyone/utils/tf.py @@ -846,8 +846,7 @@ class TFObjectDetectionDatasetExporter(TFRecordsDatasetExporter): images to disk. By default, ``fiftyone.config.default_image_ext`` is used force_rgb (False): whether to force convert all images to RGB - classes (None): the list of possible class labels. If omitted, the - class list is dynamically generated as samples are processed + classes (None): the list of possible class labels """ def __init__( @@ -873,15 +872,6 @@ def __init__( def label_cls(self): return fol.Detections - def log_collection(self, sample_collection): - if self.classes is None: - if sample_collection.default_classes: - self.classes = sample_collection.default_classes - elif sample_collection.classes: - self.classes = next(iter(sample_collection.classes.values())) - elif "classes" in sample_collection.info: - self.classes = sample_collection.info["classes"] - def _make_example_generator(self): return TFObjectDetectionExampleGenerator( force_rgb=self.force_rgb, classes=self.classes @@ -1019,8 +1009,7 @@ class TFObjectDetectionExampleGenerator(TFExampleGenerator): Args: force_rgb (False): whether to force convert all images to RGB - classes (None): the list of possible class labels. If omitted, the - class list is dynamically generated as examples are processed + classes (None): the list of possible class labels """ def __init__(self, force_rgb=False, classes=None): @@ -1029,14 +1018,14 @@ def __init__(self, force_rgb=False, classes=None): self.classes = classes if classes: - labels_map_rev = _to_labels_map_rev(classes) dynamic_classes = False + labels_map_rev = _to_labels_map_rev(classes) else: - labels_map_rev = {} dynamic_classes = True + labels_map_rev = {} - self._labels_map_rev = labels_map_rev self._dynamic_classes = dynamic_classes + self._labels_map_rev = labels_map_rev def make_tf_example(self, image_or_path, detections, filename=None): """Makes a ``tf.train.Example`` for the given data. diff --git a/fiftyone/utils/yolo.py b/fiftyone/utils/yolo.py index b153e9bd6c..d04ba85734 100644 --- a/fiftyone/utils/yolo.py +++ b/fiftyone/utils/yolo.py @@ -25,7 +25,7 @@ def add_yolo_labels( sample_collection, label_field, labels_path, - classes=None, + classes, include_missing=False, ): """Adds the given YOLO-formatted labels to the collection. @@ -55,22 +55,12 @@ def add_yolo_labels( - a directory containing YOLO TXT files whose filenames (less extension) correspond to image filenames in ``sample_collection``, in any order - classes (None): the list of class label strings. If not provided, these - must be available from - :meth:`fiftyone.core.collections.SampleCollection.get_classes` + classes: the list of class label strings include_missing (False): whether to insert empty :class:`Detections ` instances for any samples in the input collection whose ``label_field`` is ``None`` after import """ - if classes is None: - classes = sample_collection.get_classes(label_field) - - if not classes: - raise ValueError( - "You must provide `classes` in order to load YOLO labels" - ) - if isinstance(labels_path, (list, tuple)): # Explicit list of labels files labels = [load_yolo_annotations(p, classes) for p in labels_path] @@ -610,9 +600,7 @@ class YOLOv4DatasetExporter( If None, the default value of this parameter will be chosen based on the value of the ``data_path`` parameter - classes (None): the list of possible class labels. If not provided, - this list will be extracted when :meth:`log_collection` is called, - if possible + classes (None): the list of possible class labels include_confidence (False): whether to include detection confidences in the export, if they exist image_format (None): the image format to use when writing in-memory @@ -707,21 +695,6 @@ def setup(self): ) self._media_exporter.setup() - def log_collection(self, sample_collection): - if self.classes is None: - if sample_collection.default_classes: - self.classes = sample_collection.default_classes - self._parse_classes() - self._dynamic_classes = False - elif sample_collection.classes: - self.classes = next(iter(sample_collection.classes.values())) - self._parse_classes() - self._dynamic_classes = False - elif "classes" in sample_collection.info: - self.classes = sample_collection.info["classes"] - self._parse_classes() - self._dynamic_classes = False - def export_sample(self, image_or_path, detections, metadata=None): out_image_path, uuid = self._media_exporter.export(image_or_path) @@ -821,9 +794,7 @@ class YOLOv5DatasetExporter( If None, the default value of this parameter will be chosen based on the value of the ``data_path`` parameter - classes (None): the list of possible class labels. If not provided, - this list will be extracted when :meth:`log_collection` is called, - if possible + classes (None): the list of possible class labels include_confidence (False): whether to include detection confidences in the export, if they exist image_format (None): the image format to use when writing in-memory @@ -906,21 +877,6 @@ def setup(self): ) self._media_exporter.setup() - def log_collection(self, sample_collection): - if self.classes is None: - if sample_collection.default_classes: - self.classes = sample_collection.default_classes - self._parse_classes() - self._dynamic_classes = False - elif sample_collection.classes: - self.classes = next(iter(sample_collection.classes.values())) - self._parse_classes() - self._dynamic_classes = False - elif "classes" in sample_collection.info: - self.classes = sample_collection.info["classes"] - self._parse_classes() - self._dynamic_classes = False - def export_sample(self, image_or_path, detections, metadata=None): _, uuid = self._media_exporter.export(image_or_path) diff --git a/tests/unittests/import_export_tests.py b/tests/unittests/import_export_tests.py index 8c94deed7d..527a2b0cc5 100644 --- a/tests/unittests/import_export_tests.py +++ b/tests/unittests/import_export_tests.py @@ -1263,9 +1263,7 @@ def test_add_yolo_labels(self): # Standard - fouy.add_yolo_labels( - dataset, "yolo", labels_path=yolo_labels_path, classes=classes - ) + fouy.add_yolo_labels(dataset, "yolo", yolo_labels_path, classes) self.assertEqual( dataset.count_values("predictions.detections.label"), dataset.count_values("yolo.detections.label"), @@ -1277,8 +1275,8 @@ def test_add_yolo_labels(self): fouy.add_yolo_labels( dataset, "yolo_inclusive", - labels_path=yolo_labels_path, - classes=classes, + yolo_labels_path, + classes, include_missing=True, ) self.assertEqual( @@ -1300,9 +1298,7 @@ def test_add_coco_labels(self): ) coco_labels_path = os.path.join(export_dir, "labels.json") - fouc.add_coco_labels( - dataset, "coco", coco_labels_path, classes=classes - ) + fouc.add_coco_labels(dataset, "coco", coco_labels_path, classes) self.assertEqual( dataset.count_values("predictions.detections.label"), dataset.count_values("coco.detections.label"), From 14b086748154a11b38be2aab82f4f62e14cc1228 Mon Sep 17 00:00:00 2001 From: brimoor Date: Thu, 9 Jun 2022 21:30:11 -0400 Subject: [PATCH 2/2] removing default classes/mask targets from annotation API too --- docs/source/integrations/cvat.rst | 17 ++++------------- docs/source/user_guide/annotation.rst | 17 ++++------------- fiftyone/utils/annotations.py | 16 ++-------------- 3 files changed, 10 insertions(+), 40 deletions(-) diff --git a/docs/source/integrations/cvat.rst b/docs/source/integrations/cvat.rst index 3d94669abe..b819a44625 100644 --- a/docs/source/integrations/cvat.rst +++ b/docs/source/integrations/cvat.rst @@ -431,9 +431,8 @@ details: `label_field` or all fields in `label_schema` without classes specified. All new label fields must have a class list provided via one of the supported methods. For existing label fields, if classes are not provided - by this argument nor `label_schema`, they are retrieved from - :meth:`Dataset.get_classes() ` - if possible, or else the observed labels on your dataset are used + by this argument nor `label_schema`, the observed labels on your dataset + are used - **attributes** (*True*): specifies the label attributes of each label field to include (other than their `label`, which is always included) in the annotation export. Can be any of the following: @@ -631,16 +630,8 @@ FiftyOne can infer the appropriate values to use: - **label_type**: if omitted, the |Label| type of the field will be used to infer the appropriate value for this parameter -- **classes**: if omitted for a non-semantic segmentation field, the class - lists from the :meth:`classes ` or - :meth:`default_classes ` - properties of your dataset will be used, if available. Otherwise, the - observed labels on your dataset will be used to construct a classes list -- **mask_targets**: if omitted for a semantic segmentation field, the mask - targets from the - :meth:`mask_targets ` or - :meth:`default_mask_targets ` - properties of your dataset will be used, if available +- **classes**: if omitted for a non-semantic segmentation field, the observed + labels on your dataset will be used to construct a classes list .. _cvat-label-attributes: diff --git a/docs/source/user_guide/annotation.rst b/docs/source/user_guide/annotation.rst index 9e26f1402e..666a82bc2f 100644 --- a/docs/source/user_guide/annotation.rst +++ b/docs/source/user_guide/annotation.rst @@ -472,9 +472,8 @@ more details: `label_field` or all fields in `label_schema` without classes specified. All new label fields must have a class list provided via one of the supported methods. For existing label fields, if classes are not provided - by this argument nor `label_schema`, they are retrieved from - :meth:`Dataset.get_classes() ` - if possible, or else the observed labels on your dataset are used + by this argument nor `label_schema`, the observed labels on your dataset + are used - **attributes** (*True*): specifies the label attributes of each label field to include (other than their `label`, which is always included) in the annotation export. Can be any of the following: @@ -644,16 +643,8 @@ FiftyOne can infer the appropriate values to use: - **label_type**: if omitted, the |Label| type of the field will be used to infer the appropriate value for this parameter -- **classes**: if omitted for a non-semantic segmentation field, the class - lists from the :meth:`classes ` or - :meth:`default_classes ` - properties of your dataset will be used, if available. Otherwise, the - observed labels on your dataset will be used to construct a classes list -- **mask_targets**: if omitted for a semantic segmentation field, the mask - targets from the - :meth:`mask_targets ` or - :meth:`default_mask_targets ` - properties of your dataset will be used, if available +- **classes**: if omitted for a non-semantic segmentation field, the observed + labels on your dataset will be used to construct a classes list .. _annotation-label-attributes: diff --git a/fiftyone/utils/annotations.py b/fiftyone/utils/annotations.py index 42e72f01f8..61d79a64c5 100644 --- a/fiftyone/utils/annotations.py +++ b/fiftyone/utils/annotations.py @@ -115,10 +115,8 @@ def annotate( ``label_field`` or all fields in ``label_schema`` without classes specified. All new label fields must have a class list provided via one of the supported methods. For existing label fields, if classes - are not provided by this argument nor ``label_schema``, they are - parsed from - :meth:`fiftyone.core.collections.SampleCollection.get_classes` if - possible, or else the observed labels on your dataset are used + are not provided by this argument nor ``label_schema``, the + observed labels on your dataset are used attributes (True): specifies the label attributes of each label field to include (other than their ``label``, which is always included) in the annotation export. Can be any of the @@ -772,10 +770,6 @@ def _get_classes( % label_field ) - classes = samples.get_classes(label_field) - if classes: - return classes - _, label_path = samples._get_label_field_path(label_field, "label") return sorted( set(samples._dataset.distinct(label_path)) @@ -815,12 +809,6 @@ def _get_mask_targets(samples, mask_targets, label_field, label_info): if "mask_targets" in label_info: mask_targets = label_info["mask_targets"] - if mask_targets is None and label_field in samples.mask_targets: - mask_targets = samples.mask_targets[label_field] - - if mask_targets is None and samples.default_mask_targets: - mask_targets = samples.default_mask_targets - if mask_targets is None: mask_targets = {i: str(i) for i in range(1, 256)} mask_targets[0] = "background"