Repository: voxel51/fiftyone

Issue:
[FR] Add progress bar in CVAT upload
To upload big datasets to CVAT, or datasets with where each sample size is large (4k images), I set the `task_size` and `segment_size` low enough, for example around `100`.  However, during the uploading process, we have no indication of the process, rather than a simple log `Uploading samples to CVAT...` and `Upload complete` in the end.

Simple enough, we can add `ProgressBar` here https://github.com/voxel51/fiftyone/blob/cf0a75d9ec5352db8027a105bcd300c54fab0d53/fiftyone/utils/cvat.py#L4134

as follows:
```
with fo.ProgressBar(total= num_samples, iters_str="batches") as pb:
    for idx, offset in enumerate(range(0, num_samples, batch_size)):
        ...
        pb.update(batch_size)
```
and probably remove [this](https://github.com/voxel51/fiftyone/blob/cf0a75d9ec5352db8027a105bcd300c54fab0d53/fiftyone/utils/cvat.py#L3171) and [this](https://github.com/voxel51/fiftyone/blob/cf0a75d9ec5352db8027a105bcd300c54fab0d53/fiftyone/utils/cvat.py#L3173) log.


Discussion:
@ioangatop I think this would be a nice improvement! Your suggestions look pretty spot on, if you are willing to contribute, I'd be happy to review a PR for this :hugs: 
@ehofesmann I'll be happy too! Can you please add me as a collaborator, because I can not create a branch? Or is there a different way to contribute?
Amazing! The way to go about it is to create your own fork of the repository, create a custom branch in that fork with your changes, then open a pull request from your fork/branch to the `develop` branch in this voxel51 repository. Here is an example of a PR from a community member: https://github.com/voxel51/fiftyone/pull/1691

Resolving patch:
diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py
--- a/fiftyone/utils/cvat.py
+++ b/fiftyone/utils/cvat.py
@@ -500,7 +500,9 @@ def __init__(
             )
 
         data_path = self._parse_data_path(
-            dataset_dir=dataset_dir, data_path=data_path, default="data/",
+            dataset_dir=dataset_dir,
+            data_path=data_path,
+            default="data/",
         )
 
         labels_path = self._parse_labels_path(
@@ -673,7 +675,9 @@ def __init__(
             )
 
         data_path = self._parse_data_path(
-            dataset_dir=dataset_dir, data_path=data_path, default="data/",
+            dataset_dir=dataset_dir,
+            data_path=data_path,
+            default="data/",
         )
 
         labels_path = self._parse_labels_path(
@@ -1019,7 +1023,9 @@ def __init__(
         )
 
         labels_path = self._parse_labels_path(
-            export_dir=export_dir, labels_path=labels_path, default="labels/",
+            export_dir=export_dir,
+            labels_path=labels_path,
+            default="labels/",
         )
 
         super().__init__(export_dir=export_dir)
@@ -1052,7 +1058,8 @@ def frame_labels_cls(self):
     def setup(self):
         self._writer = CVATVideoAnnotationWriter()
         self._media_exporter = foud.ImageExporter(
-            self.export_media, export_path=self.data_path,
+            self.export_media,
+            export_path=self.data_path,
         )
         self._media_exporter.setup()
 
@@ -3167,11 +3174,7 @@ def connect_to_api(self):
 
     def upload_annotations(self, samples, launch_editor=False):
         api = self.connect_to_api()
-
-        logger.info("Uploading samples to CVAT...")
         results = api.upload_samples(samples, self)
-        logger.info("Upload complete")
-
         api.close()
 
         if launch_editor:
@@ -3518,7 +3521,10 @@ def task_annotation_url(self, task_id):
         return "%s/annotations" % self.task_url(task_id)
 
     def task_annotation_formatted_url(
-        self, task_id, anno_filepath, anno_format="CVAT 1.1",
+        self,
+        task_id,
+        anno_filepath,
+        anno_format="CVAT 1.1",
     ):
         return "%s/annotations?format=%s&filename=%s" % (
             self.task_url(task_id),
@@ -3703,7 +3709,10 @@ def get_user_id(self, username):
             the user ID, or None if the user was not found
         """
         user_id = self._get_value_update_map(
-            username, self._user_id_map, "username", self.user_search_url,
+            username,
+            self._user_id_map,
+            "username",
+            self.user_search_url,
         )
 
         if username is not None and user_id is None:
@@ -3743,7 +3752,10 @@ def get_project_name(self, project_id):
             return project_name
 
         return self._get_value_from_search(
-            self.project_id_search_url, project_id, "id", "name",
+            self.project_id_search_url,
+            project_id,
+            "id",
+            "name",
         )
 
     def get_empty_projects(self, project_ids):
@@ -3807,7 +3819,10 @@ def project_exists(self, project_id):
         """
         return (
             self._get_value_from_search(
-                self.project_id_search_url, project_id, "id", "id",
+                self.project_id_search_url,
+                project_id,
+                "id",
+                "id",
             )
             is not None
         )
@@ -3940,7 +3955,10 @@ def task_exists(self, task_id):
         """
         return (
             self._get_value_from_search(
-                self.task_id_search_url, task_id, "id", "id",
+                self.task_id_search_url,
+                task_id,
+                "id",
+                "id",
             )
             is not None
         )
@@ -4131,78 +4149,94 @@ def upload_samples(self, samples, backend):
             # might get labels
             samples.ensure_frames()
 
-        for idx, offset in enumerate(range(0, num_samples, batch_size)):
-            samples_batch = samples[offset : (offset + batch_size)]
-            anno_tags = []
-            anno_shapes = []
-            anno_tracks = []
-
-            for label_field, label_info in label_schema.items():
-                _tags = []
-                _shapes = []
-                _tracks = []
-
-                if label_field not in id_map:
-                    id_map[label_field] = {}
+        logger.info("Uploading samples to CVAT...")
 
-                if label_field not in labels_task_map:
-                    labels_task_map[label_field] = []
+        with fou.ProgressBar(
+            total=num_samples,
+            iters_str="samples",
+            quiet=num_samples <= batch_size,
+        ) as pb:
+
+            for idx, offset in enumerate(range(0, num_samples, batch_size)):
+                samples_batch = samples[offset : (offset + batch_size)]
+                anno_tags = []
+                anno_shapes = []
+                anno_tracks = []
+
+                for label_field, label_info in label_schema.items():
+                    _tags = []
+                    _shapes = []
+                    _tracks = []
+
+                    if label_field not in id_map:
+                        id_map[label_field] = {}
+
+                    if label_field not in labels_task_map:
+                        labels_task_map[label_field] = []
+
+                    if label_info.get("existing_field", False):
+                        label_type = label_info["type"]
+                        only_keyframes = label_info.get(
+                            "only_keyframes", False
+                        )
 
-                if label_info.get("existing_field", False):
-                    label_type = label_info["type"]
-                    only_keyframes = label_info.get("only_keyframes", False)
+                        self._update_shapes_tags_tracks(
+                            _tags,
+                            _shapes,
+                            _tracks,
+                            id_map,
+                            label_type,
+                            samples_batch,
+                            label_field,
+                            label_info,
+                            cvat_schema,
+                            assign_scalar_attrs,
+                            only_keyframes,
+                            occluded_attrs,
+                        )
 
-                    self._update_shapes_tags_tracks(
-                        _tags,
-                        _shapes,
-                        _tracks,
-                        id_map,
-                        label_type,
-                        samples_batch,
-                        label_field,
-                        label_info,
-                        cvat_schema,
-                        assign_scalar_attrs,
-                        only_keyframes,
-                        occluded_attrs,
-                    )
+                    anno_tags.extend(_tags)
+                    anno_shapes.extend(_shapes)
+                    anno_tracks.extend(_tracks)
 
-                anno_tags.extend(_tags)
-                anno_shapes.extend(_shapes)
-                anno_tracks.extend(_tracks)
+                # We must do this here because `cvat_schema` may be altered the
+                # first time shapes are created
+                if project_id is None and project_name is not None:
+                    project_id = self.create_project(project_name, cvat_schema)
+                    project_ids.append(project_id)
 
-            # We must do this here because `cvat_schema` may be altered the
-            # first time shapes are created
-            if project_id is None and project_name is not None:
-                project_id = self.create_project(project_name, cvat_schema)
-                project_ids.append(project_id)
+                _dataset_name = samples_batch._dataset.name.replace(" ", "_")
+                task_name = "FiftyOne_%s" % _dataset_name
 
-            _dataset_name = samples_batch._dataset.name.replace(" ", "_")
-            task_name = "FiftyOne_%s" % _dataset_name
+                (
+                    task_id,
+                    class_id_map,
+                    attr_id_map,
+                ) = self._create_task_upload_data(
+                    config,
+                    idx,
+                    task_name,
+                    cvat_schema,
+                    project_id,
+                    samples_batch,
+                    task_ids,
+                    job_ids,
+                    frame_id_map,
+                )
 
-            task_id, class_id_map, attr_id_map = self._create_task_upload_data(
-                config,
-                idx,
-                task_name,
-                cvat_schema,
-                project_id,
-                samples_batch,
-                task_ids,
-                job_ids,
-                frame_id_map,
-            )
+                for label_field in label_schema.keys():
+                    labels_task_map[label_field].append(task_id)
 
-            for label_field in label_schema.keys():
-                labels_task_map[label_field].append(task_id)
+                server_id_map = self._upload_annotations(
+                    anno_shapes,
+                    anno_tags,
+                    anno_tracks,
+                    class_id_map,
+                    attr_id_map,
+                    task_id,
+                )
 
-            server_id_map = self._upload_annotations(
-                anno_shapes,
-                anno_tags,
-                anno_tracks,
-                class_id_map,
-                attr_id_map,
-                task_id,
-            )
+                pb.update(batch_size)
 
         return CVATAnnotationResults(
             samples,
@@ -4307,7 +4341,10 @@ def download_annotations(self, results):
 
                 _cvat_classes = class_map.keys()
                 tags, shapes, tracks = self._filter_field_classes(
-                    all_tags, all_shapes, all_tracks, _cvat_classes,
+                    all_tags,
+                    all_shapes,
+                    all_tracks,
+                    _cvat_classes,
                 )
 
                 is_last_field = lf_ind == len(label_fields) - 1
@@ -4569,7 +4606,10 @@ def _convert_cvat_schema(
                 label_attrs[occluded_attr] = {}
 
             classes_and_attrs.append(
-                {"classes": [name], "attributes": label_attrs,}
+                {
+                    "classes": [name],
+                    "attributes": label_attrs,
+                }
             )
 
         label_field_classes = {}
@@ -6858,7 +6898,7 @@ def curve_length(points):
             for i in range(1, len(points)):
                 dx = points[i]["x"] - points[i - 1]["x"]
                 dy = points[i]["y"] - points[i - 1]["y"]
-                length += np.sqrt(dx ** 2 + dy ** 2)
+                length += np.sqrt(dx**2 + dy**2)
             return length
 
         def curve_to_offset_vec(points, length):
@@ -6867,7 +6907,7 @@ def curve_to_offset_vec(points, length):
             for i in range(1, len(points)):
                 dx = points[i]["x"] - points[i - 1]["x"]
                 dy = points[i]["y"] - points[i - 1]["y"]
-                accumulated_length += np.sqrt(dx ** 2 + dy ** 2)
+                accumulated_length += np.sqrt(dx**2 + dy**2)
                 offset_vector.append(accumulated_length / length)
 
             return offset_vector


Test patch: