Repository: voxel51/fiftyone

Issue:
[FR] Update Labelbox integration to handle deprecation of datasets
Labelbox is moving away from the "dataset" methodology for structuring data and focusing on batches. Our integration heavily uses labelbox datasets and should be updated to handle this migration: https://docs.labelbox.com/reference/migrating-to-workflows#how-do-these-changes-affect-me


Discussion:

Resolving patch:
diff --git a/fiftyone/utils/labelbox.py b/fiftyone/utils/labelbox.py
--- a/fiftyone/utils/labelbox.py
+++ b/fiftyone/utils/labelbox.py
@@ -457,6 +457,19 @@ def delete_projects(self, project_ids, delete_datasets=True):
         for project_id in project_ids:
             self.delete_project(project_id, delete_datasets=delete_datasets)
 
+    def delete_unused_ontologies(self):
+        """Deletes unused ontologies from the Labelbox server."""
+        deleted_ontologies = []
+        unused_ontologies = self._client.get_unused_ontologies()
+        while unused_ontologies:
+            for o in unused_ontologies:
+                deleted_ontologies.append(o)
+                self._client.delete_unused_ontology(o)
+
+            unused_ontologies = self._client.get_unused_ontologies()
+
+        logger.info("Deleted %d ontologies." % len(deleted_ontologies))
+
     def launch_editor(self, url=None):
         """Launches the Labelbox editor in your default web browser.
 
@@ -473,7 +486,7 @@ def upload_data(self, samples, lb_dataset, media_field="filepath"):
         """Uploads the media for the given samples to Labelbox.
 
         This method uses ``labelbox.schema.dataset.Dataset.create_data_rows()``
-        to add data in batches, and sets the external ID of each DataRow to the
+        to add data in batches, and sets the global key of each DataRow to the
         ID of the corresponding sample.
 
         Args:
@@ -487,14 +500,16 @@ def upload_data(self, samples, lb_dataset, media_field="filepath"):
         media_paths, sample_ids = samples.values([media_field, "id"])
 
         upload_info = []
-        for media_path, sample_id in zip(media_paths, sample_ids):
-            item_url = self._client.upload_file(media_path)
-            upload_info.append(
-                {
-                    lb.DataRow.row_data: item_url,
-                    lb.DataRow.external_id: sample_id,
-                }
-            )
+
+        with fou.ProgressBar(iters_str="samples") as pb:
+            for media_path, sample_id in pb(zip(media_paths, sample_ids)):
+                item_url = self._client.upload_file(media_path)
+                upload_info.append(
+                    {
+                        lb.DataRow.row_data: item_url,
+                        lb.DataRow.global_key: sample_id,
+                    }
+                )
 
         task = lb_dataset.create_data_rows(upload_info)
         task.wait_till_done()
@@ -517,6 +532,7 @@ def upload_samples(self, samples, anno_key, backend):
         project_name = config.project_name
         members = config.members
         classes_as_attrs = config.classes_as_attrs
+        is_video = samples.media_type == fomm.VIDEO
 
         for label_field, label_info in label_schema.items():
             if label_info["existing_field"]:
@@ -534,7 +550,7 @@ def upload_samples(self, samples, anno_key, backend):
         self.upload_data(samples, dataset, media_field=media_field)
 
         project = self._setup_project(
-            project_name, dataset, label_schema, classes_as_attrs
+            project_name, dataset, label_schema, classes_as_attrs, is_video
         )
 
         if members:
@@ -583,7 +599,7 @@ def download_annotations(self, results):
 
         for d in labels_json:
             labelbox_id = d["DataRow ID"]
-            sample_id = d["External ID"]
+            sample_id = d["Global Key"]
 
             if sample_id is None:
                 logger.warning(
@@ -594,7 +610,9 @@ def download_annotations(self, results):
             metadata = self._get_sample_metadata(project, sample_id)
             if metadata is None:
                 logger.warning(
-                    "Skipping sample '%s' with no metadata", sample_id
+                    "Skipping sample '%s' with no metadata, likely due to not "
+                    "finding a DataRow with a matching Global Key",
+                    sample_id,
                 )
                 continue
 
@@ -685,12 +703,18 @@ def _build_frame_id_map(self, samples):
         return frame_id_map
 
     def _setup_project(
-        self, project_name, dataset, label_schema, classes_as_attrs
+        self, project_name, dataset, label_schema, classes_as_attrs, is_video
     ):
+        media_type = lb.MediaType.Video if is_video else lb.MediaType.Image
         project = self._client.create_project(
-            name=project_name, queue_mode=lb.QueueMode.Dataset
+            name=project_name,
+            media_type=media_type,
+            queue_mode=lb.QueueMode.Batch,
+        )
+        project.create_batch(
+            name=str(uuid4()),
+            data_rows=dataset.data_rows(),
         )
-        project.datasets.connect(dataset)
 
         self._setup_editor(project, label_schema, classes_as_attrs)
 
@@ -774,14 +798,14 @@ def _build_attributes(self, attr_schema):
             if attr_type == "text":
                 attr = lbo.Classification(
                     class_type=class_type,
-                    instructions=attr_name,
+                    name=attr_name,
                 )
             else:
                 attr_values = attr_info["values"]
                 options = [lbo.Option(value=str(v)) for v in attr_values]
                 attr = lbo.Classification(
                     class_type=class_type,
-                    instructions=attr_name,
+                    name=attr_name,
                     options=options,
                 )
 
@@ -823,27 +847,27 @@ def _build_classifications(
                     prefix = "field:%s_class:%s_attr:" % (label_field, str(sc))
                     sub_attrs = deepcopy(attrs)
                     for attr in sub_attrs:
-                        attr.instructions = prefix + attr.instructions
+                        attr.name = prefix + attr.name
 
                 options.append(lbo.Option(value=str(sc), options=sub_attrs))
 
         if label_type == "scalar" and not classes:
             classification = lbo.Classification(
                 class_type=lbo.Classification.Type.TEXT,
-                instructions=name,
+                name=name,
             )
             classifications.append(classification)
         elif label_type == "classifications":
             classification = lbo.Classification(
                 class_type=lbo.Classification.Type.CHECKLIST,
-                instructions=name,
+                name=name,
                 options=options,
             )
             classifications.append(classification)
         else:
             classification = lbo.Classification(
                 class_type=lbo.Classification.Type.RADIO,
-                instructions=name,
+                name=name,
                 options=options,
             )
             classifications.append(classification)
@@ -910,7 +934,7 @@ def _create_classes_as_attrs(self, classes, general_attrs):
 
         classes_attr = lbo.Classification(
             class_type=lbo.Classification.Type.RADIO,
-            instructions="class_name",
+            name="class_name",
             options=options,
             required=True,
         )
@@ -919,12 +943,15 @@ def _create_classes_as_attrs(self, classes, general_attrs):
 
     def _get_sample_metadata(self, project, sample_id):
         metadata = None
-        for dataset in project.datasets():
-            try:
-                data_row = dataset.data_row_for_external_id(sample_id)
-                metadata = data_row.media_attributes
-            except lb.exceptions.ResourceNotFoundError:
-                pass
+        try:
+            data_row_id_response = (
+                self._client.get_data_row_ids_for_global_keys([sample_id])
+            )
+            data_row_id = data_row_id_response["results"][0]
+            data_row = self._client.get_data_row(data_row_id)
+            metadata = data_row.media_attributes
+        except:
+            pass
 
         return metadata
 
@@ -2132,7 +2159,8 @@ def _make_mask(instance_uri, color):
     }
 
 
-# https://labelbox.com/docs/exporting-data/export-format-detail#video
+# Parse v1 export format
+# https://docs.labelbox.com/reference/export-video-annotations
 def _parse_video_labels(video_label_d, frame_size):
     url_or_filepath = video_label_d["frames"]
     label_d_list = _download_or_load_ndjson(url_or_filepath)
@@ -2145,7 +2173,8 @@ def _parse_video_labels(video_label_d, frame_size):
     return frames
 
 
-# https://labelbox.com/docs/exporting-data/export-format-detail#images
+# Parse v1 export format
+# https://docs.labelbox.com/reference/export-image-annotations#annotation-export-formats
 def _parse_image_labels(label_d, frame_size, class_attr=None):
     labels = {}
 


Test patch: