dtlpy 1.115.44__py3-none-any.whl → 1.117.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dtlpy/__init__.py +491 -491
- dtlpy/__version__.py +1 -1
- dtlpy/assets/__init__.py +26 -26
- dtlpy/assets/code_server/config.yaml +2 -2
- dtlpy/assets/code_server/installation.sh +24 -24
- dtlpy/assets/code_server/launch.json +13 -13
- dtlpy/assets/code_server/settings.json +2 -2
- dtlpy/assets/main.py +53 -53
- dtlpy/assets/main_partial.py +18 -18
- dtlpy/assets/mock.json +11 -11
- dtlpy/assets/model_adapter.py +83 -83
- dtlpy/assets/package.json +61 -61
- dtlpy/assets/package_catalog.json +29 -29
- dtlpy/assets/package_gitignore +307 -307
- dtlpy/assets/service_runners/__init__.py +33 -33
- dtlpy/assets/service_runners/converter.py +96 -96
- dtlpy/assets/service_runners/multi_method.py +49 -49
- dtlpy/assets/service_runners/multi_method_annotation.py +54 -54
- dtlpy/assets/service_runners/multi_method_dataset.py +55 -55
- dtlpy/assets/service_runners/multi_method_item.py +52 -52
- dtlpy/assets/service_runners/multi_method_json.py +52 -52
- dtlpy/assets/service_runners/single_method.py +37 -37
- dtlpy/assets/service_runners/single_method_annotation.py +43 -43
- dtlpy/assets/service_runners/single_method_dataset.py +43 -43
- dtlpy/assets/service_runners/single_method_item.py +41 -41
- dtlpy/assets/service_runners/single_method_json.py +42 -42
- dtlpy/assets/service_runners/single_method_multi_input.py +45 -45
- dtlpy/assets/voc_annotation_template.xml +23 -23
- dtlpy/caches/base_cache.py +32 -32
- dtlpy/caches/cache.py +473 -473
- dtlpy/caches/dl_cache.py +201 -201
- dtlpy/caches/filesystem_cache.py +89 -89
- dtlpy/caches/redis_cache.py +84 -84
- dtlpy/dlp/__init__.py +20 -20
- dtlpy/dlp/cli_utilities.py +367 -367
- dtlpy/dlp/command_executor.py +764 -764
- dtlpy/dlp/dlp +1 -1
- dtlpy/dlp/dlp.bat +1 -1
- dtlpy/dlp/dlp.py +128 -128
- dtlpy/dlp/parser.py +651 -651
- dtlpy/entities/__init__.py +83 -83
- dtlpy/entities/analytic.py +347 -347
- dtlpy/entities/annotation.py +1879 -1879
- dtlpy/entities/annotation_collection.py +699 -699
- dtlpy/entities/annotation_definitions/__init__.py +20 -20
- dtlpy/entities/annotation_definitions/base_annotation_definition.py +100 -100
- dtlpy/entities/annotation_definitions/box.py +195 -195
- dtlpy/entities/annotation_definitions/classification.py +67 -67
- dtlpy/entities/annotation_definitions/comparison.py +72 -72
- dtlpy/entities/annotation_definitions/cube.py +204 -204
- dtlpy/entities/annotation_definitions/cube_3d.py +149 -149
- dtlpy/entities/annotation_definitions/description.py +32 -32
- dtlpy/entities/annotation_definitions/ellipse.py +124 -124
- dtlpy/entities/annotation_definitions/free_text.py +62 -62
- dtlpy/entities/annotation_definitions/gis.py +69 -69
- dtlpy/entities/annotation_definitions/note.py +139 -139
- dtlpy/entities/annotation_definitions/point.py +117 -117
- dtlpy/entities/annotation_definitions/polygon.py +182 -182
- dtlpy/entities/annotation_definitions/polyline.py +111 -111
- dtlpy/entities/annotation_definitions/pose.py +92 -92
- dtlpy/entities/annotation_definitions/ref_image.py +86 -86
- dtlpy/entities/annotation_definitions/segmentation.py +240 -240
- dtlpy/entities/annotation_definitions/subtitle.py +34 -34
- dtlpy/entities/annotation_definitions/text.py +85 -85
- dtlpy/entities/annotation_definitions/undefined_annotation.py +74 -74
- dtlpy/entities/app.py +220 -220
- dtlpy/entities/app_module.py +107 -107
- dtlpy/entities/artifact.py +174 -174
- dtlpy/entities/assignment.py +399 -399
- dtlpy/entities/base_entity.py +214 -214
- dtlpy/entities/bot.py +113 -113
- dtlpy/entities/codebase.py +292 -292
- dtlpy/entities/collection.py +38 -38
- dtlpy/entities/command.py +169 -169
- dtlpy/entities/compute.py +449 -449
- dtlpy/entities/dataset.py +1299 -1299
- dtlpy/entities/directory_tree.py +44 -44
- dtlpy/entities/dpk.py +470 -470
- dtlpy/entities/driver.py +235 -235
- dtlpy/entities/execution.py +397 -397
- dtlpy/entities/feature.py +124 -124
- dtlpy/entities/feature_set.py +152 -145
- dtlpy/entities/filters.py +798 -798
- dtlpy/entities/gis_item.py +107 -107
- dtlpy/entities/integration.py +184 -184
- dtlpy/entities/item.py +975 -959
- dtlpy/entities/label.py +123 -123
- dtlpy/entities/links.py +85 -85
- dtlpy/entities/message.py +175 -175
- dtlpy/entities/model.py +684 -684
- dtlpy/entities/node.py +1005 -1005
- dtlpy/entities/ontology.py +810 -803
- dtlpy/entities/organization.py +287 -287
- dtlpy/entities/package.py +657 -657
- dtlpy/entities/package_defaults.py +5 -5
- dtlpy/entities/package_function.py +185 -185
- dtlpy/entities/package_module.py +113 -113
- dtlpy/entities/package_slot.py +118 -118
- dtlpy/entities/paged_entities.py +299 -299
- dtlpy/entities/pipeline.py +624 -624
- dtlpy/entities/pipeline_execution.py +279 -279
- dtlpy/entities/project.py +394 -394
- dtlpy/entities/prompt_item.py +505 -505
- dtlpy/entities/recipe.py +301 -301
- dtlpy/entities/reflect_dict.py +102 -102
- dtlpy/entities/resource_execution.py +138 -138
- dtlpy/entities/service.py +974 -963
- dtlpy/entities/service_driver.py +117 -117
- dtlpy/entities/setting.py +294 -294
- dtlpy/entities/task.py +495 -495
- dtlpy/entities/time_series.py +143 -143
- dtlpy/entities/trigger.py +426 -426
- dtlpy/entities/user.py +118 -118
- dtlpy/entities/webhook.py +124 -124
- dtlpy/examples/__init__.py +19 -19
- dtlpy/examples/add_labels.py +135 -135
- dtlpy/examples/add_metadata_to_item.py +21 -21
- dtlpy/examples/annotate_items_using_model.py +65 -65
- dtlpy/examples/annotate_video_using_model_and_tracker.py +75 -75
- dtlpy/examples/annotations_convert_to_voc.py +9 -9
- dtlpy/examples/annotations_convert_to_yolo.py +9 -9
- dtlpy/examples/convert_annotation_types.py +51 -51
- dtlpy/examples/converter.py +143 -143
- dtlpy/examples/copy_annotations.py +22 -22
- dtlpy/examples/copy_folder.py +31 -31
- dtlpy/examples/create_annotations.py +51 -51
- dtlpy/examples/create_video_annotations.py +83 -83
- dtlpy/examples/delete_annotations.py +26 -26
- dtlpy/examples/filters.py +113 -113
- dtlpy/examples/move_item.py +23 -23
- dtlpy/examples/play_video_annotation.py +13 -13
- dtlpy/examples/show_item_and_mask.py +53 -53
- dtlpy/examples/triggers.py +49 -49
- dtlpy/examples/upload_batch_of_items.py +20 -20
- dtlpy/examples/upload_items_and_custom_format_annotations.py +55 -55
- dtlpy/examples/upload_items_with_modalities.py +43 -43
- dtlpy/examples/upload_segmentation_annotations_from_mask_image.py +44 -44
- dtlpy/examples/upload_yolo_format_annotations.py +70 -70
- dtlpy/exceptions.py +125 -125
- dtlpy/miscellaneous/__init__.py +20 -20
- dtlpy/miscellaneous/dict_differ.py +95 -95
- dtlpy/miscellaneous/git_utils.py +217 -217
- dtlpy/miscellaneous/json_utils.py +14 -14
- dtlpy/miscellaneous/list_print.py +105 -105
- dtlpy/miscellaneous/zipping.py +130 -130
- dtlpy/ml/__init__.py +20 -20
- dtlpy/ml/base_feature_extractor_adapter.py +27 -27
- dtlpy/ml/base_model_adapter.py +1287 -1230
- dtlpy/ml/metrics.py +461 -461
- dtlpy/ml/predictions_utils.py +274 -274
- dtlpy/ml/summary_writer.py +57 -57
- dtlpy/ml/train_utils.py +60 -60
- dtlpy/new_instance.py +252 -252
- dtlpy/repositories/__init__.py +56 -56
- dtlpy/repositories/analytics.py +85 -85
- dtlpy/repositories/annotations.py +916 -916
- dtlpy/repositories/apps.py +383 -383
- dtlpy/repositories/artifacts.py +452 -452
- dtlpy/repositories/assignments.py +599 -599
- dtlpy/repositories/bots.py +213 -213
- dtlpy/repositories/codebases.py +559 -559
- dtlpy/repositories/collections.py +332 -332
- dtlpy/repositories/commands.py +152 -152
- dtlpy/repositories/compositions.py +61 -61
- dtlpy/repositories/computes.py +439 -439
- dtlpy/repositories/datasets.py +1585 -1504
- dtlpy/repositories/downloader.py +1157 -923
- dtlpy/repositories/dpks.py +433 -433
- dtlpy/repositories/drivers.py +482 -482
- dtlpy/repositories/executions.py +815 -815
- dtlpy/repositories/feature_sets.py +256 -226
- dtlpy/repositories/features.py +255 -255
- dtlpy/repositories/integrations.py +484 -484
- dtlpy/repositories/items.py +912 -912
- dtlpy/repositories/messages.py +94 -94
- dtlpy/repositories/models.py +1000 -1000
- dtlpy/repositories/nodes.py +80 -80
- dtlpy/repositories/ontologies.py +511 -511
- dtlpy/repositories/organizations.py +525 -525
- dtlpy/repositories/packages.py +1941 -1941
- dtlpy/repositories/pipeline_executions.py +451 -451
- dtlpy/repositories/pipelines.py +640 -640
- dtlpy/repositories/projects.py +539 -539
- dtlpy/repositories/recipes.py +429 -399
- dtlpy/repositories/resource_executions.py +137 -137
- dtlpy/repositories/schema.py +120 -120
- dtlpy/repositories/service_drivers.py +213 -213
- dtlpy/repositories/services.py +1704 -1704
- dtlpy/repositories/settings.py +339 -339
- dtlpy/repositories/tasks.py +1477 -1477
- dtlpy/repositories/times_series.py +278 -278
- dtlpy/repositories/triggers.py +536 -536
- dtlpy/repositories/upload_element.py +257 -257
- dtlpy/repositories/uploader.py +661 -661
- dtlpy/repositories/webhooks.py +249 -249
- dtlpy/services/__init__.py +22 -22
- dtlpy/services/aihttp_retry.py +131 -131
- dtlpy/services/api_client.py +1786 -1785
- dtlpy/services/api_reference.py +40 -40
- dtlpy/services/async_utils.py +133 -133
- dtlpy/services/calls_counter.py +44 -44
- dtlpy/services/check_sdk.py +68 -68
- dtlpy/services/cookie.py +115 -115
- dtlpy/services/create_logger.py +156 -156
- dtlpy/services/events.py +84 -84
- dtlpy/services/logins.py +235 -235
- dtlpy/services/reporter.py +256 -256
- dtlpy/services/service_defaults.py +91 -91
- dtlpy/utilities/__init__.py +20 -20
- dtlpy/utilities/annotations/__init__.py +16 -16
- dtlpy/utilities/annotations/annotation_converters.py +269 -269
- dtlpy/utilities/base_package_runner.py +285 -264
- dtlpy/utilities/converter.py +1650 -1650
- dtlpy/utilities/dataset_generators/__init__.py +1 -1
- dtlpy/utilities/dataset_generators/dataset_generator.py +670 -670
- dtlpy/utilities/dataset_generators/dataset_generator_tensorflow.py +23 -23
- dtlpy/utilities/dataset_generators/dataset_generator_torch.py +21 -21
- dtlpy/utilities/local_development/__init__.py +1 -1
- dtlpy/utilities/local_development/local_session.py +179 -179
- dtlpy/utilities/reports/__init__.py +2 -2
- dtlpy/utilities/reports/figures.py +343 -343
- dtlpy/utilities/reports/report.py +71 -71
- dtlpy/utilities/videos/__init__.py +17 -17
- dtlpy/utilities/videos/video_player.py +598 -598
- dtlpy/utilities/videos/videos.py +470 -470
- {dtlpy-1.115.44.data → dtlpy-1.117.6.data}/scripts/dlp +1 -1
- dtlpy-1.117.6.data/scripts/dlp.bat +2 -0
- {dtlpy-1.115.44.data → dtlpy-1.117.6.data}/scripts/dlp.py +128 -128
- {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/METADATA +186 -186
- dtlpy-1.117.6.dist-info/RECORD +239 -0
- {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/WHEEL +1 -1
- {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/licenses/LICENSE +200 -200
- tests/features/environment.py +551 -551
- dtlpy/assets/__pycache__/__init__.cpython-310.pyc +0 -0
- dtlpy-1.115.44.data/scripts/dlp.bat +0 -2
- dtlpy-1.115.44.dist-info/RECORD +0 -240
- {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/entry_points.txt +0 -0
- {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/top_level.txt +0 -0
dtlpy/utilities/converter.py
CHANGED
|
@@ -1,1650 +1,1650 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
from jinja2 import Environment, PackageLoader
|
|
4
|
-
from multiprocessing.pool import ThreadPool
|
|
5
|
-
from multiprocessing import Lock
|
|
6
|
-
from .base_package_runner import Progress
|
|
7
|
-
from .. import exceptions, entities
|
|
8
|
-
import xml.etree.ElementTree as Et
|
|
9
|
-
from ..services import Reporter
|
|
10
|
-
from itertools import groupby
|
|
11
|
-
from PIL import Image
|
|
12
|
-
import numpy as np
|
|
13
|
-
import mimetypes
|
|
14
|
-
import traceback
|
|
15
|
-
import logging
|
|
16
|
-
import json
|
|
17
|
-
import copy
|
|
18
|
-
import tqdm
|
|
19
|
-
import os
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger(name='dtlpy')
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class AnnotationFormat:
|
|
25
|
-
YOLO = 'yolo'
|
|
26
|
-
COCO = 'coco'
|
|
27
|
-
VOC = 'voc'
|
|
28
|
-
DATALOOP = 'dataloop'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class COCOUtils:
|
|
32
|
-
|
|
33
|
-
@staticmethod
|
|
34
|
-
def binary_mask_to_rle_encode(binary_mask):
|
|
35
|
-
try:
|
|
36
|
-
import pycocotools.mask as coco_utils_mask
|
|
37
|
-
except ModuleNotFoundError:
|
|
38
|
-
raise Exception('To use this functionality please install pycocotools: "pip install pycocotools"')
|
|
39
|
-
fortran_ground_truth_binary_mask = np.asfortranarray(binary_mask.astype(np.uint8))
|
|
40
|
-
encoded_ground_truth = coco_utils_mask.encode(fortran_ground_truth_binary_mask)
|
|
41
|
-
encoded_ground_truth['counts'] = encoded_ground_truth['counts'].decode()
|
|
42
|
-
return encoded_ground_truth
|
|
43
|
-
|
|
44
|
-
@staticmethod
|
|
45
|
-
def binary_mask_to_rle(binary_mask, height, width):
|
|
46
|
-
rle = {'counts': [], 'size': [height, width]}
|
|
47
|
-
counts = rle.get('counts')
|
|
48
|
-
for i, (value, elements) in enumerate(groupby(binary_mask.ravel(order='F'))):
|
|
49
|
-
if i == 0 and value == 1:
|
|
50
|
-
counts.append(0)
|
|
51
|
-
counts.append(len(list(elements)))
|
|
52
|
-
return rle
|
|
53
|
-
|
|
54
|
-
@staticmethod
|
|
55
|
-
def polygon_to_rle(geo, height, width):
|
|
56
|
-
segmentation = [float(n) for n in geo.flatten()]
|
|
57
|
-
area = np.sum(entities.Segmentation.from_polygon(geo=geo, label=None, shape=(height, width)).geo > 0)
|
|
58
|
-
return [segmentation], int(area)
|
|
59
|
-
|
|
60
|
-
@staticmethod
|
|
61
|
-
def rle_to_binary_mask(rle):
|
|
62
|
-
rows, cols = rle['size']
|
|
63
|
-
rle_numbers = rle['counts']
|
|
64
|
-
if isinstance(rle_numbers, list):
|
|
65
|
-
if len(rle_numbers) % 2 != 0:
|
|
66
|
-
rle_numbers.append(0)
|
|
67
|
-
|
|
68
|
-
rle_pairs = np.array(rle_numbers).reshape(-1, 2)
|
|
69
|
-
img = np.zeros(rows * cols, dtype=np.uint8)
|
|
70
|
-
index = 0
|
|
71
|
-
for i, length in rle_pairs:
|
|
72
|
-
index += i
|
|
73
|
-
img[index:index + length] = 1
|
|
74
|
-
index += length
|
|
75
|
-
img = img.reshape(cols, rows)
|
|
76
|
-
return img.T
|
|
77
|
-
else:
|
|
78
|
-
try:
|
|
79
|
-
import pycocotools.mask as coco_utils_mask
|
|
80
|
-
except ModuleNotFoundError:
|
|
81
|
-
raise Exception('To use this functionality please install pycocotools: "pip install pycocotools"')
|
|
82
|
-
img = coco_utils_mask.decode(rle)
|
|
83
|
-
return img
|
|
84
|
-
|
|
85
|
-
@staticmethod
|
|
86
|
-
def rle_to_binary_polygon(segmentation):
|
|
87
|
-
return [segmentation[x:x + 2] for x in range(0, len(segmentation), 2)]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class Converter:
|
|
91
|
-
"""
|
|
92
|
-
Annotation Converter
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
def __init__(self, concurrency=6, return_error_filepath=False):
|
|
96
|
-
self.known_formats = [AnnotationFormat.YOLO,
|
|
97
|
-
AnnotationFormat.COCO,
|
|
98
|
-
AnnotationFormat.VOC,
|
|
99
|
-
AnnotationFormat.DATALOOP]
|
|
100
|
-
self.converter_dict = {
|
|
101
|
-
AnnotationFormat.YOLO: {"from": self.from_yolo, "to": self.to_yolo},
|
|
102
|
-
AnnotationFormat.COCO: {"from": self.from_coco, "to": self.to_coco},
|
|
103
|
-
AnnotationFormat.VOC: {"from": self.from_voc, "to": self.to_voc},
|
|
104
|
-
}
|
|
105
|
-
self.dataset = None
|
|
106
|
-
self.save_to_format = None
|
|
107
|
-
self.xml_template_path = 'voc_annotation_template.xml'
|
|
108
|
-
self.labels = dict()
|
|
109
|
-
self._only_bbox = False
|
|
110
|
-
self._progress = None
|
|
111
|
-
self._progress_update_frequency = 5
|
|
112
|
-
self._update_agent_progress = False
|
|
113
|
-
self._progress_checkpoint = 0
|
|
114
|
-
self._checkpoint_lock = Lock()
|
|
115
|
-
self.remote_items = None
|
|
116
|
-
self.concurrency = concurrency
|
|
117
|
-
self.return_error_filepath = return_error_filepath
|
|
118
|
-
|
|
119
|
-
def attach_agent_progress(self, progress: Progress, progress_update_frequency: int = None):
|
|
120
|
-
"""
|
|
121
|
-
Attach agent progress.
|
|
122
|
-
|
|
123
|
-
:param Progress progress: the progress object that follows the work
|
|
124
|
-
:param int progress_update_frequency: progress update frequency in percentages
|
|
125
|
-
"""
|
|
126
|
-
self._progress = progress
|
|
127
|
-
self._progress_update_frequency = progress_update_frequency if progress_update_frequency is not None \
|
|
128
|
-
else self._progress_update_frequency
|
|
129
|
-
self._update_agent_progress = True
|
|
130
|
-
|
|
131
|
-
@property
|
|
132
|
-
def _update_progress_active(self):
|
|
133
|
-
return self._progress is not None and self._update_agent_progress and isinstance(
|
|
134
|
-
self._progress_update_frequency, int)
|
|
135
|
-
|
|
136
|
-
def __update_progress(self, total, of_total):
|
|
137
|
-
if self._update_progress_active:
|
|
138
|
-
try:
|
|
139
|
-
progress = int((of_total / total) * 100)
|
|
140
|
-
if progress > self._progress_checkpoint and (progress % self._progress_update_frequency == 0):
|
|
141
|
-
self._progress.update(progress=progress)
|
|
142
|
-
with self._checkpoint_lock:
|
|
143
|
-
self._progress_checkpoint = progress
|
|
144
|
-
except Exception:
|
|
145
|
-
logger.warning('[Converter] Failed to update agent progress.')
|
|
146
|
-
|
|
147
|
-
def _get_labels(self):
|
|
148
|
-
self.labels = dict()
|
|
149
|
-
if self.dataset:
|
|
150
|
-
labels = list(self.dataset.instance_map.keys())
|
|
151
|
-
labels.sort()
|
|
152
|
-
self.labels = {label: i for i, label in enumerate(labels)}
|
|
153
|
-
|
|
154
|
-
def convert_dataset(
|
|
155
|
-
self,
|
|
156
|
-
dataset,
|
|
157
|
-
to_format: str,
|
|
158
|
-
local_path: str,
|
|
159
|
-
conversion_func=None,
|
|
160
|
-
filters=None,
|
|
161
|
-
annotation_filter=None
|
|
162
|
-
):
|
|
163
|
-
"""
|
|
164
|
-
Convert entire dataset.
|
|
165
|
-
|
|
166
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
167
|
-
|
|
168
|
-
:param dtlpy.entities.dataet.Dataset dataset: dataset entity
|
|
169
|
-
:param str to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
170
|
-
:param str local_path: path to save the result to
|
|
171
|
-
:param Callable conversion_func: Custom conversion service
|
|
172
|
-
:param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filter parameters
|
|
173
|
-
:param dtlpy.entities.filters.Filters annotation_filter: Filter entity
|
|
174
|
-
:return: the error log file path if there are errors and the coco json if the format is coco
|
|
175
|
-
"""
|
|
176
|
-
if to_format.lower() == AnnotationFormat.COCO:
|
|
177
|
-
coco_json, has_errors, log_filepath = self.__convert_dataset_to_coco(
|
|
178
|
-
dataset=dataset,
|
|
179
|
-
local_path=local_path,
|
|
180
|
-
filters=filters,
|
|
181
|
-
annotation_filter=annotation_filter
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
if self.return_error_filepath:
|
|
185
|
-
return coco_json, has_errors, log_filepath
|
|
186
|
-
else:
|
|
187
|
-
return coco_json
|
|
188
|
-
|
|
189
|
-
assert isinstance(dataset, entities.Dataset)
|
|
190
|
-
self.dataset = dataset
|
|
191
|
-
|
|
192
|
-
# download annotations
|
|
193
|
-
if annotation_filter is None:
|
|
194
|
-
logger.info('Downloading annotations...')
|
|
195
|
-
dataset.download_annotations(local_path=local_path, overwrite=True, filters=filters)
|
|
196
|
-
logger.info('Annotations downloaded')
|
|
197
|
-
local_annotations_path = os.path.join(local_path, "json")
|
|
198
|
-
output_annotations_path = os.path.join(local_path, to_format)
|
|
199
|
-
pool = ThreadPool(processes=self.concurrency)
|
|
200
|
-
i_item = 0
|
|
201
|
-
pages = dataset.items.list(filters=filters)
|
|
202
|
-
|
|
203
|
-
# if yolo - create labels file
|
|
204
|
-
if to_format == AnnotationFormat.YOLO:
|
|
205
|
-
self._get_labels()
|
|
206
|
-
with open('{}/{}.names'.format(local_path, dataset.name), 'w') as fp:
|
|
207
|
-
labels = list(self.labels.keys())
|
|
208
|
-
labels.sort()
|
|
209
|
-
for label in labels:
|
|
210
|
-
fp.write("{}\n".format(label))
|
|
211
|
-
|
|
212
|
-
pbar = tqdm.tqdm(total=pages.items_count, disable=dataset._client_api.verbose.disable_progress_bar_convert_annotations,
|
|
213
|
-
file=sys.stdout, desc='Convert Annotations')
|
|
214
|
-
reporter = Reporter(
|
|
215
|
-
num_workers=pages.items_count,
|
|
216
|
-
resource=Reporter.CONVERTER,
|
|
217
|
-
print_error_logs=self.dataset._client_api.verbose.print_error_logs,
|
|
218
|
-
client_api=self.dataset._client_api
|
|
219
|
-
)
|
|
220
|
-
for page in pages:
|
|
221
|
-
for item in page:
|
|
222
|
-
# create input annotations json
|
|
223
|
-
in_filepath = os.path.join(local_annotations_path, item.filename[1:])
|
|
224
|
-
name, ext = os.path.splitext(in_filepath)
|
|
225
|
-
in_filepath = name + '.json'
|
|
226
|
-
|
|
227
|
-
save_to = os.path.dirname(in_filepath.replace(local_annotations_path, output_annotations_path))
|
|
228
|
-
|
|
229
|
-
if not os.path.isdir(save_to):
|
|
230
|
-
os.makedirs(save_to, exist_ok=True)
|
|
231
|
-
|
|
232
|
-
pool.apply_async(
|
|
233
|
-
func=self.__save_filtered_annotations_and_convert,
|
|
234
|
-
kwds={
|
|
235
|
-
"to_format": to_format,
|
|
236
|
-
"from_format": AnnotationFormat.DATALOOP,
|
|
237
|
-
"file_path": in_filepath,
|
|
238
|
-
"save_locally": True,
|
|
239
|
-
"save_to": save_to,
|
|
240
|
-
'conversion_func': conversion_func,
|
|
241
|
-
'item': item,
|
|
242
|
-
'pbar': pbar,
|
|
243
|
-
'filters': annotation_filter,
|
|
244
|
-
'reporter': reporter,
|
|
245
|
-
'i_item': i_item
|
|
246
|
-
}
|
|
247
|
-
)
|
|
248
|
-
i_item += 1
|
|
249
|
-
pool.close()
|
|
250
|
-
pool.join()
|
|
251
|
-
pool.terminate()
|
|
252
|
-
|
|
253
|
-
log_filepath = None
|
|
254
|
-
|
|
255
|
-
if reporter.has_errors:
|
|
256
|
-
log_filepath = reporter.generate_log_files()
|
|
257
|
-
if log_filepath is not None:
|
|
258
|
-
logger.warning(
|
|
259
|
-
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
260
|
-
|
|
261
|
-
logger.info('Total converted: {}'.format(reporter.status_count('success')))
|
|
262
|
-
logger.info('Total skipped: {}'.format(reporter.status_count('skip')))
|
|
263
|
-
logger.info('Total failed: {}'.format(reporter.status_count('failed')))
|
|
264
|
-
|
|
265
|
-
if self.return_error_filepath:
|
|
266
|
-
return reporter.has_errors, log_filepath
|
|
267
|
-
|
|
268
|
-
def __save_filtered_annotations_and_convert(self, item: entities.Item, filters, to_format, from_format, file_path,
|
|
269
|
-
save_locally=False,
|
|
270
|
-
save_to=None, conversion_func=None,
|
|
271
|
-
pbar=None, **kwargs):
|
|
272
|
-
reporter = kwargs.get('reporter', None)
|
|
273
|
-
i_item = kwargs.get('i_item', None)
|
|
274
|
-
|
|
275
|
-
try:
|
|
276
|
-
if item.annotated and item.type != 'dir':
|
|
277
|
-
if filters is not None:
|
|
278
|
-
assert filters.resource == entities.FiltersResource.ANNOTATION
|
|
279
|
-
copy_filters = copy.deepcopy(filters)
|
|
280
|
-
copy_filters.add(field='itemId', values=item.id, method='and')
|
|
281
|
-
annotations_page = item.dataset.items.list(filters=copy_filters)
|
|
282
|
-
annotations = item.annotations.builder()
|
|
283
|
-
for page in annotations_page:
|
|
284
|
-
for annotation in page:
|
|
285
|
-
annotations.annotations.append(annotation)
|
|
286
|
-
|
|
287
|
-
if not os.path.isdir(os.path.dirname(file_path)):
|
|
288
|
-
os.makedirs(os.path.dirname(file_path))
|
|
289
|
-
with open(file_path, 'w') as f:
|
|
290
|
-
json.dump(annotations.to_json(), f, indent=2)
|
|
291
|
-
|
|
292
|
-
annotations_list, errors = self.convert_file(
|
|
293
|
-
item=item,
|
|
294
|
-
to_format=to_format,
|
|
295
|
-
from_format=from_format,
|
|
296
|
-
file_path=file_path,
|
|
297
|
-
save_locally=save_locally,
|
|
298
|
-
save_to=save_to,
|
|
299
|
-
conversion_func=conversion_func,
|
|
300
|
-
pbar=pbar
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
if errors:
|
|
304
|
-
raise Exception('Partial conversion: \n{}'.format(errors))
|
|
305
|
-
|
|
306
|
-
if reporter is not None and i_item is not None:
|
|
307
|
-
reporter.set_index(status='success', success=True, ref=item.id)
|
|
308
|
-
else:
|
|
309
|
-
if reporter is not None and i_item is not None:
|
|
310
|
-
reporter.set_index(ref=item.id, status='skip', success=True)
|
|
311
|
-
if reporter is not None:
|
|
312
|
-
self.__update_progress(total=reporter.num_workers, of_total=i_item)
|
|
313
|
-
except Exception:
|
|
314
|
-
if reporter is not None and i_item is not None:
|
|
315
|
-
reporter.set_index(status='failed', success=False, error=traceback.format_exc(),
|
|
316
|
-
ref=item.id)
|
|
317
|
-
|
|
318
|
-
@staticmethod
|
|
319
|
-
def __gen_coco_categories(instance_map, recipe):
|
|
320
|
-
categories = list()
|
|
321
|
-
last_id = 0
|
|
322
|
-
for label, label_id in instance_map.items():
|
|
323
|
-
label_name, sup = label.split('.')[-1], '.'.join(label.split('.')[0:-1])
|
|
324
|
-
category = {'id': label_id, 'name': label_name}
|
|
325
|
-
last_id = max(last_id, label_id)
|
|
326
|
-
if sup:
|
|
327
|
-
category['supercategory'] = sup
|
|
328
|
-
categories.append(category)
|
|
329
|
-
|
|
330
|
-
# add keypoint category
|
|
331
|
-
collection_templates = list()
|
|
332
|
-
if 'system' in recipe.metadata and 'collectionTemplates' in recipe.metadata['system']:
|
|
333
|
-
collection_templates = recipe.metadata['system']['collectionTemplates']
|
|
334
|
-
|
|
335
|
-
for template in collection_templates:
|
|
336
|
-
last_id += 1
|
|
337
|
-
order_dict = {key: i for i, key in enumerate(template['order'])}
|
|
338
|
-
skeleton = list()
|
|
339
|
-
for pair in template['arcs']:
|
|
340
|
-
skeleton.append([order_dict[pair[0]], order_dict[pair[1]]])
|
|
341
|
-
category = {'id': last_id,
|
|
342
|
-
'name': template['name'],
|
|
343
|
-
'templateId': template['id'],
|
|
344
|
-
'keypoints': template['order'],
|
|
345
|
-
'skeleton': skeleton}
|
|
346
|
-
instance_map[template['name']] = last_id
|
|
347
|
-
categories.append(category)
|
|
348
|
-
return categories
|
|
349
|
-
|
|
350
|
-
def __convert_dataset_to_coco(self, dataset: entities.Dataset, local_path, filters=None, annotation_filter=None):
|
|
351
|
-
pages = dataset.items.list(filters=filters)
|
|
352
|
-
logger.info('items count: {}'.format(pages.items_count))
|
|
353
|
-
dataset.download_annotations(local_path=local_path, filters=filters, annotation_filters=annotation_filter)
|
|
354
|
-
path_to_dataloop_annotations_dir = os.path.join(local_path, 'json')
|
|
355
|
-
label_to_id = dataset.instance_map
|
|
356
|
-
recipe = dataset._get_recipe()
|
|
357
|
-
categories = self.__gen_coco_categories(instance_map=label_to_id, recipe=recipe)
|
|
358
|
-
images = [None for _ in range(pages.items_count)]
|
|
359
|
-
converted_annotations = [None for _ in range(pages.items_count)]
|
|
360
|
-
item_id_counter = 0
|
|
361
|
-
pool = ThreadPool(processes=self.concurrency)
|
|
362
|
-
pbar = tqdm.tqdm(total=pages.items_count, disable=dataset._client_api.verbose.disable_progress_bar_convert_annotations,
|
|
363
|
-
file=sys.stdout, desc='Convert Annotations')
|
|
364
|
-
reporter = Reporter(
|
|
365
|
-
num_workers=pages.items_count,
|
|
366
|
-
resource=Reporter.CONVERTER,
|
|
367
|
-
print_error_logs=dataset._client_api.verbose.print_error_logs,
|
|
368
|
-
client_api=dataset._client_api
|
|
369
|
-
)
|
|
370
|
-
for page in pages:
|
|
371
|
-
for item in page:
|
|
372
|
-
try:
|
|
373
|
-
pool.apply_async(func=self.__single_item_to_coco,
|
|
374
|
-
kwds={
|
|
375
|
-
'item': item,
|
|
376
|
-
'images': images,
|
|
377
|
-
'path_to_dataloop_annotations_dir': path_to_dataloop_annotations_dir,
|
|
378
|
-
'item_id': item_id_counter,
|
|
379
|
-
'reporter': reporter,
|
|
380
|
-
'converted_annotations': converted_annotations,
|
|
381
|
-
'annotation_filter': annotation_filter,
|
|
382
|
-
'label_to_id': label_to_id,
|
|
383
|
-
'categories': categories,
|
|
384
|
-
'pbar': pbar
|
|
385
|
-
})
|
|
386
|
-
item_id_counter += 1
|
|
387
|
-
except Exception as e:
|
|
388
|
-
logger.error('Failed to convert item: {}'.format(item.id))
|
|
389
|
-
|
|
390
|
-
pool.close()
|
|
391
|
-
pool.join()
|
|
392
|
-
pool.terminate()
|
|
393
|
-
|
|
394
|
-
total_converted_annotations = list()
|
|
395
|
-
for ls in converted_annotations:
|
|
396
|
-
if ls is not None:
|
|
397
|
-
total_converted_annotations += ls
|
|
398
|
-
|
|
399
|
-
for i_ann, ann in enumerate(total_converted_annotations):
|
|
400
|
-
ann['id'] = i_ann
|
|
401
|
-
|
|
402
|
-
info = {
|
|
403
|
-
'description': dataset.name
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
coco_json = {'images': [image for image in images if image is not None],
|
|
407
|
-
'info': info,
|
|
408
|
-
'annotations': total_converted_annotations,
|
|
409
|
-
'categories': categories}
|
|
410
|
-
|
|
411
|
-
with open(os.path.join(local_path, 'coco.json'), 'w+', encoding='utf-8') as f:
|
|
412
|
-
json.dump(coco_json, f, ensure_ascii=False)
|
|
413
|
-
|
|
414
|
-
log_filepath = None
|
|
415
|
-
if reporter.has_errors:
|
|
416
|
-
log_filepath = reporter.generate_log_files()
|
|
417
|
-
if log_filepath is not None:
|
|
418
|
-
logger.warning(
|
|
419
|
-
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
420
|
-
|
|
421
|
-
logger.info('Total converted: {}'.format(reporter.success_count))
|
|
422
|
-
logger.info('Total failed: {}'.format(reporter.failure_count))
|
|
423
|
-
logger.info('Total skipped: {}'.format(reporter.status_count('skip')))
|
|
424
|
-
if reporter.success_count + reporter.status_count('skip') + reporter.failure_count != pages.items_count:
|
|
425
|
-
raise ValueError('Not all items were processed')
|
|
426
|
-
|
|
427
|
-
return coco_json, reporter.has_errors, log_filepath
|
|
428
|
-
|
|
429
|
-
@staticmethod
|
|
430
|
-
def __get_item_shape(item: entities.Item = None, local_path: str = None):
|
|
431
|
-
if isinstance(item, entities.Item) and (item.width is None or item.height is None):
|
|
432
|
-
try:
|
|
433
|
-
img = Image.open(item.download(save_locally=False)) if local_path is None else Image.open(local_path)
|
|
434
|
-
item.height = img.height
|
|
435
|
-
item.width = img.width
|
|
436
|
-
except Exception:
|
|
437
|
-
pass
|
|
438
|
-
return item
|
|
439
|
-
|
|
440
|
-
def __add_item_converted_annotation(self, item, annotation, label_to_id, item_id,
|
|
441
|
-
i_annotation, item_converted_annotations):
|
|
442
|
-
try:
|
|
443
|
-
ann = self.to_coco(annotation=annotation, item=item)
|
|
444
|
-
ann['category_id'] = label_to_id[annotation.label]
|
|
445
|
-
ann['image_id'] = item_id
|
|
446
|
-
ann['id'] = int('{}{}'.format(item_id, i_annotation))
|
|
447
|
-
|
|
448
|
-
item_converted_annotations.append(ann)
|
|
449
|
-
except Exception:
|
|
450
|
-
err = 'Error converting annotation: \n' \
|
|
451
|
-
'Item: {}, annotation: {} - ' \
|
|
452
|
-
'fail to convert some of the annotation\n{}'.format(item_id,
|
|
453
|
-
annotation.id,
|
|
454
|
-
traceback.format_exc())
|
|
455
|
-
item_converted_annotations.append(err)
|
|
456
|
-
|
|
457
|
-
def __coco_handle_pose_annotations(self, item, item_id, pose_annotations, point_annotations,
|
|
458
|
-
categories, label_to_id, item_converted_annotations):
|
|
459
|
-
# link points to pose and convert it
|
|
460
|
-
for pose in pose_annotations:
|
|
461
|
-
pose_idx = pose[1]
|
|
462
|
-
pose = pose[0]
|
|
463
|
-
pose_category = None
|
|
464
|
-
for category in categories:
|
|
465
|
-
if pose.coordinates.get('templateId', "") == category.get('templateId', None):
|
|
466
|
-
pose_category = category
|
|
467
|
-
continue
|
|
468
|
-
if pose_category is None:
|
|
469
|
-
err = 'Error converting annotation: \n' \
|
|
470
|
-
'Item: {}, annotation: {} - ' \
|
|
471
|
-
'Pose annotation without known template\n{}'.format(item_id,
|
|
472
|
-
pose.id,
|
|
473
|
-
traceback.format_exc())
|
|
474
|
-
item_converted_annotations.append(err)
|
|
475
|
-
continue
|
|
476
|
-
if pose.id not in point_annotations or (pose.id in point_annotations and
|
|
477
|
-
len(point_annotations[pose.id]) != len(pose_category['keypoints'])):
|
|
478
|
-
err = 'Error converting annotation: \n' \
|
|
479
|
-
'Item: {}, annotation: {} - ' \
|
|
480
|
-
'Pose annotation has {} children ' \
|
|
481
|
-
'while it template has {} points\n{}'.format(item_id,
|
|
482
|
-
pose.id,
|
|
483
|
-
len(point_annotations[pose.id]),
|
|
484
|
-
len(pose_category['keypoints']),
|
|
485
|
-
traceback.format_exc())
|
|
486
|
-
item_converted_annotations.append(err)
|
|
487
|
-
continue
|
|
488
|
-
# verify points labels are unique
|
|
489
|
-
if len(point_annotations[pose.id]) != len(set([ann.label for ann in point_annotations[pose.id]])):
|
|
490
|
-
err = 'Error converting annotation: \n' \
|
|
491
|
-
'Item: {}, annotation: {} - Pose annotation ' \
|
|
492
|
-
'does not have unique children points\n{}'.format(item_id,
|
|
493
|
-
pose.id,
|
|
494
|
-
traceback.format_exc())
|
|
495
|
-
item_converted_annotations.append(err)
|
|
496
|
-
continue
|
|
497
|
-
|
|
498
|
-
ordered_points = list()
|
|
499
|
-
for pose_point in pose_category['keypoints']:
|
|
500
|
-
for point_annotation in point_annotations[pose.id]:
|
|
501
|
-
if point_annotation.label == pose_point:
|
|
502
|
-
ordered_points.append(point_annotation)
|
|
503
|
-
break
|
|
504
|
-
pose.annotation_definition.points = ordered_points
|
|
505
|
-
|
|
506
|
-
self.__add_item_converted_annotation(item=item, annotation=pose,
|
|
507
|
-
label_to_id=label_to_id,
|
|
508
|
-
item_id=item_id, i_annotation=pose_idx,
|
|
509
|
-
item_converted_annotations=item_converted_annotations)
|
|
510
|
-
|
|
511
|
-
def __single_item_to_coco(self, item: entities.Item,
|
|
512
|
-
images,
|
|
513
|
-
path_to_dataloop_annotations_dir,
|
|
514
|
-
item_id,
|
|
515
|
-
converted_annotations,
|
|
516
|
-
annotation_filter,
|
|
517
|
-
label_to_id,
|
|
518
|
-
reporter,
|
|
519
|
-
categories,
|
|
520
|
-
pbar=None):
|
|
521
|
-
try:
|
|
522
|
-
if item.type != 'dir':
|
|
523
|
-
item = Converter.__get_item_shape(item=item)
|
|
524
|
-
filepath = item.filename[1:] if item.filename.startswith('/') else item.filename
|
|
525
|
-
images[item_id] = {'file_name': os.path.normpath(filepath),
|
|
526
|
-
'id': item_id,
|
|
527
|
-
'width': item.width,
|
|
528
|
-
'height': item.height
|
|
529
|
-
}
|
|
530
|
-
if annotation_filter is None:
|
|
531
|
-
try:
|
|
532
|
-
filename, ext = os.path.splitext(item.filename)
|
|
533
|
-
filename = '{}.json'.format(filename[1:])
|
|
534
|
-
with open(os.path.join(path_to_dataloop_annotations_dir, filename), 'r', encoding="utf8") as f:
|
|
535
|
-
annotations = json.load(f)['annotations']
|
|
536
|
-
annotations = entities.AnnotationCollection.from_json(annotations)
|
|
537
|
-
except Exception:
|
|
538
|
-
annotations = item.annotations.list()
|
|
539
|
-
else:
|
|
540
|
-
copy_filters = copy.deepcopy(annotation_filter)
|
|
541
|
-
copy_filters.add(field='itemId', values=item.id, method='and')
|
|
542
|
-
annotations_page = item.dataset.items.list(filters=copy_filters)
|
|
543
|
-
annotations = item.annotations.builder()
|
|
544
|
-
for page in annotations_page:
|
|
545
|
-
for annotation in page:
|
|
546
|
-
annotations.annotations.append(annotation)
|
|
547
|
-
|
|
548
|
-
item_converted_annotations = list()
|
|
549
|
-
point_annotations = dict()
|
|
550
|
-
pose_annotations = list()
|
|
551
|
-
|
|
552
|
-
for i_annotation, annotation in enumerate(annotations.annotations):
|
|
553
|
-
if annotation.type == 'point' and annotation.parent_id is not None:
|
|
554
|
-
if annotation.parent_id not in point_annotations:
|
|
555
|
-
point_annotations[annotation.parent_id] = list()
|
|
556
|
-
point_annotations[annotation.parent_id].append(annotation)
|
|
557
|
-
continue
|
|
558
|
-
if annotation.type == 'pose':
|
|
559
|
-
pose_annotations.append([annotation, i_annotation])
|
|
560
|
-
continue
|
|
561
|
-
self.__add_item_converted_annotation(item=item, annotation=annotation,
|
|
562
|
-
label_to_id=label_to_id,
|
|
563
|
-
item_id=item_id, i_annotation=i_annotation,
|
|
564
|
-
item_converted_annotations=item_converted_annotations)
|
|
565
|
-
|
|
566
|
-
self.__coco_handle_pose_annotations(item=item, item_id=item_id,
|
|
567
|
-
pose_annotations=pose_annotations,
|
|
568
|
-
point_annotations=point_annotations,
|
|
569
|
-
categories=categories,
|
|
570
|
-
label_to_id=label_to_id,
|
|
571
|
-
item_converted_annotations=item_converted_annotations)
|
|
572
|
-
|
|
573
|
-
success, errors = self._sort_annotations(annotations=item_converted_annotations)
|
|
574
|
-
converted_annotations[item_id] = success
|
|
575
|
-
if errors:
|
|
576
|
-
reporter.set_index(ref=item.id, status='failed', success=False,
|
|
577
|
-
error=errors)
|
|
578
|
-
else:
|
|
579
|
-
reporter.set_index(ref=item.id, status='success', success=True)
|
|
580
|
-
else:
|
|
581
|
-
reporter.set_index(ref=item.id, status='skipped', success=True)
|
|
582
|
-
except Exception:
|
|
583
|
-
reporter.set_index(ref=item.id, status='failed', success=False,
|
|
584
|
-
error=traceback.format_exc())
|
|
585
|
-
logger.debug('Error converting item: {}\n{}'.format(item.id, traceback.format_exc()))
|
|
586
|
-
raise Exception('Error converting item: {}\n{}'.format(item.id, traceback.format_exc()))
|
|
587
|
-
if pbar is not None:
|
|
588
|
-
pbar.update()
|
|
589
|
-
|
|
590
|
-
if reporter is not None:
|
|
591
|
-
self.__update_progress(total=reporter.num_workers, of_total=item_id)
|
|
592
|
-
|
|
593
|
-
def _upload_coco_labels(self, coco_json):
|
|
594
|
-
labels = coco_json.get('categories', None)
|
|
595
|
-
upload_labels = dict()
|
|
596
|
-
for label in labels:
|
|
597
|
-
if 'supercategory' in label and label['supercategory'] is not None:
|
|
598
|
-
if label['supercategory'] not in upload_labels:
|
|
599
|
-
upload_labels[label['supercategory']] = entities.Label(tag=label['supercategory'])
|
|
600
|
-
upload_labels[label['supercategory']].children.append(entities.Label(tag=label['name']))
|
|
601
|
-
tag = '{}.{}'.format(label['supercategory'], label['name'])
|
|
602
|
-
else:
|
|
603
|
-
tag = label['name']
|
|
604
|
-
upload_labels[label['name']] = entities.Label(tag=tag)
|
|
605
|
-
self.labels[tag] = label['id']
|
|
606
|
-
|
|
607
|
-
return upload_labels
|
|
608
|
-
|
|
609
|
-
def _upload_coco_dataset(self, local_items_path, local_annotations_path, only_bbox=False, remote_items=False):
|
|
610
|
-
logger.info('loading annotations json...')
|
|
611
|
-
with open(local_annotations_path, 'r', encoding="utf8") as f:
|
|
612
|
-
coco_json = json.load(f)
|
|
613
|
-
|
|
614
|
-
labels_tags_tree = self._upload_coco_labels(coco_json=coco_json)
|
|
615
|
-
try:
|
|
616
|
-
logger.info('Uploading labels to dataset')
|
|
617
|
-
self.dataset.add_labels(list(labels_tags_tree.values()))
|
|
618
|
-
except Exception:
|
|
619
|
-
logger.warning('Failed to upload labels to dataset, please add manually')
|
|
620
|
-
|
|
621
|
-
image_annotations = dict()
|
|
622
|
-
image_name_id = dict()
|
|
623
|
-
for image in coco_json['images']:
|
|
624
|
-
image_metadata = image
|
|
625
|
-
image_annotations[image['file_name']] = {
|
|
626
|
-
'id': image['id'],
|
|
627
|
-
'metadata': image_metadata,
|
|
628
|
-
'annotations': list()
|
|
629
|
-
}
|
|
630
|
-
image_name_id[image['id']] = image['file_name']
|
|
631
|
-
for ann in coco_json['annotations']:
|
|
632
|
-
image_annotations[image_name_id[ann['image_id']]]['annotations'].append(ann)
|
|
633
|
-
|
|
634
|
-
if remote_items:
|
|
635
|
-
return self._upload_annotations(local_annotations_path=image_annotations,
|
|
636
|
-
from_format=AnnotationFormat.COCO,
|
|
637
|
-
only_bbox=only_bbox)
|
|
638
|
-
else:
|
|
639
|
-
return self._upload_directory(local_items_path=local_items_path,
|
|
640
|
-
local_annotations_path=image_annotations,
|
|
641
|
-
from_format=AnnotationFormat.COCO,
|
|
642
|
-
only_bbox=only_bbox)
|
|
643
|
-
|
|
644
|
-
def _read_labels(self, labels_file_path):
|
|
645
|
-
if labels_file_path:
|
|
646
|
-
with open(labels_file_path, 'r') as fp:
|
|
647
|
-
labels = [line.strip() for line in fp.readlines()]
|
|
648
|
-
self.dataset.add_labels(label_list=labels)
|
|
649
|
-
self.labels = {label: i_label for i_label, label in enumerate(labels)}
|
|
650
|
-
else:
|
|
651
|
-
logger.warning('No labels file path provided (.names), skipping labels upload')
|
|
652
|
-
self._get_labels()
|
|
653
|
-
|
|
654
|
-
def _upload_yolo_dataset(self, local_items_path, local_annotations_path, labels_file_path, remote_items=False):
|
|
655
|
-
self._read_labels(labels_file_path=labels_file_path)
|
|
656
|
-
if remote_items:
|
|
657
|
-
return self._upload_annotations(local_annotations_path=local_annotations_path,
|
|
658
|
-
from_format=AnnotationFormat.YOLO)
|
|
659
|
-
else:
|
|
660
|
-
return self._upload_directory(local_items_path=local_items_path,
|
|
661
|
-
local_annotations_path=local_annotations_path,
|
|
662
|
-
from_format=AnnotationFormat.YOLO)
|
|
663
|
-
|
|
664
|
-
def _upload_voc_dataset(self, local_items_path, local_annotations_path, remote_items=False, **_):
|
|
665
|
-
# TODO - implement VOC annotations upload
|
|
666
|
-
logger.warning('labels upload from VOC dataset is not implemented, please upload labels manually')
|
|
667
|
-
|
|
668
|
-
if remote_items:
|
|
669
|
-
return self._upload_annotations(local_annotations_path=local_annotations_path,
|
|
670
|
-
from_format=AnnotationFormat.VOC)
|
|
671
|
-
else:
|
|
672
|
-
return self._upload_directory(local_items_path=local_items_path,
|
|
673
|
-
local_annotations_path=local_annotations_path,
|
|
674
|
-
from_format=AnnotationFormat.VOC)
|
|
675
|
-
|
|
676
|
-
@staticmethod
|
|
677
|
-
def _find_yolo_voc_item_annotations(local_annotations_path: str, item: entities.Item, from_format: str):
|
|
678
|
-
found = False
|
|
679
|
-
metadata = None
|
|
680
|
-
|
|
681
|
-
extension = '.txt' if from_format == AnnotationFormat.YOLO else '.xml'
|
|
682
|
-
filename, _ = os.path.splitext(item.filename)
|
|
683
|
-
annotations_filepath = os.path.join(local_annotations_path, filename[1:] + extension)
|
|
684
|
-
if os.path.isfile(annotations_filepath):
|
|
685
|
-
found = True
|
|
686
|
-
|
|
687
|
-
return found, annotations_filepath, metadata
|
|
688
|
-
|
|
689
|
-
@staticmethod
|
|
690
|
-
def _find_coco_item_annotations(local_annotations_path: dict, item: entities.Item):
|
|
691
|
-
found = False
|
|
692
|
-
ann_dict = None
|
|
693
|
-
filename = item.filename[1:] if item.filename.startswith('/') else item.filename
|
|
694
|
-
filename = os.path.normpath(filename)
|
|
695
|
-
if filename in local_annotations_path:
|
|
696
|
-
ann_dict = local_annotations_path[filename]
|
|
697
|
-
found = True
|
|
698
|
-
elif item.name in local_annotations_path:
|
|
699
|
-
ann_dict = local_annotations_path[item.name]
|
|
700
|
-
found = True
|
|
701
|
-
metadata = ann_dict.get('metadata', None) if found else None
|
|
702
|
-
return found, ann_dict, metadata
|
|
703
|
-
|
|
704
|
-
def _upload_annotations(self, local_annotations_path, from_format, **kwargs):
|
|
705
|
-
self._only_bbox = kwargs.get('only_bbox', False)
|
|
706
|
-
file_count = self.remote_items.items_count
|
|
707
|
-
reporter = Reporter(
|
|
708
|
-
num_workers=file_count,
|
|
709
|
-
resource=Reporter.CONVERTER,
|
|
710
|
-
print_error_logs=self.dataset._client_api.verbose.print_error_logs,
|
|
711
|
-
client_api=self.dataset._client_api
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
pbar = tqdm.tqdm(total=file_count, desc='Upload Annotations')
|
|
715
|
-
pool = ThreadPool(processes=self.concurrency)
|
|
716
|
-
i_item = 0
|
|
717
|
-
|
|
718
|
-
for page in self.remote_items:
|
|
719
|
-
for item in page:
|
|
720
|
-
if from_format == AnnotationFormat.COCO:
|
|
721
|
-
found, ann_filepath, metadata = self._find_coco_item_annotations(
|
|
722
|
-
local_annotations_path=local_annotations_path,
|
|
723
|
-
item=item
|
|
724
|
-
)
|
|
725
|
-
elif from_format in [AnnotationFormat.YOLO, AnnotationFormat.VOC]:
|
|
726
|
-
found, ann_filepath, metadata = self._find_yolo_voc_item_annotations(
|
|
727
|
-
local_annotations_path=local_annotations_path,
|
|
728
|
-
item=item,
|
|
729
|
-
from_format=from_format
|
|
730
|
-
)
|
|
731
|
-
else:
|
|
732
|
-
raise exceptions.PlatformException('400', 'Unknown annotation format: {}'.format(from_format))
|
|
733
|
-
if not found:
|
|
734
|
-
pbar.update()
|
|
735
|
-
reporter.set_index(ref=item.filename, status='skip', success=False,
|
|
736
|
-
error='Cannot find annotations for item')
|
|
737
|
-
i_item += 1
|
|
738
|
-
continue
|
|
739
|
-
pool.apply_async(
|
|
740
|
-
func=self._upload_item_and_convert,
|
|
741
|
-
kwds={
|
|
742
|
-
"from_format": from_format,
|
|
743
|
-
"item_path": item,
|
|
744
|
-
"ann_path": ann_filepath,
|
|
745
|
-
'conversion_func': None,
|
|
746
|
-
"reporter": reporter,
|
|
747
|
-
'i_item': i_item,
|
|
748
|
-
'pbar': pbar,
|
|
749
|
-
'metadata': metadata
|
|
750
|
-
}
|
|
751
|
-
)
|
|
752
|
-
i_item += 1
|
|
753
|
-
pool.close()
|
|
754
|
-
pool.join()
|
|
755
|
-
pool.terminate()
|
|
756
|
-
|
|
757
|
-
log_filepath = None
|
|
758
|
-
if reporter.has_errors:
|
|
759
|
-
log_filepath = reporter.generate_log_files()
|
|
760
|
-
if log_filepath is not None:
|
|
761
|
-
logger.warning(
|
|
762
|
-
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
763
|
-
|
|
764
|
-
logger.info('Total converted and uploaded: {}'.format(reporter.status_count('success')))
|
|
765
|
-
logger.info('Total failed: {}'.format(reporter.status_count('failed')))
|
|
766
|
-
|
|
767
|
-
if self.return_error_filepath:
|
|
768
|
-
return reporter.has_errors, log_filepath
|
|
769
|
-
|
|
770
|
-
def _upload_directory(self, local_items_path, local_annotations_path, from_format, conversion_func=None, **kwargs):
|
|
771
|
-
"""
|
|
772
|
-
Convert annotation files in entire directory.
|
|
773
|
-
|
|
774
|
-
:param local_items_path:
|
|
775
|
-
:param local_annotations_path:
|
|
776
|
-
:param from_format:
|
|
777
|
-
:param conversion_func:
|
|
778
|
-
:return:
|
|
779
|
-
"""
|
|
780
|
-
self._only_bbox = kwargs.get('only_bbox', False)
|
|
781
|
-
file_count = sum(len([file for file in files if not file.endswith('.xml')]) for _, _, files in
|
|
782
|
-
os.walk(local_items_path))
|
|
783
|
-
|
|
784
|
-
reporter = Reporter(
|
|
785
|
-
num_workers=file_count,
|
|
786
|
-
resource=Reporter.CONVERTER,
|
|
787
|
-
print_error_logs=self.dataset._client_api.verbose.print_error_logs,
|
|
788
|
-
client_api=self.dataset._client_api
|
|
789
|
-
)
|
|
790
|
-
|
|
791
|
-
pbar = tqdm.tqdm(total=file_count, desc='Upload Annotations')
|
|
792
|
-
|
|
793
|
-
pool = ThreadPool(processes=self.concurrency)
|
|
794
|
-
i_item = 0
|
|
795
|
-
metadata = None
|
|
796
|
-
for path, subdirs, files in os.walk(local_items_path):
|
|
797
|
-
for name in files:
|
|
798
|
-
ann_filepath = None
|
|
799
|
-
if not os.path.isfile(os.path.join(path, name)):
|
|
800
|
-
continue
|
|
801
|
-
item_filepath = os.path.join(path, name)
|
|
802
|
-
prefix = os.path.relpath(path, local_items_path)
|
|
803
|
-
coco_name = name
|
|
804
|
-
if prefix != '.':
|
|
805
|
-
coco_name = os.path.join(prefix, name)
|
|
806
|
-
else:
|
|
807
|
-
prefix = None
|
|
808
|
-
if from_format == AnnotationFormat.COCO:
|
|
809
|
-
ann_filepath = local_annotations_path[coco_name]
|
|
810
|
-
metadata = {'user': local_annotations_path[coco_name]['metadata']}
|
|
811
|
-
elif from_format == AnnotationFormat.VOC:
|
|
812
|
-
if name.endswith('.xml'):
|
|
813
|
-
continue
|
|
814
|
-
else:
|
|
815
|
-
ext = os.path.splitext(name)[-1]
|
|
816
|
-
try:
|
|
817
|
-
m = mimetypes.types_map[ext.lower()]
|
|
818
|
-
except Exception:
|
|
819
|
-
m = ''
|
|
820
|
-
|
|
821
|
-
if ext == '' or ext is None or 'image' not in m:
|
|
822
|
-
continue
|
|
823
|
-
|
|
824
|
-
ann_filepath = os.path.join(path, '.'.join(name.split('.')[0:-1] + ['xml'])).replace(
|
|
825
|
-
local_items_path, local_annotations_path)
|
|
826
|
-
elif from_format == AnnotationFormat.YOLO:
|
|
827
|
-
ann_filepath = os.path.join(path, os.path.splitext(name)[0]) + '.txt'
|
|
828
|
-
ann_filepath = ann_filepath.replace(local_items_path, local_annotations_path)
|
|
829
|
-
pool.apply_async(
|
|
830
|
-
func=self._upload_item_and_convert,
|
|
831
|
-
kwds={
|
|
832
|
-
"from_format": from_format,
|
|
833
|
-
"item_path": item_filepath,
|
|
834
|
-
"ann_path": ann_filepath,
|
|
835
|
-
'conversion_func': conversion_func,
|
|
836
|
-
"reporter": reporter,
|
|
837
|
-
'i_item': i_item,
|
|
838
|
-
'pbar': pbar,
|
|
839
|
-
'metadata': metadata,
|
|
840
|
-
'remote_path': prefix
|
|
841
|
-
}
|
|
842
|
-
)
|
|
843
|
-
i_item += 1
|
|
844
|
-
pool.close()
|
|
845
|
-
pool.join()
|
|
846
|
-
pool.terminate()
|
|
847
|
-
|
|
848
|
-
log_filepath = None
|
|
849
|
-
if reporter.has_errors:
|
|
850
|
-
log_filepath = reporter.generate_log_files()
|
|
851
|
-
if log_filepath is not None:
|
|
852
|
-
logger.warning(
|
|
853
|
-
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
854
|
-
|
|
855
|
-
logger.info('Total converted and uploaded: {}'.format(reporter.status_count('success')))
|
|
856
|
-
logger.info('Total failed: {}'.format(reporter.status_count('failed')))
|
|
857
|
-
|
|
858
|
-
if self.return_error_filepath:
|
|
859
|
-
return reporter.has_errors, log_filepath
|
|
860
|
-
|
|
861
|
-
def _upload_item_and_convert(self, item_path, ann_path, from_format, conversion_func=None, **kwargs):
|
|
862
|
-
reporter = kwargs.get('reporter', None)
|
|
863
|
-
i_item = kwargs.get('i_item', None)
|
|
864
|
-
pbar = kwargs.get('pbar', None)
|
|
865
|
-
metadata = kwargs.get('metadata', None)
|
|
866
|
-
remote_path = kwargs.get('remote_path', None)
|
|
867
|
-
report_ref = item_path
|
|
868
|
-
try:
|
|
869
|
-
if isinstance(item_path, entities.Item):
|
|
870
|
-
item = item_path
|
|
871
|
-
report_ref = item.filename
|
|
872
|
-
else:
|
|
873
|
-
item = self.dataset.items.upload(local_path=item_path, item_metadata=metadata, remote_path=remote_path)
|
|
874
|
-
report_ref = item.filename
|
|
875
|
-
if from_format == AnnotationFormat.YOLO:
|
|
876
|
-
item = Converter.__get_item_shape(item=item, local_path=item_path)
|
|
877
|
-
annotations_list, errors = self.convert_file(to_format=AnnotationFormat.DATALOOP,
|
|
878
|
-
from_format=from_format,
|
|
879
|
-
item=item,
|
|
880
|
-
file_path=ann_path,
|
|
881
|
-
save_locally=False,
|
|
882
|
-
conversion_func=conversion_func,
|
|
883
|
-
upload=True,
|
|
884
|
-
pbar=pbar)
|
|
885
|
-
if errors:
|
|
886
|
-
if reporter is not None and i_item is not None:
|
|
887
|
-
reporter.set_index(ref=report_ref,
|
|
888
|
-
status='warning',
|
|
889
|
-
success=False,
|
|
890
|
-
error='partial annotations upload: \n{}'.format(errors))
|
|
891
|
-
else:
|
|
892
|
-
if reporter is not None and i_item is not None:
|
|
893
|
-
reporter.set_index(status='success',
|
|
894
|
-
success=True,
|
|
895
|
-
ref=report_ref)
|
|
896
|
-
if reporter is not None:
|
|
897
|
-
self.__update_progress(total=reporter.num_workers, of_total=i_item)
|
|
898
|
-
except Exception:
|
|
899
|
-
if reporter is not None and i_item is not None:
|
|
900
|
-
reporter.set_index(status='failed',
|
|
901
|
-
success=False,
|
|
902
|
-
error=traceback.format_exc(),
|
|
903
|
-
ref=report_ref)
|
|
904
|
-
|
|
905
|
-
def upload_local_dataset(self,
|
|
906
|
-
from_format: AnnotationFormat,
|
|
907
|
-
dataset,
|
|
908
|
-
local_items_path: str = None,
|
|
909
|
-
local_labels_path: str = None,
|
|
910
|
-
local_annotations_path: str = None,
|
|
911
|
-
only_bbox: bool = False,
|
|
912
|
-
filters=None,
|
|
913
|
-
remote_items=None
|
|
914
|
-
):
|
|
915
|
-
"""
|
|
916
|
-
Convert and upload local dataset to dataloop platform.
|
|
917
|
-
|
|
918
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
919
|
-
|
|
920
|
-
:param str from_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
921
|
-
:param dtlpy.entities.dataset.Dataset dataset: dataset entity
|
|
922
|
-
:param str local_items_path: path to items to upload
|
|
923
|
-
:param str local_annotations_path: path to annotations to upload
|
|
924
|
-
:param str local_labels_path: path to labels to upload
|
|
925
|
-
:param bool only_bbox: only for coco datasets, if True upload only bbox
|
|
926
|
-
:param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filter parameters
|
|
927
|
-
:param list remote_items: list of the items to upload
|
|
928
|
-
:return: the error log file path if there are errors
|
|
929
|
-
"""
|
|
930
|
-
|
|
931
|
-
if remote_items is None:
|
|
932
|
-
remote_items = local_items_path is None
|
|
933
|
-
|
|
934
|
-
if remote_items:
|
|
935
|
-
logger.info('Getting remote items...')
|
|
936
|
-
self.remote_items = dataset.items.list(filters=filters)
|
|
937
|
-
|
|
938
|
-
self.dataset = dataset
|
|
939
|
-
if from_format.lower() == AnnotationFormat.COCO:
|
|
940
|
-
return self._upload_coco_dataset(
|
|
941
|
-
local_items_path=local_items_path,
|
|
942
|
-
local_annotations_path=local_annotations_path,
|
|
943
|
-
only_bbox=only_bbox,
|
|
944
|
-
remote_items=remote_items
|
|
945
|
-
)
|
|
946
|
-
if from_format.lower() == AnnotationFormat.YOLO:
|
|
947
|
-
return self._upload_yolo_dataset(
|
|
948
|
-
local_items_path=local_items_path,
|
|
949
|
-
local_annotations_path=local_annotations_path,
|
|
950
|
-
labels_file_path=local_labels_path,
|
|
951
|
-
remote_items=remote_items
|
|
952
|
-
)
|
|
953
|
-
if from_format.lower() == AnnotationFormat.VOC:
|
|
954
|
-
return self._upload_voc_dataset(
|
|
955
|
-
local_items_path=local_items_path,
|
|
956
|
-
local_annotations_path=local_annotations_path,
|
|
957
|
-
labels_file_path=local_labels_path,
|
|
958
|
-
remote_items=remote_items
|
|
959
|
-
)
|
|
960
|
-
|
|
961
|
-
def convert_directory(self,
|
|
962
|
-
local_path: str,
|
|
963
|
-
to_format: AnnotationFormat,
|
|
964
|
-
from_format: AnnotationFormat,
|
|
965
|
-
dataset,
|
|
966
|
-
conversion_func=None):
|
|
967
|
-
"""
|
|
968
|
-
Convert annotation files in entire directory.
|
|
969
|
-
|
|
970
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
971
|
-
|
|
972
|
-
:param str local_path: path to the directory
|
|
973
|
-
:param str to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
974
|
-
:param str from_format: AnnotationFormat to convert from – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
975
|
-
:param dtlpy.entities.dataset.Dataset dataset: dataset entity
|
|
976
|
-
:param Callable conversion_func: Custom conversion service
|
|
977
|
-
:return: the error log file path if there are errors
|
|
978
|
-
"""
|
|
979
|
-
file_count = sum(len(files) for _, _, files in os.walk(local_path))
|
|
980
|
-
self.dataset = dataset
|
|
981
|
-
reporter = Reporter(
|
|
982
|
-
num_workers=file_count,
|
|
983
|
-
resource=Reporter.CONVERTER,
|
|
984
|
-
print_error_logs=self.dataset._client_api.verbose.print_error_logs,
|
|
985
|
-
client_api=self.dataset._client_api
|
|
986
|
-
)
|
|
987
|
-
|
|
988
|
-
pool = ThreadPool(processes=self.concurrency)
|
|
989
|
-
i_item = 0
|
|
990
|
-
for path, subdirs, files in os.walk(local_path):
|
|
991
|
-
for name in files:
|
|
992
|
-
save_to = os.path.join(os.path.split(local_path)[0], to_format)
|
|
993
|
-
save_to = path.replace(local_path, save_to)
|
|
994
|
-
os.makedirs(save_to, exist_ok=True)
|
|
995
|
-
file_path = os.path.join(path, name)
|
|
996
|
-
pool.apply_async(
|
|
997
|
-
func=self._convert_and_report,
|
|
998
|
-
kwds={
|
|
999
|
-
"to_format": to_format,
|
|
1000
|
-
"from_format": from_format,
|
|
1001
|
-
"file_path": file_path,
|
|
1002
|
-
"save_locally": True,
|
|
1003
|
-
"save_to": save_to,
|
|
1004
|
-
'conversion_func': conversion_func,
|
|
1005
|
-
'reporter': reporter,
|
|
1006
|
-
'i_item': i_item
|
|
1007
|
-
}
|
|
1008
|
-
)
|
|
1009
|
-
i_item += 1
|
|
1010
|
-
pool.close()
|
|
1011
|
-
pool.join()
|
|
1012
|
-
pool.terminate()
|
|
1013
|
-
|
|
1014
|
-
log_filepath = None
|
|
1015
|
-
if reporter.has_errors:
|
|
1016
|
-
log_filepath = reporter.generate_log_files()
|
|
1017
|
-
if log_filepath is not None:
|
|
1018
|
-
logger.warning(
|
|
1019
|
-
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
1020
|
-
|
|
1021
|
-
logger.info('Total converted and uploaded: {}'.format(reporter.status_count('success')))
|
|
1022
|
-
logger.info('Total failed: {}'.format(reporter.status_count('failed')))
|
|
1023
|
-
|
|
1024
|
-
if self.return_error_filepath:
|
|
1025
|
-
return reporter.has_errors, log_filepath
|
|
1026
|
-
|
|
1027
|
-
def _convert_and_report(self, to_format, from_format, file_path, save_locally, save_to, conversion_func, reporter,
|
|
1028
|
-
i_item):
|
|
1029
|
-
try:
|
|
1030
|
-
annotations_list, errors = self.convert_file(
|
|
1031
|
-
to_format=to_format,
|
|
1032
|
-
from_format=from_format,
|
|
1033
|
-
file_path=file_path,
|
|
1034
|
-
save_locally=save_locally,
|
|
1035
|
-
save_to=save_to,
|
|
1036
|
-
conversion_func=conversion_func
|
|
1037
|
-
)
|
|
1038
|
-
if errors:
|
|
1039
|
-
reporter.set_index(ref=file_path, status='warning', success=False,
|
|
1040
|
-
error='partial annotations upload')
|
|
1041
|
-
else:
|
|
1042
|
-
reporter.set_index(ref=file_path, status='success', success=True)
|
|
1043
|
-
except Exception:
|
|
1044
|
-
reporter.set_index(ref=file_path, status='failed', success=False,
|
|
1045
|
-
error=traceback.format_exc())
|
|
1046
|
-
raise
|
|
1047
|
-
|
|
1048
|
-
@staticmethod
|
|
1049
|
-
def _extract_annotations_from_file(file_path, from_format, item):
|
|
1050
|
-
item_id = None
|
|
1051
|
-
annotations = None
|
|
1052
|
-
if isinstance(file_path, dict):
|
|
1053
|
-
annotations = file_path['annotations']
|
|
1054
|
-
elif isinstance(file_path, str) and os.path.isfile(file_path):
|
|
1055
|
-
with open(file_path, "r") as f:
|
|
1056
|
-
if file_path.endswith(".json"):
|
|
1057
|
-
annotations = json.load(f)
|
|
1058
|
-
if from_format.lower() == AnnotationFormat.DATALOOP:
|
|
1059
|
-
if item is None:
|
|
1060
|
-
item_id = annotations.get('_id', annotations.get('id', None))
|
|
1061
|
-
annotations = annotations["annotations"]
|
|
1062
|
-
elif from_format.lower() == AnnotationFormat.YOLO:
|
|
1063
|
-
annotations = [[float(param.replace('\n', ''))
|
|
1064
|
-
for param in line.strip().split(' ')]
|
|
1065
|
-
for line in f.readlines()]
|
|
1066
|
-
elif file_path.endswith(".xml"):
|
|
1067
|
-
annotations = Et.parse(f)
|
|
1068
|
-
annotations = [e for e in annotations.iter('object')]
|
|
1069
|
-
else:
|
|
1070
|
-
raise NotImplementedError('Unknown file format: {}'.format(file_path))
|
|
1071
|
-
else:
|
|
1072
|
-
raise NotImplementedError('Unknown file_path: {}'.format(file_path))
|
|
1073
|
-
|
|
1074
|
-
return item_id, annotations
|
|
1075
|
-
|
|
1076
|
-
def convert_file(self,
|
|
1077
|
-
to_format: str,
|
|
1078
|
-
from_format: str,
|
|
1079
|
-
file_path: str,
|
|
1080
|
-
save_locally: bool = False,
|
|
1081
|
-
save_to: str = None,
|
|
1082
|
-
conversion_func=None,
|
|
1083
|
-
item=None,
|
|
1084
|
-
pbar=None,
|
|
1085
|
-
upload: bool = False,
|
|
1086
|
-
**_):
|
|
1087
|
-
"""
|
|
1088
|
-
Convert file containing annotations.
|
|
1089
|
-
|
|
1090
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1091
|
-
|
|
1092
|
-
:param str to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1093
|
-
:param str from_format: AnnotationFormat to convert from – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1094
|
-
:param str file_path: path of the file to convert
|
|
1095
|
-
:param tqdm pbar: tqdm object that follows the work (progress bar)
|
|
1096
|
-
:param bool upload: if True upload
|
|
1097
|
-
:param bool save_locally: If True, save locally
|
|
1098
|
-
:param str save_to: path to save the result to
|
|
1099
|
-
:param Callable conversion_func: Custom conversion service
|
|
1100
|
-
:param dtlpy.entities.item.Item item: item entity
|
|
1101
|
-
:return: annotation list, errors
|
|
1102
|
-
"""
|
|
1103
|
-
item_id, annotations = self._extract_annotations_from_file(
|
|
1104
|
-
from_format=from_format,
|
|
1105
|
-
file_path=file_path,
|
|
1106
|
-
item=item
|
|
1107
|
-
)
|
|
1108
|
-
|
|
1109
|
-
converted_annotations = self.convert(
|
|
1110
|
-
to_format=to_format,
|
|
1111
|
-
from_format=from_format,
|
|
1112
|
-
annotations=annotations,
|
|
1113
|
-
conversion_func=conversion_func,
|
|
1114
|
-
item=item
|
|
1115
|
-
)
|
|
1116
|
-
|
|
1117
|
-
annotations_list, errors = self._sort_annotations(annotations=converted_annotations)
|
|
1118
|
-
|
|
1119
|
-
if annotations_list:
|
|
1120
|
-
if save_locally:
|
|
1121
|
-
if item_id is not None:
|
|
1122
|
-
item = self.dataset.items.get(item_id=item_id)
|
|
1123
|
-
filename = os.path.split(file_path)[-1]
|
|
1124
|
-
filename_no_ext = os.path.splitext(filename)[0]
|
|
1125
|
-
save_to = os.path.join(save_to, filename_no_ext)
|
|
1126
|
-
self.save_to_file(save_to=save_to, to_format=to_format, annotations=annotations_list, item=item)
|
|
1127
|
-
elif upload and to_format == AnnotationFormat.DATALOOP:
|
|
1128
|
-
item.annotations.upload(annotations=annotations_list)
|
|
1129
|
-
|
|
1130
|
-
if pbar is not None:
|
|
1131
|
-
pbar.update()
|
|
1132
|
-
|
|
1133
|
-
return annotations_list, errors
|
|
1134
|
-
|
|
1135
|
-
@staticmethod
|
|
1136
|
-
def _sort_annotations(annotations):
|
|
1137
|
-
errors = list()
|
|
1138
|
-
success = list()
|
|
1139
|
-
|
|
1140
|
-
for ann in annotations:
|
|
1141
|
-
if isinstance(ann, str) and 'Error converting annotation' in ann:
|
|
1142
|
-
errors.append(ann)
|
|
1143
|
-
else:
|
|
1144
|
-
success.append(ann)
|
|
1145
|
-
|
|
1146
|
-
return success, errors
|
|
1147
|
-
|
|
1148
|
-
def save_to_file(self, save_to, to_format, annotations, item=None):
|
|
1149
|
-
"""
|
|
1150
|
-
Save annotations to a file.
|
|
1151
|
-
|
|
1152
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1153
|
-
|
|
1154
|
-
:param str save_to: path to save the result to
|
|
1155
|
-
:param to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1156
|
-
:param list annotations: annotation list to convert
|
|
1157
|
-
:param dtlpy.entities.item.Item item: item entity
|
|
1158
|
-
"""
|
|
1159
|
-
# what file format
|
|
1160
|
-
if self.save_to_format is None:
|
|
1161
|
-
if to_format.lower() in [AnnotationFormat.DATALOOP, AnnotationFormat.COCO]:
|
|
1162
|
-
self.save_to_format = 'json'
|
|
1163
|
-
elif to_format.lower() in [AnnotationFormat.YOLO]:
|
|
1164
|
-
self.save_to_format = 'txt'
|
|
1165
|
-
else:
|
|
1166
|
-
self.save_to_format = 'xml'
|
|
1167
|
-
|
|
1168
|
-
# save
|
|
1169
|
-
# JSON #
|
|
1170
|
-
if self.save_to_format == 'json':
|
|
1171
|
-
# save json
|
|
1172
|
-
save_to = save_to + '.json'
|
|
1173
|
-
with open(save_to, "w") as f:
|
|
1174
|
-
json.dump(annotations, f, indent=2)
|
|
1175
|
-
|
|
1176
|
-
# TXT #
|
|
1177
|
-
elif self.save_to_format == 'txt':
|
|
1178
|
-
# save txt
|
|
1179
|
-
save_to = save_to + '.txt'
|
|
1180
|
-
with open(save_to, "w") as f:
|
|
1181
|
-
for ann in annotations:
|
|
1182
|
-
if ann is not None:
|
|
1183
|
-
f.write(' '.join([str(x) for x in ann]) + '\n')
|
|
1184
|
-
|
|
1185
|
-
# XML #
|
|
1186
|
-
elif self.save_to_format == 'xml':
|
|
1187
|
-
item = Converter.__get_item_shape(item=item)
|
|
1188
|
-
depth = item.metadata['system'].get('channels', 3)
|
|
1189
|
-
output_annotation = {
|
|
1190
|
-
'path': item.filename,
|
|
1191
|
-
'filename': os.path.basename(item.filename),
|
|
1192
|
-
'folder': os.path.basename(os.path.dirname(item.filename)),
|
|
1193
|
-
'width': item.width,
|
|
1194
|
-
'height': item.height,
|
|
1195
|
-
'depth': depth,
|
|
1196
|
-
'database': 'Unknown',
|
|
1197
|
-
'segmented': 0,
|
|
1198
|
-
'objects': annotations
|
|
1199
|
-
}
|
|
1200
|
-
save_to = save_to + '.xml'
|
|
1201
|
-
environment = Environment(loader=PackageLoader('dtlpy', 'assets'),
|
|
1202
|
-
keep_trailing_newline=True)
|
|
1203
|
-
annotation_template = environment.get_template(self.xml_template_path)
|
|
1204
|
-
with open(save_to, 'w') as file:
|
|
1205
|
-
content = annotation_template.render(**output_annotation)
|
|
1206
|
-
file.write(content)
|
|
1207
|
-
else:
|
|
1208
|
-
raise exceptions.PlatformException('400', 'Unknown file format to save to')
|
|
1209
|
-
|
|
1210
|
-
def _check_formats(self, from_format, to_format, conversion_func):
|
|
1211
|
-
if from_format.lower() not in self.known_formats and conversion_func is None:
|
|
1212
|
-
raise exceptions.PlatformException(
|
|
1213
|
-
"400",
|
|
1214
|
-
"Unknown annotation format: {}, possible formats: {}".format(
|
|
1215
|
-
from_format, self.known_formats
|
|
1216
|
-
),
|
|
1217
|
-
)
|
|
1218
|
-
if to_format.lower() not in self.known_formats and conversion_func is None:
|
|
1219
|
-
raise exceptions.PlatformException(
|
|
1220
|
-
"400",
|
|
1221
|
-
"Unknown annotation format: {}, possible formats: {}".format(
|
|
1222
|
-
to_format, self.known_formats
|
|
1223
|
-
),
|
|
1224
|
-
)
|
|
1225
|
-
|
|
1226
|
-
def convert(self,
|
|
1227
|
-
annotations,
|
|
1228
|
-
from_format: str,
|
|
1229
|
-
to_format: str,
|
|
1230
|
-
conversion_func=None,
|
|
1231
|
-
item=None):
|
|
1232
|
-
"""
|
|
1233
|
-
Convert annotation list or single annotation.
|
|
1234
|
-
|
|
1235
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1236
|
-
|
|
1237
|
-
:param dtlpy.entities.item.Item item: item entity
|
|
1238
|
-
:param list or AnnotationCollection annotations: annotations list to convert
|
|
1239
|
-
:param str from_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1240
|
-
:param str to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1241
|
-
:param Callable conversion_func: Custom conversion service
|
|
1242
|
-
:return: the annotations
|
|
1243
|
-
"""
|
|
1244
|
-
# check known format
|
|
1245
|
-
self._check_formats(from_format=from_format, to_format=to_format, conversion_func=conversion_func)
|
|
1246
|
-
|
|
1247
|
-
# check annotations param type
|
|
1248
|
-
if isinstance(annotations, entities.AnnotationCollection):
|
|
1249
|
-
annotations = annotations.annotations
|
|
1250
|
-
if not isinstance(annotations, list):
|
|
1251
|
-
annotations = [annotations]
|
|
1252
|
-
|
|
1253
|
-
if AnnotationFormat.DATALOOP not in [to_format, from_format]:
|
|
1254
|
-
raise exceptions.PlatformException(
|
|
1255
|
-
"400", "Can only convert from or to dataloop format"
|
|
1256
|
-
)
|
|
1257
|
-
|
|
1258
|
-
# call method
|
|
1259
|
-
if conversion_func is None:
|
|
1260
|
-
if from_format == AnnotationFormat.DATALOOP:
|
|
1261
|
-
method = self.converter_dict[to_format]["to"]
|
|
1262
|
-
else:
|
|
1263
|
-
method = self.converter_dict[from_format]["from"]
|
|
1264
|
-
else:
|
|
1265
|
-
method = self.custom_format
|
|
1266
|
-
|
|
1267
|
-
# run all annotations
|
|
1268
|
-
for i_annotation, annotation in enumerate(annotations):
|
|
1269
|
-
self._convert_single(
|
|
1270
|
-
annotation=annotation,
|
|
1271
|
-
i_annotation=i_annotation,
|
|
1272
|
-
conversion_func=conversion_func,
|
|
1273
|
-
annotations=annotations,
|
|
1274
|
-
from_format=AnnotationFormat.DATALOOP,
|
|
1275
|
-
item=item,
|
|
1276
|
-
method=method
|
|
1277
|
-
)
|
|
1278
|
-
return annotations
|
|
1279
|
-
|
|
1280
|
-
@staticmethod
|
|
1281
|
-
def _convert_single(method, **kwargs):
|
|
1282
|
-
annotations = kwargs.get('annotations', None)
|
|
1283
|
-
i_annotation = kwargs.get('i_annotation', None)
|
|
1284
|
-
try:
|
|
1285
|
-
ann = method(**kwargs)
|
|
1286
|
-
if annotations is not None and isinstance(i_annotation, int) and ann is not None:
|
|
1287
|
-
annotations[i_annotation] = ann
|
|
1288
|
-
except Exception:
|
|
1289
|
-
if annotations is not None and isinstance(i_annotation, int):
|
|
1290
|
-
annotations[i_annotation] = 'Error converting annotation: {}'.format(traceback.format_exc())
|
|
1291
|
-
else:
|
|
1292
|
-
raise
|
|
1293
|
-
|
|
1294
|
-
@staticmethod
|
|
1295
|
-
def from_voc(annotation, **_):
|
|
1296
|
-
"""
|
|
1297
|
-
Convert from VOC format to DATALOOP format. Use this as conversion_func for functions that ask for this param.
|
|
1298
|
-
|
|
1299
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1300
|
-
|
|
1301
|
-
:param annotation: annotations to convert
|
|
1302
|
-
:return: converted Annotation entity
|
|
1303
|
-
:rtype: dtlpy.entities.annotation.Annotation
|
|
1304
|
-
"""
|
|
1305
|
-
bndbox = annotation.find('bndbox')
|
|
1306
|
-
|
|
1307
|
-
if bndbox is None:
|
|
1308
|
-
raise Exception('No bndbox field found in annotation object')
|
|
1309
|
-
|
|
1310
|
-
bottom = float(bndbox.find('ymax').text)
|
|
1311
|
-
top = float(bndbox.find('ymin').text)
|
|
1312
|
-
left = float(bndbox.find('xmin').text)
|
|
1313
|
-
right = float(bndbox.find('xmax').text)
|
|
1314
|
-
label = annotation.find('name').text
|
|
1315
|
-
|
|
1316
|
-
if annotation.find('segmented'):
|
|
1317
|
-
if annotation.find('segmented') == '1':
|
|
1318
|
-
logger.warning('Only bounding box conversion is supported in voc format. Segmentation will be ignored.')
|
|
1319
|
-
|
|
1320
|
-
attributes = list(annotation)
|
|
1321
|
-
attrs = dict()
|
|
1322
|
-
for attribute in attributes:
|
|
1323
|
-
if attribute.tag not in ['bndbox', 'name'] and len(list(attribute)) == 0:
|
|
1324
|
-
if attribute.text is not None:
|
|
1325
|
-
if attribute.tag == 'attributes':
|
|
1326
|
-
attribute_value = eval(attribute.text)
|
|
1327
|
-
if isinstance(attribute_value, list):
|
|
1328
|
-
attrs = {attribute_value[i]: i for i in range(len(attribute_value))}
|
|
1329
|
-
else:
|
|
1330
|
-
attrs.update(attribute_value)
|
|
1331
|
-
else:
|
|
1332
|
-
try:
|
|
1333
|
-
attr_value = eval(attribute.text)
|
|
1334
|
-
except:
|
|
1335
|
-
attr_value = attribute.text
|
|
1336
|
-
attrs[attribute.tag] = attr_value
|
|
1337
|
-
|
|
1338
|
-
ann_def = entities.Box(label=label, top=top, bottom=bottom, left=left, right=right)
|
|
1339
|
-
new_annotation = entities.Annotation.new(annotation_definition=ann_def)
|
|
1340
|
-
if attrs is not None:
|
|
1341
|
-
try:
|
|
1342
|
-
new_annotation.attributes = attrs
|
|
1343
|
-
except:
|
|
1344
|
-
new_annotation.attributes = list(attrs.keys())
|
|
1345
|
-
return new_annotation
|
|
1346
|
-
|
|
1347
|
-
def from_yolo(self, annotation, item=None, **kwargs):
|
|
1348
|
-
"""
|
|
1349
|
-
Convert from YOLO format to DATALOOP format. Use this as conversion_func param for functions that ask for this param.
|
|
1350
|
-
|
|
1351
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1352
|
-
|
|
1353
|
-
:param annotation: annotations to convert
|
|
1354
|
-
:param dtlpy.entities.item.Item item: item entity
|
|
1355
|
-
:param kwargs: additional params
|
|
1356
|
-
:return: converted Annotation entity
|
|
1357
|
-
:rtype: dtlpy.entities.annotation.Annotation
|
|
1358
|
-
"""
|
|
1359
|
-
(label_id, x, y, w, h) = annotation
|
|
1360
|
-
label_id = int(label_id)
|
|
1361
|
-
|
|
1362
|
-
item = Converter.__get_item_shape(item=item)
|
|
1363
|
-
height = kwargs.get('height', None)
|
|
1364
|
-
width = kwargs.get('width', None)
|
|
1365
|
-
|
|
1366
|
-
if height is None or width is None:
|
|
1367
|
-
if item is None or item.width is None or item.height is None:
|
|
1368
|
-
raise Exception('Need item width and height in order to convert yolo annotation to dataloop')
|
|
1369
|
-
height = item.height
|
|
1370
|
-
width = item.width
|
|
1371
|
-
|
|
1372
|
-
x_center = x * width
|
|
1373
|
-
y_center = y * height
|
|
1374
|
-
w = w * width
|
|
1375
|
-
h = h * height
|
|
1376
|
-
|
|
1377
|
-
top = y_center - (h / 2)
|
|
1378
|
-
bottom = y_center + (h / 2)
|
|
1379
|
-
left = x_center - (w / 2)
|
|
1380
|
-
right = x_center + (w / 2)
|
|
1381
|
-
|
|
1382
|
-
label = self._label_by_category_id(category_id=label_id)
|
|
1383
|
-
|
|
1384
|
-
ann_def = entities.Box(label=label, top=top, bottom=bottom, left=left, right=right)
|
|
1385
|
-
return entities.Annotation.new(annotation_definition=ann_def, item=item)
|
|
1386
|
-
|
|
1387
|
-
def to_yolo(self, annotation, item=None, **_):
|
|
1388
|
-
"""
|
|
1389
|
-
Convert from DATALOOP format to YOLO format. Use this as conversion_func param for functions that ask for this param.
|
|
1390
|
-
|
|
1391
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1392
|
-
|
|
1393
|
-
:param dtlpy.entities.annotation.Annotation or dict annotation: annotations to convert
|
|
1394
|
-
:param dtlpy.entities.item.Item item: item entity
|
|
1395
|
-
:param **_: additional params
|
|
1396
|
-
:return: converted Annotation
|
|
1397
|
-
:rtype: tuple
|
|
1398
|
-
"""
|
|
1399
|
-
if not isinstance(annotation, entities.Annotation):
|
|
1400
|
-
if item is None:
|
|
1401
|
-
item = self.dataset.items.get(item_id=annotation['itemId'])
|
|
1402
|
-
annotation = entities.Annotation.from_json(_json=annotation, item=item)
|
|
1403
|
-
elif item is None:
|
|
1404
|
-
item = annotation.item
|
|
1405
|
-
|
|
1406
|
-
if annotation.type != "box":
|
|
1407
|
-
raise Exception('Only box annotations can be converted')
|
|
1408
|
-
|
|
1409
|
-
item = Converter.__get_item_shape(item=item)
|
|
1410
|
-
|
|
1411
|
-
if item is None or item.width is None or item.height is None:
|
|
1412
|
-
raise Exception("Cannot get item's width and height")
|
|
1413
|
-
|
|
1414
|
-
width, height = (item.width, item.height)
|
|
1415
|
-
if item.system.get('exif', {}).get('Orientation', 0) in [5, 6, 7, 8]:
|
|
1416
|
-
width, height = (item.height, item.width)
|
|
1417
|
-
|
|
1418
|
-
dw = 1.0 / width
|
|
1419
|
-
dh = 1.0 / height
|
|
1420
|
-
x = (annotation.left + annotation.right) / 2.0
|
|
1421
|
-
y = (annotation.top + annotation.bottom) / 2.0
|
|
1422
|
-
w = annotation.right - annotation.left
|
|
1423
|
-
h = annotation.bottom - annotation.top
|
|
1424
|
-
x = x * dw
|
|
1425
|
-
w = w * dw
|
|
1426
|
-
y = y * dh
|
|
1427
|
-
h = h * dh
|
|
1428
|
-
|
|
1429
|
-
label_id = self.labels[annotation.label]
|
|
1430
|
-
|
|
1431
|
-
ann = (label_id, x, y, w, h)
|
|
1432
|
-
return ann
|
|
1433
|
-
|
|
1434
|
-
def _label_by_category_id(self, category_id):
|
|
1435
|
-
if len(self.labels) > 0:
|
|
1436
|
-
for label_name, label_index in self.labels.items():
|
|
1437
|
-
if label_index == category_id:
|
|
1438
|
-
return label_name
|
|
1439
|
-
raise Exception('label category id not found: {}'.format(category_id))
|
|
1440
|
-
|
|
1441
|
-
def from_coco(self, annotation, **kwargs):
|
|
1442
|
-
"""
|
|
1443
|
-
Convert from COCO format to DATALOOP format. Use this as conversion_func param for functions that ask for this param.
|
|
1444
|
-
|
|
1445
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1446
|
-
|
|
1447
|
-
:param annotation: annotations to convert
|
|
1448
|
-
:param kwargs: additional params
|
|
1449
|
-
:return: converted Annotation entity
|
|
1450
|
-
:rtype: dtlpy.entities.annotation.Annotation
|
|
1451
|
-
"""
|
|
1452
|
-
item = kwargs.get('item', None)
|
|
1453
|
-
_id = annotation.get('id', None)
|
|
1454
|
-
category_id = annotation.get('category_id', None)
|
|
1455
|
-
segmentation = annotation.get('segmentation', None)
|
|
1456
|
-
iscrowd = annotation.get('iscrowd', None)
|
|
1457
|
-
label = self._label_by_category_id(category_id=category_id)
|
|
1458
|
-
ann_def = None
|
|
1459
|
-
|
|
1460
|
-
if hasattr(self, '_only_bbox') and self._only_bbox:
|
|
1461
|
-
bbox = annotation.get('bbox', None)
|
|
1462
|
-
left = bbox[0]
|
|
1463
|
-
top = bbox[1]
|
|
1464
|
-
right = left + bbox[2]
|
|
1465
|
-
bottom = top + bbox[3]
|
|
1466
|
-
|
|
1467
|
-
ann_def = entities.Box(top=top, left=left, bottom=bottom, right=right, label=label)
|
|
1468
|
-
else:
|
|
1469
|
-
if iscrowd is not None and int(iscrowd) == 1:
|
|
1470
|
-
ann_def = entities.Segmentation(label=label, geo=COCOUtils.rle_to_binary_mask(rle=segmentation))
|
|
1471
|
-
else:
|
|
1472
|
-
if segmentation is not None and len(segmentation) == 1:
|
|
1473
|
-
segmentation = segmentation[0]
|
|
1474
|
-
if segmentation:
|
|
1475
|
-
ann_def = entities.Polygon(label=label,
|
|
1476
|
-
geo=COCOUtils.rle_to_binary_polygon(segmentation=segmentation))
|
|
1477
|
-
|
|
1478
|
-
elif segmentation is not None and len(segmentation) > 1:
|
|
1479
|
-
# TODO - support conversion of split annotations
|
|
1480
|
-
raise exceptions.PlatformException('400',
|
|
1481
|
-
'unable to convert.'
|
|
1482
|
-
'Converter does not support split annotations: {}'.format(
|
|
1483
|
-
_id))
|
|
1484
|
-
if not segmentation:
|
|
1485
|
-
bbox = annotation.get('bbox', None)
|
|
1486
|
-
if bbox:
|
|
1487
|
-
left = bbox[0]
|
|
1488
|
-
top = bbox[1]
|
|
1489
|
-
right = left + bbox[2]
|
|
1490
|
-
bottom = top + bbox[3]
|
|
1491
|
-
ann_def = entities.Box(top=top, left=left, bottom=bottom, right=right, label=label)
|
|
1492
|
-
else:
|
|
1493
|
-
raise Exception('Unable to convert annotation, not coordinates')
|
|
1494
|
-
|
|
1495
|
-
return entities.Annotation.new(annotation_definition=ann_def, item=item)
|
|
1496
|
-
|
|
1497
|
-
@staticmethod
|
|
1498
|
-
def to_coco(annotation, item=None, **_):
|
|
1499
|
-
"""
|
|
1500
|
-
Convert from DATALOOP format to COCO format. Use this as conversion_func param for functions that ask for this param.
|
|
1501
|
-
|
|
1502
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1503
|
-
|
|
1504
|
-
:param dtlpy.entities.annotation.Annotation or dict annotation: annotations to convert
|
|
1505
|
-
:param dtlpy.entities.item.Item item: item entity
|
|
1506
|
-
:param **_: additional params
|
|
1507
|
-
:return: converted Annotation
|
|
1508
|
-
:rtype: dict
|
|
1509
|
-
"""
|
|
1510
|
-
item = Converter.__get_item_shape(item=item)
|
|
1511
|
-
height = item.height if item is not None else None
|
|
1512
|
-
width = item.width if item is not None else None
|
|
1513
|
-
|
|
1514
|
-
if not isinstance(annotation, entities.Annotation):
|
|
1515
|
-
annotation = entities.Annotation.from_json(annotation, item=item)
|
|
1516
|
-
area = 0
|
|
1517
|
-
iscrowd = 0
|
|
1518
|
-
segmentation = [[]]
|
|
1519
|
-
if annotation.type == 'polyline':
|
|
1520
|
-
raise Exception('Unable to convert annotation of type "polyline" to coco')
|
|
1521
|
-
|
|
1522
|
-
if annotation.type in ['binary', 'segment']:
|
|
1523
|
-
if height is None or width is None:
|
|
1524
|
-
raise Exception(
|
|
1525
|
-
'Item must have height and width to convert {!r} annotation to coco'.format(annotation.type))
|
|
1526
|
-
|
|
1527
|
-
# build annotation
|
|
1528
|
-
keypoints = None
|
|
1529
|
-
if annotation.type in ['binary', 'box', 'segment', 'pose']:
|
|
1530
|
-
x = annotation.left
|
|
1531
|
-
y = annotation.top
|
|
1532
|
-
w = annotation.right - x
|
|
1533
|
-
h = annotation.bottom - y
|
|
1534
|
-
area = float(h * w)
|
|
1535
|
-
if annotation.type == 'binary':
|
|
1536
|
-
# segmentation = COCOUtils.binary_mask_to_rle(binary_mask=annotation.geo, height=height, width=width)
|
|
1537
|
-
segmentation = COCOUtils.binary_mask_to_rle_encode(binary_mask=annotation.geo)
|
|
1538
|
-
area = int(annotation.geo.sum())
|
|
1539
|
-
iscrowd = 1
|
|
1540
|
-
elif annotation.type in ['segment']:
|
|
1541
|
-
segmentation, area = COCOUtils.polygon_to_rle(geo=annotation.geo, height=height, width=width)
|
|
1542
|
-
elif annotation.type == 'pose':
|
|
1543
|
-
keypoints = list()
|
|
1544
|
-
for point in annotation.annotation_definition.points:
|
|
1545
|
-
keypoints.append(point.x)
|
|
1546
|
-
keypoints.append(point.y)
|
|
1547
|
-
if isinstance(point.attributes, list):
|
|
1548
|
-
if 'visible' in point.attributes and \
|
|
1549
|
-
("not-visible" in point.attributes or 'not_visible' in point.attributes):
|
|
1550
|
-
keypoints.append(0)
|
|
1551
|
-
elif 'not-visible' in point.attributes or 'not_visible' in point.attributes:
|
|
1552
|
-
keypoints.append(1)
|
|
1553
|
-
elif 'visible' in point.attributes:
|
|
1554
|
-
keypoints.append(2)
|
|
1555
|
-
else:
|
|
1556
|
-
keypoints.append(0)
|
|
1557
|
-
else:
|
|
1558
|
-
list_attributes = list(point.attributes.values())
|
|
1559
|
-
if 'Visible' in list_attributes:
|
|
1560
|
-
keypoints.append(2)
|
|
1561
|
-
else:
|
|
1562
|
-
keypoints.append(0)
|
|
1563
|
-
x_points = keypoints[0::3]
|
|
1564
|
-
y_points = keypoints[1::3]
|
|
1565
|
-
x0, x1, y0, y1 = np.min(x_points), np.max(x_points), np.min(y_points), np.max(y_points)
|
|
1566
|
-
area = float((x1 - x0) * (y1 - y0))
|
|
1567
|
-
else:
|
|
1568
|
-
raise Exception('Unable to convert annotation of type {} to coco'.format(annotation.type))
|
|
1569
|
-
|
|
1570
|
-
ann = dict()
|
|
1571
|
-
ann['bbox'] = [float(x), float(y), float(w), float(h)]
|
|
1572
|
-
ann["segmentation"] = segmentation
|
|
1573
|
-
ann["area"] = area
|
|
1574
|
-
ann["iscrowd"] = iscrowd
|
|
1575
|
-
if keypoints is not None:
|
|
1576
|
-
ann["keypoints"] = keypoints
|
|
1577
|
-
return ann
|
|
1578
|
-
|
|
1579
|
-
@staticmethod
|
|
1580
|
-
def to_voc(annotation, item=None, **_):
|
|
1581
|
-
"""
|
|
1582
|
-
Convert from DATALOOP format to VOC format. Use this as conversion_func param for functions that ask for this param.
|
|
1583
|
-
|
|
1584
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1585
|
-
|
|
1586
|
-
:param dtlpy.entities.annotation.Annotation or dict annotation: annotations to convert
|
|
1587
|
-
:param dtlpy.entities.item.Item item: item entity
|
|
1588
|
-
:param **_: additional params
|
|
1589
|
-
:return: converted Annotation
|
|
1590
|
-
:rtype: dict
|
|
1591
|
-
"""
|
|
1592
|
-
if not isinstance(annotation, entities.Annotation):
|
|
1593
|
-
annotation = entities.Annotation.from_json(annotation, item=item)
|
|
1594
|
-
|
|
1595
|
-
if annotation.type != 'box':
|
|
1596
|
-
raise exceptions.PlatformException('400', 'Annotation must be of type box')
|
|
1597
|
-
|
|
1598
|
-
label = annotation.label
|
|
1599
|
-
|
|
1600
|
-
attributes = dict()
|
|
1601
|
-
if annotation.attributes is not None:
|
|
1602
|
-
if isinstance(annotation.attributes, dict) and len(annotation.attributes) > 0:
|
|
1603
|
-
attributes = annotation.attributes
|
|
1604
|
-
elif isinstance(annotation.attributes, list) and len(annotation.attributes) > 0:
|
|
1605
|
-
attributes = {annotation.attributes[i]: i for i in range(len(annotation.attributes))}
|
|
1606
|
-
|
|
1607
|
-
left = annotation.left
|
|
1608
|
-
top = annotation.top
|
|
1609
|
-
right = annotation.right
|
|
1610
|
-
bottom = annotation.bottom
|
|
1611
|
-
|
|
1612
|
-
ann = {'name': label,
|
|
1613
|
-
'xmin': left,
|
|
1614
|
-
'ymin': top,
|
|
1615
|
-
'xmax': right,
|
|
1616
|
-
'ymax': bottom
|
|
1617
|
-
}
|
|
1618
|
-
if attributes:
|
|
1619
|
-
ann['attributes'] = attributes
|
|
1620
|
-
return ann
|
|
1621
|
-
|
|
1622
|
-
@staticmethod
|
|
1623
|
-
def custom_format(annotation,
|
|
1624
|
-
conversion_func,
|
|
1625
|
-
i_annotation=None,
|
|
1626
|
-
annotations=None,
|
|
1627
|
-
from_format=None,
|
|
1628
|
-
item=None, **_):
|
|
1629
|
-
"""
|
|
1630
|
-
Custom convert function.
|
|
1631
|
-
|
|
1632
|
-
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1633
|
-
|
|
1634
|
-
:param dtlpy.entities.annotation.Annotation or dict annotation: annotations to convert
|
|
1635
|
-
:param Callable conversion_func: Custom conversion service
|
|
1636
|
-
:param int i_annotation: annotation index
|
|
1637
|
-
:param list annotations: list of annotations
|
|
1638
|
-
param str from_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1639
|
-
:param dtlpy.entities.item.Item item: item entity
|
|
1640
|
-
:return: converted Annotation
|
|
1641
|
-
"""
|
|
1642
|
-
if from_format == AnnotationFormat.DATALOOP and isinstance(annotation, dict):
|
|
1643
|
-
annotation = entities.Annotation.from_json(_json=annotation, item=item)
|
|
1644
|
-
|
|
1645
|
-
ann = conversion_func(annotation, item)
|
|
1646
|
-
|
|
1647
|
-
if isinstance(annotations[i_annotation], entities.Annotation) and annotations[i_annotation].item is None:
|
|
1648
|
-
ann.item = item
|
|
1649
|
-
|
|
1650
|
-
return ann
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from jinja2 import Environment, PackageLoader
|
|
4
|
+
from multiprocessing.pool import ThreadPool
|
|
5
|
+
from multiprocessing import Lock
|
|
6
|
+
from .base_package_runner import Progress
|
|
7
|
+
from .. import exceptions, entities
|
|
8
|
+
import xml.etree.ElementTree as Et
|
|
9
|
+
from ..services import Reporter
|
|
10
|
+
from itertools import groupby
|
|
11
|
+
from PIL import Image
|
|
12
|
+
import numpy as np
|
|
13
|
+
import mimetypes
|
|
14
|
+
import traceback
|
|
15
|
+
import logging
|
|
16
|
+
import json
|
|
17
|
+
import copy
|
|
18
|
+
import tqdm
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(name='dtlpy')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AnnotationFormat:
|
|
25
|
+
YOLO = 'yolo'
|
|
26
|
+
COCO = 'coco'
|
|
27
|
+
VOC = 'voc'
|
|
28
|
+
DATALOOP = 'dataloop'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class COCOUtils:
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def binary_mask_to_rle_encode(binary_mask):
|
|
35
|
+
try:
|
|
36
|
+
import pycocotools.mask as coco_utils_mask
|
|
37
|
+
except ModuleNotFoundError:
|
|
38
|
+
raise Exception('To use this functionality please install pycocotools: "pip install pycocotools"')
|
|
39
|
+
fortran_ground_truth_binary_mask = np.asfortranarray(binary_mask.astype(np.uint8))
|
|
40
|
+
encoded_ground_truth = coco_utils_mask.encode(fortran_ground_truth_binary_mask)
|
|
41
|
+
encoded_ground_truth['counts'] = encoded_ground_truth['counts'].decode()
|
|
42
|
+
return encoded_ground_truth
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def binary_mask_to_rle(binary_mask, height, width):
|
|
46
|
+
rle = {'counts': [], 'size': [height, width]}
|
|
47
|
+
counts = rle.get('counts')
|
|
48
|
+
for i, (value, elements) in enumerate(groupby(binary_mask.ravel(order='F'))):
|
|
49
|
+
if i == 0 and value == 1:
|
|
50
|
+
counts.append(0)
|
|
51
|
+
counts.append(len(list(elements)))
|
|
52
|
+
return rle
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def polygon_to_rle(geo, height, width):
|
|
56
|
+
segmentation = [float(n) for n in geo.flatten()]
|
|
57
|
+
area = np.sum(entities.Segmentation.from_polygon(geo=geo, label=None, shape=(height, width)).geo > 0)
|
|
58
|
+
return [segmentation], int(area)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def rle_to_binary_mask(rle):
|
|
62
|
+
rows, cols = rle['size']
|
|
63
|
+
rle_numbers = rle['counts']
|
|
64
|
+
if isinstance(rle_numbers, list):
|
|
65
|
+
if len(rle_numbers) % 2 != 0:
|
|
66
|
+
rle_numbers.append(0)
|
|
67
|
+
|
|
68
|
+
rle_pairs = np.array(rle_numbers).reshape(-1, 2)
|
|
69
|
+
img = np.zeros(rows * cols, dtype=np.uint8)
|
|
70
|
+
index = 0
|
|
71
|
+
for i, length in rle_pairs:
|
|
72
|
+
index += i
|
|
73
|
+
img[index:index + length] = 1
|
|
74
|
+
index += length
|
|
75
|
+
img = img.reshape(cols, rows)
|
|
76
|
+
return img.T
|
|
77
|
+
else:
|
|
78
|
+
try:
|
|
79
|
+
import pycocotools.mask as coco_utils_mask
|
|
80
|
+
except ModuleNotFoundError:
|
|
81
|
+
raise Exception('To use this functionality please install pycocotools: "pip install pycocotools"')
|
|
82
|
+
img = coco_utils_mask.decode(rle)
|
|
83
|
+
return img
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def rle_to_binary_polygon(segmentation):
|
|
87
|
+
return [segmentation[x:x + 2] for x in range(0, len(segmentation), 2)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Converter:
|
|
91
|
+
"""
|
|
92
|
+
Annotation Converter
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, concurrency=6, return_error_filepath=False):
|
|
96
|
+
self.known_formats = [AnnotationFormat.YOLO,
|
|
97
|
+
AnnotationFormat.COCO,
|
|
98
|
+
AnnotationFormat.VOC,
|
|
99
|
+
AnnotationFormat.DATALOOP]
|
|
100
|
+
self.converter_dict = {
|
|
101
|
+
AnnotationFormat.YOLO: {"from": self.from_yolo, "to": self.to_yolo},
|
|
102
|
+
AnnotationFormat.COCO: {"from": self.from_coco, "to": self.to_coco},
|
|
103
|
+
AnnotationFormat.VOC: {"from": self.from_voc, "to": self.to_voc},
|
|
104
|
+
}
|
|
105
|
+
self.dataset = None
|
|
106
|
+
self.save_to_format = None
|
|
107
|
+
self.xml_template_path = 'voc_annotation_template.xml'
|
|
108
|
+
self.labels = dict()
|
|
109
|
+
self._only_bbox = False
|
|
110
|
+
self._progress = None
|
|
111
|
+
self._progress_update_frequency = 5
|
|
112
|
+
self._update_agent_progress = False
|
|
113
|
+
self._progress_checkpoint = 0
|
|
114
|
+
self._checkpoint_lock = Lock()
|
|
115
|
+
self.remote_items = None
|
|
116
|
+
self.concurrency = concurrency
|
|
117
|
+
self.return_error_filepath = return_error_filepath
|
|
118
|
+
|
|
119
|
+
def attach_agent_progress(self, progress: Progress, progress_update_frequency: int = None):
|
|
120
|
+
"""
|
|
121
|
+
Attach agent progress.
|
|
122
|
+
|
|
123
|
+
:param Progress progress: the progress object that follows the work
|
|
124
|
+
:param int progress_update_frequency: progress update frequency in percentages
|
|
125
|
+
"""
|
|
126
|
+
self._progress = progress
|
|
127
|
+
self._progress_update_frequency = progress_update_frequency if progress_update_frequency is not None \
|
|
128
|
+
else self._progress_update_frequency
|
|
129
|
+
self._update_agent_progress = True
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def _update_progress_active(self):
|
|
133
|
+
return self._progress is not None and self._update_agent_progress and isinstance(
|
|
134
|
+
self._progress_update_frequency, int)
|
|
135
|
+
|
|
136
|
+
def __update_progress(self, total, of_total):
|
|
137
|
+
if self._update_progress_active:
|
|
138
|
+
try:
|
|
139
|
+
progress = int((of_total / total) * 100)
|
|
140
|
+
if progress > self._progress_checkpoint and (progress % self._progress_update_frequency == 0):
|
|
141
|
+
self._progress.update(progress=progress)
|
|
142
|
+
with self._checkpoint_lock:
|
|
143
|
+
self._progress_checkpoint = progress
|
|
144
|
+
except Exception:
|
|
145
|
+
logger.warning('[Converter] Failed to update agent progress.')
|
|
146
|
+
|
|
147
|
+
def _get_labels(self):
|
|
148
|
+
self.labels = dict()
|
|
149
|
+
if self.dataset:
|
|
150
|
+
labels = list(self.dataset.instance_map.keys())
|
|
151
|
+
labels.sort()
|
|
152
|
+
self.labels = {label: i for i, label in enumerate(labels)}
|
|
153
|
+
|
|
154
|
+
def convert_dataset(
|
|
155
|
+
self,
|
|
156
|
+
dataset,
|
|
157
|
+
to_format: str,
|
|
158
|
+
local_path: str,
|
|
159
|
+
conversion_func=None,
|
|
160
|
+
filters=None,
|
|
161
|
+
annotation_filter=None
|
|
162
|
+
):
|
|
163
|
+
"""
|
|
164
|
+
Convert entire dataset.
|
|
165
|
+
|
|
166
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
167
|
+
|
|
168
|
+
:param dtlpy.entities.dataet.Dataset dataset: dataset entity
|
|
169
|
+
:param str to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
170
|
+
:param str local_path: path to save the result to
|
|
171
|
+
:param Callable conversion_func: Custom conversion service
|
|
172
|
+
:param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filter parameters
|
|
173
|
+
:param dtlpy.entities.filters.Filters annotation_filter: Filter entity
|
|
174
|
+
:return: the error log file path if there are errors and the coco json if the format is coco
|
|
175
|
+
"""
|
|
176
|
+
if to_format.lower() == AnnotationFormat.COCO:
|
|
177
|
+
coco_json, has_errors, log_filepath = self.__convert_dataset_to_coco(
|
|
178
|
+
dataset=dataset,
|
|
179
|
+
local_path=local_path,
|
|
180
|
+
filters=filters,
|
|
181
|
+
annotation_filter=annotation_filter
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if self.return_error_filepath:
|
|
185
|
+
return coco_json, has_errors, log_filepath
|
|
186
|
+
else:
|
|
187
|
+
return coco_json
|
|
188
|
+
|
|
189
|
+
assert isinstance(dataset, entities.Dataset)
|
|
190
|
+
self.dataset = dataset
|
|
191
|
+
|
|
192
|
+
# download annotations
|
|
193
|
+
if annotation_filter is None:
|
|
194
|
+
logger.info('Downloading annotations...')
|
|
195
|
+
dataset.download_annotations(local_path=local_path, overwrite=True, filters=filters)
|
|
196
|
+
logger.info('Annotations downloaded')
|
|
197
|
+
local_annotations_path = os.path.join(local_path, "json")
|
|
198
|
+
output_annotations_path = os.path.join(local_path, to_format)
|
|
199
|
+
pool = ThreadPool(processes=self.concurrency)
|
|
200
|
+
i_item = 0
|
|
201
|
+
pages = dataset.items.list(filters=filters)
|
|
202
|
+
|
|
203
|
+
# if yolo - create labels file
|
|
204
|
+
if to_format == AnnotationFormat.YOLO:
|
|
205
|
+
self._get_labels()
|
|
206
|
+
with open('{}/{}.names'.format(local_path, dataset.name), 'w') as fp:
|
|
207
|
+
labels = list(self.labels.keys())
|
|
208
|
+
labels.sort()
|
|
209
|
+
for label in labels:
|
|
210
|
+
fp.write("{}\n".format(label))
|
|
211
|
+
|
|
212
|
+
pbar = tqdm.tqdm(total=pages.items_count, disable=dataset._client_api.verbose.disable_progress_bar_convert_annotations,
|
|
213
|
+
file=sys.stdout, desc='Convert Annotations')
|
|
214
|
+
reporter = Reporter(
|
|
215
|
+
num_workers=pages.items_count,
|
|
216
|
+
resource=Reporter.CONVERTER,
|
|
217
|
+
print_error_logs=self.dataset._client_api.verbose.print_error_logs,
|
|
218
|
+
client_api=self.dataset._client_api
|
|
219
|
+
)
|
|
220
|
+
for page in pages:
|
|
221
|
+
for item in page:
|
|
222
|
+
# create input annotations json
|
|
223
|
+
in_filepath = os.path.join(local_annotations_path, item.filename[1:])
|
|
224
|
+
name, ext = os.path.splitext(in_filepath)
|
|
225
|
+
in_filepath = name + '.json'
|
|
226
|
+
|
|
227
|
+
save_to = os.path.dirname(in_filepath.replace(local_annotations_path, output_annotations_path))
|
|
228
|
+
|
|
229
|
+
if not os.path.isdir(save_to):
|
|
230
|
+
os.makedirs(save_to, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
pool.apply_async(
|
|
233
|
+
func=self.__save_filtered_annotations_and_convert,
|
|
234
|
+
kwds={
|
|
235
|
+
"to_format": to_format,
|
|
236
|
+
"from_format": AnnotationFormat.DATALOOP,
|
|
237
|
+
"file_path": in_filepath,
|
|
238
|
+
"save_locally": True,
|
|
239
|
+
"save_to": save_to,
|
|
240
|
+
'conversion_func': conversion_func,
|
|
241
|
+
'item': item,
|
|
242
|
+
'pbar': pbar,
|
|
243
|
+
'filters': annotation_filter,
|
|
244
|
+
'reporter': reporter,
|
|
245
|
+
'i_item': i_item
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
i_item += 1
|
|
249
|
+
pool.close()
|
|
250
|
+
pool.join()
|
|
251
|
+
pool.terminate()
|
|
252
|
+
|
|
253
|
+
log_filepath = None
|
|
254
|
+
|
|
255
|
+
if reporter.has_errors:
|
|
256
|
+
log_filepath = reporter.generate_log_files()
|
|
257
|
+
if log_filepath is not None:
|
|
258
|
+
logger.warning(
|
|
259
|
+
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
260
|
+
|
|
261
|
+
logger.info('Total converted: {}'.format(reporter.status_count('success')))
|
|
262
|
+
logger.info('Total skipped: {}'.format(reporter.status_count('skip')))
|
|
263
|
+
logger.info('Total failed: {}'.format(reporter.status_count('failed')))
|
|
264
|
+
|
|
265
|
+
if self.return_error_filepath:
|
|
266
|
+
return reporter.has_errors, log_filepath
|
|
267
|
+
|
|
268
|
+
def __save_filtered_annotations_and_convert(self, item: entities.Item, filters, to_format, from_format, file_path,
|
|
269
|
+
save_locally=False,
|
|
270
|
+
save_to=None, conversion_func=None,
|
|
271
|
+
pbar=None, **kwargs):
|
|
272
|
+
reporter = kwargs.get('reporter', None)
|
|
273
|
+
i_item = kwargs.get('i_item', None)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
if item.annotated and item.type != 'dir':
|
|
277
|
+
if filters is not None:
|
|
278
|
+
assert filters.resource == entities.FiltersResource.ANNOTATION
|
|
279
|
+
copy_filters = copy.deepcopy(filters)
|
|
280
|
+
copy_filters.add(field='itemId', values=item.id, method='and')
|
|
281
|
+
annotations_page = item.dataset.items.list(filters=copy_filters)
|
|
282
|
+
annotations = item.annotations.builder()
|
|
283
|
+
for page in annotations_page:
|
|
284
|
+
for annotation in page:
|
|
285
|
+
annotations.annotations.append(annotation)
|
|
286
|
+
|
|
287
|
+
if not os.path.isdir(os.path.dirname(file_path)):
|
|
288
|
+
os.makedirs(os.path.dirname(file_path))
|
|
289
|
+
with open(file_path, 'w') as f:
|
|
290
|
+
json.dump(annotations.to_json(), f, indent=2)
|
|
291
|
+
|
|
292
|
+
annotations_list, errors = self.convert_file(
|
|
293
|
+
item=item,
|
|
294
|
+
to_format=to_format,
|
|
295
|
+
from_format=from_format,
|
|
296
|
+
file_path=file_path,
|
|
297
|
+
save_locally=save_locally,
|
|
298
|
+
save_to=save_to,
|
|
299
|
+
conversion_func=conversion_func,
|
|
300
|
+
pbar=pbar
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if errors:
|
|
304
|
+
raise Exception('Partial conversion: \n{}'.format(errors))
|
|
305
|
+
|
|
306
|
+
if reporter is not None and i_item is not None:
|
|
307
|
+
reporter.set_index(status='success', success=True, ref=item.id)
|
|
308
|
+
else:
|
|
309
|
+
if reporter is not None and i_item is not None:
|
|
310
|
+
reporter.set_index(ref=item.id, status='skip', success=True)
|
|
311
|
+
if reporter is not None:
|
|
312
|
+
self.__update_progress(total=reporter.num_workers, of_total=i_item)
|
|
313
|
+
except Exception:
|
|
314
|
+
if reporter is not None and i_item is not None:
|
|
315
|
+
reporter.set_index(status='failed', success=False, error=traceback.format_exc(),
|
|
316
|
+
ref=item.id)
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def __gen_coco_categories(instance_map, recipe):
|
|
320
|
+
categories = list()
|
|
321
|
+
last_id = 0
|
|
322
|
+
for label, label_id in instance_map.items():
|
|
323
|
+
label_name, sup = label.split('.')[-1], '.'.join(label.split('.')[0:-1])
|
|
324
|
+
category = {'id': label_id, 'name': label_name}
|
|
325
|
+
last_id = max(last_id, label_id)
|
|
326
|
+
if sup:
|
|
327
|
+
category['supercategory'] = sup
|
|
328
|
+
categories.append(category)
|
|
329
|
+
|
|
330
|
+
# add keypoint category
|
|
331
|
+
collection_templates = list()
|
|
332
|
+
if 'system' in recipe.metadata and 'collectionTemplates' in recipe.metadata['system']:
|
|
333
|
+
collection_templates = recipe.metadata['system']['collectionTemplates']
|
|
334
|
+
|
|
335
|
+
for template in collection_templates:
|
|
336
|
+
last_id += 1
|
|
337
|
+
order_dict = {key: i for i, key in enumerate(template['order'])}
|
|
338
|
+
skeleton = list()
|
|
339
|
+
for pair in template['arcs']:
|
|
340
|
+
skeleton.append([order_dict[pair[0]], order_dict[pair[1]]])
|
|
341
|
+
category = {'id': last_id,
|
|
342
|
+
'name': template['name'],
|
|
343
|
+
'templateId': template['id'],
|
|
344
|
+
'keypoints': template['order'],
|
|
345
|
+
'skeleton': skeleton}
|
|
346
|
+
instance_map[template['name']] = last_id
|
|
347
|
+
categories.append(category)
|
|
348
|
+
return categories
|
|
349
|
+
|
|
350
|
+
def __convert_dataset_to_coco(self, dataset: entities.Dataset, local_path, filters=None, annotation_filter=None):
|
|
351
|
+
pages = dataset.items.list(filters=filters)
|
|
352
|
+
logger.info('items count: {}'.format(pages.items_count))
|
|
353
|
+
dataset.download_annotations(local_path=local_path, filters=filters, annotation_filters=annotation_filter)
|
|
354
|
+
path_to_dataloop_annotations_dir = os.path.join(local_path, 'json')
|
|
355
|
+
label_to_id = dataset.instance_map
|
|
356
|
+
recipe = dataset._get_recipe()
|
|
357
|
+
categories = self.__gen_coco_categories(instance_map=label_to_id, recipe=recipe)
|
|
358
|
+
images = [None for _ in range(pages.items_count)]
|
|
359
|
+
converted_annotations = [None for _ in range(pages.items_count)]
|
|
360
|
+
item_id_counter = 0
|
|
361
|
+
pool = ThreadPool(processes=self.concurrency)
|
|
362
|
+
pbar = tqdm.tqdm(total=pages.items_count, disable=dataset._client_api.verbose.disable_progress_bar_convert_annotations,
|
|
363
|
+
file=sys.stdout, desc='Convert Annotations')
|
|
364
|
+
reporter = Reporter(
|
|
365
|
+
num_workers=pages.items_count,
|
|
366
|
+
resource=Reporter.CONVERTER,
|
|
367
|
+
print_error_logs=dataset._client_api.verbose.print_error_logs,
|
|
368
|
+
client_api=dataset._client_api
|
|
369
|
+
)
|
|
370
|
+
for page in pages:
|
|
371
|
+
for item in page:
|
|
372
|
+
try:
|
|
373
|
+
pool.apply_async(func=self.__single_item_to_coco,
|
|
374
|
+
kwds={
|
|
375
|
+
'item': item,
|
|
376
|
+
'images': images,
|
|
377
|
+
'path_to_dataloop_annotations_dir': path_to_dataloop_annotations_dir,
|
|
378
|
+
'item_id': item_id_counter,
|
|
379
|
+
'reporter': reporter,
|
|
380
|
+
'converted_annotations': converted_annotations,
|
|
381
|
+
'annotation_filter': annotation_filter,
|
|
382
|
+
'label_to_id': label_to_id,
|
|
383
|
+
'categories': categories,
|
|
384
|
+
'pbar': pbar
|
|
385
|
+
})
|
|
386
|
+
item_id_counter += 1
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.error('Failed to convert item: {}'.format(item.id))
|
|
389
|
+
|
|
390
|
+
pool.close()
|
|
391
|
+
pool.join()
|
|
392
|
+
pool.terminate()
|
|
393
|
+
|
|
394
|
+
total_converted_annotations = list()
|
|
395
|
+
for ls in converted_annotations:
|
|
396
|
+
if ls is not None:
|
|
397
|
+
total_converted_annotations += ls
|
|
398
|
+
|
|
399
|
+
for i_ann, ann in enumerate(total_converted_annotations):
|
|
400
|
+
ann['id'] = i_ann
|
|
401
|
+
|
|
402
|
+
info = {
|
|
403
|
+
'description': dataset.name
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
coco_json = {'images': [image for image in images if image is not None],
|
|
407
|
+
'info': info,
|
|
408
|
+
'annotations': total_converted_annotations,
|
|
409
|
+
'categories': categories}
|
|
410
|
+
|
|
411
|
+
with open(os.path.join(local_path, 'coco.json'), 'w+', encoding='utf-8') as f:
|
|
412
|
+
json.dump(coco_json, f, ensure_ascii=False)
|
|
413
|
+
|
|
414
|
+
log_filepath = None
|
|
415
|
+
if reporter.has_errors:
|
|
416
|
+
log_filepath = reporter.generate_log_files()
|
|
417
|
+
if log_filepath is not None:
|
|
418
|
+
logger.warning(
|
|
419
|
+
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
420
|
+
|
|
421
|
+
logger.info('Total converted: {}'.format(reporter.success_count))
|
|
422
|
+
logger.info('Total failed: {}'.format(reporter.failure_count))
|
|
423
|
+
logger.info('Total skipped: {}'.format(reporter.status_count('skip')))
|
|
424
|
+
if reporter.success_count + reporter.status_count('skip') + reporter.failure_count != pages.items_count:
|
|
425
|
+
raise ValueError('Not all items were processed')
|
|
426
|
+
|
|
427
|
+
return coco_json, reporter.has_errors, log_filepath
|
|
428
|
+
|
|
429
|
+
@staticmethod
|
|
430
|
+
def __get_item_shape(item: entities.Item = None, local_path: str = None):
|
|
431
|
+
if isinstance(item, entities.Item) and (item.width is None or item.height is None):
|
|
432
|
+
try:
|
|
433
|
+
img = Image.open(item.download(save_locally=False)) if local_path is None else Image.open(local_path)
|
|
434
|
+
item.height = img.height
|
|
435
|
+
item.width = img.width
|
|
436
|
+
except Exception:
|
|
437
|
+
pass
|
|
438
|
+
return item
|
|
439
|
+
|
|
440
|
+
def __add_item_converted_annotation(self, item, annotation, label_to_id, item_id,
|
|
441
|
+
i_annotation, item_converted_annotations):
|
|
442
|
+
try:
|
|
443
|
+
ann = self.to_coco(annotation=annotation, item=item)
|
|
444
|
+
ann['category_id'] = label_to_id[annotation.label]
|
|
445
|
+
ann['image_id'] = item_id
|
|
446
|
+
ann['id'] = int('{}{}'.format(item_id, i_annotation))
|
|
447
|
+
|
|
448
|
+
item_converted_annotations.append(ann)
|
|
449
|
+
except Exception:
|
|
450
|
+
err = 'Error converting annotation: \n' \
|
|
451
|
+
'Item: {}, annotation: {} - ' \
|
|
452
|
+
'fail to convert some of the annotation\n{}'.format(item_id,
|
|
453
|
+
annotation.id,
|
|
454
|
+
traceback.format_exc())
|
|
455
|
+
item_converted_annotations.append(err)
|
|
456
|
+
|
|
457
|
+
def __coco_handle_pose_annotations(self, item, item_id, pose_annotations, point_annotations,
|
|
458
|
+
categories, label_to_id, item_converted_annotations):
|
|
459
|
+
# link points to pose and convert it
|
|
460
|
+
for pose in pose_annotations:
|
|
461
|
+
pose_idx = pose[1]
|
|
462
|
+
pose = pose[0]
|
|
463
|
+
pose_category = None
|
|
464
|
+
for category in categories:
|
|
465
|
+
if pose.coordinates.get('templateId', "") == category.get('templateId', None):
|
|
466
|
+
pose_category = category
|
|
467
|
+
continue
|
|
468
|
+
if pose_category is None:
|
|
469
|
+
err = 'Error converting annotation: \n' \
|
|
470
|
+
'Item: {}, annotation: {} - ' \
|
|
471
|
+
'Pose annotation without known template\n{}'.format(item_id,
|
|
472
|
+
pose.id,
|
|
473
|
+
traceback.format_exc())
|
|
474
|
+
item_converted_annotations.append(err)
|
|
475
|
+
continue
|
|
476
|
+
if pose.id not in point_annotations or (pose.id in point_annotations and
|
|
477
|
+
len(point_annotations[pose.id]) != len(pose_category['keypoints'])):
|
|
478
|
+
err = 'Error converting annotation: \n' \
|
|
479
|
+
'Item: {}, annotation: {} - ' \
|
|
480
|
+
'Pose annotation has {} children ' \
|
|
481
|
+
'while it template has {} points\n{}'.format(item_id,
|
|
482
|
+
pose.id,
|
|
483
|
+
len(point_annotations[pose.id]),
|
|
484
|
+
len(pose_category['keypoints']),
|
|
485
|
+
traceback.format_exc())
|
|
486
|
+
item_converted_annotations.append(err)
|
|
487
|
+
continue
|
|
488
|
+
# verify points labels are unique
|
|
489
|
+
if len(point_annotations[pose.id]) != len(set([ann.label for ann in point_annotations[pose.id]])):
|
|
490
|
+
err = 'Error converting annotation: \n' \
|
|
491
|
+
'Item: {}, annotation: {} - Pose annotation ' \
|
|
492
|
+
'does not have unique children points\n{}'.format(item_id,
|
|
493
|
+
pose.id,
|
|
494
|
+
traceback.format_exc())
|
|
495
|
+
item_converted_annotations.append(err)
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
ordered_points = list()
|
|
499
|
+
for pose_point in pose_category['keypoints']:
|
|
500
|
+
for point_annotation in point_annotations[pose.id]:
|
|
501
|
+
if point_annotation.label == pose_point:
|
|
502
|
+
ordered_points.append(point_annotation)
|
|
503
|
+
break
|
|
504
|
+
pose.annotation_definition.points = ordered_points
|
|
505
|
+
|
|
506
|
+
self.__add_item_converted_annotation(item=item, annotation=pose,
|
|
507
|
+
label_to_id=label_to_id,
|
|
508
|
+
item_id=item_id, i_annotation=pose_idx,
|
|
509
|
+
item_converted_annotations=item_converted_annotations)
|
|
510
|
+
|
|
511
|
+
def __single_item_to_coco(self, item: entities.Item,
|
|
512
|
+
images,
|
|
513
|
+
path_to_dataloop_annotations_dir,
|
|
514
|
+
item_id,
|
|
515
|
+
converted_annotations,
|
|
516
|
+
annotation_filter,
|
|
517
|
+
label_to_id,
|
|
518
|
+
reporter,
|
|
519
|
+
categories,
|
|
520
|
+
pbar=None):
|
|
521
|
+
try:
|
|
522
|
+
if item.type != 'dir':
|
|
523
|
+
item = Converter.__get_item_shape(item=item)
|
|
524
|
+
filepath = item.filename[1:] if item.filename.startswith('/') else item.filename
|
|
525
|
+
images[item_id] = {'file_name': os.path.normpath(filepath),
|
|
526
|
+
'id': item_id,
|
|
527
|
+
'width': item.width,
|
|
528
|
+
'height': item.height
|
|
529
|
+
}
|
|
530
|
+
if annotation_filter is None:
|
|
531
|
+
try:
|
|
532
|
+
filename, ext = os.path.splitext(item.filename)
|
|
533
|
+
filename = '{}.json'.format(filename[1:])
|
|
534
|
+
with open(os.path.join(path_to_dataloop_annotations_dir, filename), 'r', encoding="utf8") as f:
|
|
535
|
+
annotations = json.load(f)['annotations']
|
|
536
|
+
annotations = entities.AnnotationCollection.from_json(annotations)
|
|
537
|
+
except Exception:
|
|
538
|
+
annotations = item.annotations.list()
|
|
539
|
+
else:
|
|
540
|
+
copy_filters = copy.deepcopy(annotation_filter)
|
|
541
|
+
copy_filters.add(field='itemId', values=item.id, method='and')
|
|
542
|
+
annotations_page = item.dataset.items.list(filters=copy_filters)
|
|
543
|
+
annotations = item.annotations.builder()
|
|
544
|
+
for page in annotations_page:
|
|
545
|
+
for annotation in page:
|
|
546
|
+
annotations.annotations.append(annotation)
|
|
547
|
+
|
|
548
|
+
item_converted_annotations = list()
|
|
549
|
+
point_annotations = dict()
|
|
550
|
+
pose_annotations = list()
|
|
551
|
+
|
|
552
|
+
for i_annotation, annotation in enumerate(annotations.annotations):
|
|
553
|
+
if annotation.type == 'point' and annotation.parent_id is not None:
|
|
554
|
+
if annotation.parent_id not in point_annotations:
|
|
555
|
+
point_annotations[annotation.parent_id] = list()
|
|
556
|
+
point_annotations[annotation.parent_id].append(annotation)
|
|
557
|
+
continue
|
|
558
|
+
if annotation.type == 'pose':
|
|
559
|
+
pose_annotations.append([annotation, i_annotation])
|
|
560
|
+
continue
|
|
561
|
+
self.__add_item_converted_annotation(item=item, annotation=annotation,
|
|
562
|
+
label_to_id=label_to_id,
|
|
563
|
+
item_id=item_id, i_annotation=i_annotation,
|
|
564
|
+
item_converted_annotations=item_converted_annotations)
|
|
565
|
+
|
|
566
|
+
self.__coco_handle_pose_annotations(item=item, item_id=item_id,
|
|
567
|
+
pose_annotations=pose_annotations,
|
|
568
|
+
point_annotations=point_annotations,
|
|
569
|
+
categories=categories,
|
|
570
|
+
label_to_id=label_to_id,
|
|
571
|
+
item_converted_annotations=item_converted_annotations)
|
|
572
|
+
|
|
573
|
+
success, errors = self._sort_annotations(annotations=item_converted_annotations)
|
|
574
|
+
converted_annotations[item_id] = success
|
|
575
|
+
if errors:
|
|
576
|
+
reporter.set_index(ref=item.id, status='failed', success=False,
|
|
577
|
+
error=errors)
|
|
578
|
+
else:
|
|
579
|
+
reporter.set_index(ref=item.id, status='success', success=True)
|
|
580
|
+
else:
|
|
581
|
+
reporter.set_index(ref=item.id, status='skipped', success=True)
|
|
582
|
+
except Exception:
|
|
583
|
+
reporter.set_index(ref=item.id, status='failed', success=False,
|
|
584
|
+
error=traceback.format_exc())
|
|
585
|
+
logger.debug('Error converting item: {}\n{}'.format(item.id, traceback.format_exc()))
|
|
586
|
+
raise Exception('Error converting item: {}\n{}'.format(item.id, traceback.format_exc()))
|
|
587
|
+
if pbar is not None:
|
|
588
|
+
pbar.update()
|
|
589
|
+
|
|
590
|
+
if reporter is not None:
|
|
591
|
+
self.__update_progress(total=reporter.num_workers, of_total=item_id)
|
|
592
|
+
|
|
593
|
+
def _upload_coco_labels(self, coco_json):
|
|
594
|
+
labels = coco_json.get('categories', None)
|
|
595
|
+
upload_labels = dict()
|
|
596
|
+
for label in labels:
|
|
597
|
+
if 'supercategory' in label and label['supercategory'] is not None:
|
|
598
|
+
if label['supercategory'] not in upload_labels:
|
|
599
|
+
upload_labels[label['supercategory']] = entities.Label(tag=label['supercategory'])
|
|
600
|
+
upload_labels[label['supercategory']].children.append(entities.Label(tag=label['name']))
|
|
601
|
+
tag = '{}.{}'.format(label['supercategory'], label['name'])
|
|
602
|
+
else:
|
|
603
|
+
tag = label['name']
|
|
604
|
+
upload_labels[label['name']] = entities.Label(tag=tag)
|
|
605
|
+
self.labels[tag] = label['id']
|
|
606
|
+
|
|
607
|
+
return upload_labels
|
|
608
|
+
|
|
609
|
+
def _upload_coco_dataset(self, local_items_path, local_annotations_path, only_bbox=False, remote_items=False):
|
|
610
|
+
logger.info('loading annotations json...')
|
|
611
|
+
with open(local_annotations_path, 'r', encoding="utf8") as f:
|
|
612
|
+
coco_json = json.load(f)
|
|
613
|
+
|
|
614
|
+
labels_tags_tree = self._upload_coco_labels(coco_json=coco_json)
|
|
615
|
+
try:
|
|
616
|
+
logger.info('Uploading labels to dataset')
|
|
617
|
+
self.dataset.add_labels(list(labels_tags_tree.values()))
|
|
618
|
+
except Exception:
|
|
619
|
+
logger.warning('Failed to upload labels to dataset, please add manually')
|
|
620
|
+
|
|
621
|
+
image_annotations = dict()
|
|
622
|
+
image_name_id = dict()
|
|
623
|
+
for image in coco_json['images']:
|
|
624
|
+
image_metadata = image
|
|
625
|
+
image_annotations[image['file_name']] = {
|
|
626
|
+
'id': image['id'],
|
|
627
|
+
'metadata': image_metadata,
|
|
628
|
+
'annotations': list()
|
|
629
|
+
}
|
|
630
|
+
image_name_id[image['id']] = image['file_name']
|
|
631
|
+
for ann in coco_json['annotations']:
|
|
632
|
+
image_annotations[image_name_id[ann['image_id']]]['annotations'].append(ann)
|
|
633
|
+
|
|
634
|
+
if remote_items:
|
|
635
|
+
return self._upload_annotations(local_annotations_path=image_annotations,
|
|
636
|
+
from_format=AnnotationFormat.COCO,
|
|
637
|
+
only_bbox=only_bbox)
|
|
638
|
+
else:
|
|
639
|
+
return self._upload_directory(local_items_path=local_items_path,
|
|
640
|
+
local_annotations_path=image_annotations,
|
|
641
|
+
from_format=AnnotationFormat.COCO,
|
|
642
|
+
only_bbox=only_bbox)
|
|
643
|
+
|
|
644
|
+
def _read_labels(self, labels_file_path):
|
|
645
|
+
if labels_file_path:
|
|
646
|
+
with open(labels_file_path, 'r') as fp:
|
|
647
|
+
labels = [line.strip() for line in fp.readlines()]
|
|
648
|
+
self.dataset.add_labels(label_list=labels)
|
|
649
|
+
self.labels = {label: i_label for i_label, label in enumerate(labels)}
|
|
650
|
+
else:
|
|
651
|
+
logger.warning('No labels file path provided (.names), skipping labels upload')
|
|
652
|
+
self._get_labels()
|
|
653
|
+
|
|
654
|
+
def _upload_yolo_dataset(self, local_items_path, local_annotations_path, labels_file_path, remote_items=False):
|
|
655
|
+
self._read_labels(labels_file_path=labels_file_path)
|
|
656
|
+
if remote_items:
|
|
657
|
+
return self._upload_annotations(local_annotations_path=local_annotations_path,
|
|
658
|
+
from_format=AnnotationFormat.YOLO)
|
|
659
|
+
else:
|
|
660
|
+
return self._upload_directory(local_items_path=local_items_path,
|
|
661
|
+
local_annotations_path=local_annotations_path,
|
|
662
|
+
from_format=AnnotationFormat.YOLO)
|
|
663
|
+
|
|
664
|
+
def _upload_voc_dataset(self, local_items_path, local_annotations_path, remote_items=False, **_):
|
|
665
|
+
# TODO - implement VOC annotations upload
|
|
666
|
+
logger.warning('labels upload from VOC dataset is not implemented, please upload labels manually')
|
|
667
|
+
|
|
668
|
+
if remote_items:
|
|
669
|
+
return self._upload_annotations(local_annotations_path=local_annotations_path,
|
|
670
|
+
from_format=AnnotationFormat.VOC)
|
|
671
|
+
else:
|
|
672
|
+
return self._upload_directory(local_items_path=local_items_path,
|
|
673
|
+
local_annotations_path=local_annotations_path,
|
|
674
|
+
from_format=AnnotationFormat.VOC)
|
|
675
|
+
|
|
676
|
+
@staticmethod
|
|
677
|
+
def _find_yolo_voc_item_annotations(local_annotations_path: str, item: entities.Item, from_format: str):
|
|
678
|
+
found = False
|
|
679
|
+
metadata = None
|
|
680
|
+
|
|
681
|
+
extension = '.txt' if from_format == AnnotationFormat.YOLO else '.xml'
|
|
682
|
+
filename, _ = os.path.splitext(item.filename)
|
|
683
|
+
annotations_filepath = os.path.join(local_annotations_path, filename[1:] + extension)
|
|
684
|
+
if os.path.isfile(annotations_filepath):
|
|
685
|
+
found = True
|
|
686
|
+
|
|
687
|
+
return found, annotations_filepath, metadata
|
|
688
|
+
|
|
689
|
+
@staticmethod
|
|
690
|
+
def _find_coco_item_annotations(local_annotations_path: dict, item: entities.Item):
|
|
691
|
+
found = False
|
|
692
|
+
ann_dict = None
|
|
693
|
+
filename = item.filename[1:] if item.filename.startswith('/') else item.filename
|
|
694
|
+
filename = os.path.normpath(filename)
|
|
695
|
+
if filename in local_annotations_path:
|
|
696
|
+
ann_dict = local_annotations_path[filename]
|
|
697
|
+
found = True
|
|
698
|
+
elif item.name in local_annotations_path:
|
|
699
|
+
ann_dict = local_annotations_path[item.name]
|
|
700
|
+
found = True
|
|
701
|
+
metadata = ann_dict.get('metadata', None) if found else None
|
|
702
|
+
return found, ann_dict, metadata
|
|
703
|
+
|
|
704
|
+
def _upload_annotations(self, local_annotations_path, from_format, **kwargs):
|
|
705
|
+
self._only_bbox = kwargs.get('only_bbox', False)
|
|
706
|
+
file_count = self.remote_items.items_count
|
|
707
|
+
reporter = Reporter(
|
|
708
|
+
num_workers=file_count,
|
|
709
|
+
resource=Reporter.CONVERTER,
|
|
710
|
+
print_error_logs=self.dataset._client_api.verbose.print_error_logs,
|
|
711
|
+
client_api=self.dataset._client_api
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
pbar = tqdm.tqdm(total=file_count, desc='Upload Annotations')
|
|
715
|
+
pool = ThreadPool(processes=self.concurrency)
|
|
716
|
+
i_item = 0
|
|
717
|
+
|
|
718
|
+
for page in self.remote_items:
|
|
719
|
+
for item in page:
|
|
720
|
+
if from_format == AnnotationFormat.COCO:
|
|
721
|
+
found, ann_filepath, metadata = self._find_coco_item_annotations(
|
|
722
|
+
local_annotations_path=local_annotations_path,
|
|
723
|
+
item=item
|
|
724
|
+
)
|
|
725
|
+
elif from_format in [AnnotationFormat.YOLO, AnnotationFormat.VOC]:
|
|
726
|
+
found, ann_filepath, metadata = self._find_yolo_voc_item_annotations(
|
|
727
|
+
local_annotations_path=local_annotations_path,
|
|
728
|
+
item=item,
|
|
729
|
+
from_format=from_format
|
|
730
|
+
)
|
|
731
|
+
else:
|
|
732
|
+
raise exceptions.PlatformException('400', 'Unknown annotation format: {}'.format(from_format))
|
|
733
|
+
if not found:
|
|
734
|
+
pbar.update()
|
|
735
|
+
reporter.set_index(ref=item.filename, status='skip', success=False,
|
|
736
|
+
error='Cannot find annotations for item')
|
|
737
|
+
i_item += 1
|
|
738
|
+
continue
|
|
739
|
+
pool.apply_async(
|
|
740
|
+
func=self._upload_item_and_convert,
|
|
741
|
+
kwds={
|
|
742
|
+
"from_format": from_format,
|
|
743
|
+
"item_path": item,
|
|
744
|
+
"ann_path": ann_filepath,
|
|
745
|
+
'conversion_func': None,
|
|
746
|
+
"reporter": reporter,
|
|
747
|
+
'i_item': i_item,
|
|
748
|
+
'pbar': pbar,
|
|
749
|
+
'metadata': metadata
|
|
750
|
+
}
|
|
751
|
+
)
|
|
752
|
+
i_item += 1
|
|
753
|
+
pool.close()
|
|
754
|
+
pool.join()
|
|
755
|
+
pool.terminate()
|
|
756
|
+
|
|
757
|
+
log_filepath = None
|
|
758
|
+
if reporter.has_errors:
|
|
759
|
+
log_filepath = reporter.generate_log_files()
|
|
760
|
+
if log_filepath is not None:
|
|
761
|
+
logger.warning(
|
|
762
|
+
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
763
|
+
|
|
764
|
+
logger.info('Total converted and uploaded: {}'.format(reporter.status_count('success')))
|
|
765
|
+
logger.info('Total failed: {}'.format(reporter.status_count('failed')))
|
|
766
|
+
|
|
767
|
+
if self.return_error_filepath:
|
|
768
|
+
return reporter.has_errors, log_filepath
|
|
769
|
+
|
|
770
|
+
def _upload_directory(self, local_items_path, local_annotations_path, from_format, conversion_func=None, **kwargs):
|
|
771
|
+
"""
|
|
772
|
+
Convert annotation files in entire directory.
|
|
773
|
+
|
|
774
|
+
:param local_items_path:
|
|
775
|
+
:param local_annotations_path:
|
|
776
|
+
:param from_format:
|
|
777
|
+
:param conversion_func:
|
|
778
|
+
:return:
|
|
779
|
+
"""
|
|
780
|
+
self._only_bbox = kwargs.get('only_bbox', False)
|
|
781
|
+
file_count = sum(len([file for file in files if not file.endswith('.xml')]) for _, _, files in
|
|
782
|
+
os.walk(local_items_path))
|
|
783
|
+
|
|
784
|
+
reporter = Reporter(
|
|
785
|
+
num_workers=file_count,
|
|
786
|
+
resource=Reporter.CONVERTER,
|
|
787
|
+
print_error_logs=self.dataset._client_api.verbose.print_error_logs,
|
|
788
|
+
client_api=self.dataset._client_api
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
pbar = tqdm.tqdm(total=file_count, desc='Upload Annotations')
|
|
792
|
+
|
|
793
|
+
pool = ThreadPool(processes=self.concurrency)
|
|
794
|
+
i_item = 0
|
|
795
|
+
metadata = None
|
|
796
|
+
for path, subdirs, files in os.walk(local_items_path):
|
|
797
|
+
for name in files:
|
|
798
|
+
ann_filepath = None
|
|
799
|
+
if not os.path.isfile(os.path.join(path, name)):
|
|
800
|
+
continue
|
|
801
|
+
item_filepath = os.path.join(path, name)
|
|
802
|
+
prefix = os.path.relpath(path, local_items_path)
|
|
803
|
+
coco_name = name
|
|
804
|
+
if prefix != '.':
|
|
805
|
+
coco_name = os.path.join(prefix, name)
|
|
806
|
+
else:
|
|
807
|
+
prefix = None
|
|
808
|
+
if from_format == AnnotationFormat.COCO:
|
|
809
|
+
ann_filepath = local_annotations_path[coco_name]
|
|
810
|
+
metadata = {'user': local_annotations_path[coco_name]['metadata']}
|
|
811
|
+
elif from_format == AnnotationFormat.VOC:
|
|
812
|
+
if name.endswith('.xml'):
|
|
813
|
+
continue
|
|
814
|
+
else:
|
|
815
|
+
ext = os.path.splitext(name)[-1]
|
|
816
|
+
try:
|
|
817
|
+
m = mimetypes.types_map[ext.lower()]
|
|
818
|
+
except Exception:
|
|
819
|
+
m = ''
|
|
820
|
+
|
|
821
|
+
if ext == '' or ext is None or 'image' not in m:
|
|
822
|
+
continue
|
|
823
|
+
|
|
824
|
+
ann_filepath = os.path.join(path, '.'.join(name.split('.')[0:-1] + ['xml'])).replace(
|
|
825
|
+
local_items_path, local_annotations_path)
|
|
826
|
+
elif from_format == AnnotationFormat.YOLO:
|
|
827
|
+
ann_filepath = os.path.join(path, os.path.splitext(name)[0]) + '.txt'
|
|
828
|
+
ann_filepath = ann_filepath.replace(local_items_path, local_annotations_path)
|
|
829
|
+
pool.apply_async(
|
|
830
|
+
func=self._upload_item_and_convert,
|
|
831
|
+
kwds={
|
|
832
|
+
"from_format": from_format,
|
|
833
|
+
"item_path": item_filepath,
|
|
834
|
+
"ann_path": ann_filepath,
|
|
835
|
+
'conversion_func': conversion_func,
|
|
836
|
+
"reporter": reporter,
|
|
837
|
+
'i_item': i_item,
|
|
838
|
+
'pbar': pbar,
|
|
839
|
+
'metadata': metadata,
|
|
840
|
+
'remote_path': prefix
|
|
841
|
+
}
|
|
842
|
+
)
|
|
843
|
+
i_item += 1
|
|
844
|
+
pool.close()
|
|
845
|
+
pool.join()
|
|
846
|
+
pool.terminate()
|
|
847
|
+
|
|
848
|
+
log_filepath = None
|
|
849
|
+
if reporter.has_errors:
|
|
850
|
+
log_filepath = reporter.generate_log_files()
|
|
851
|
+
if log_filepath is not None:
|
|
852
|
+
logger.warning(
|
|
853
|
+
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
854
|
+
|
|
855
|
+
logger.info('Total converted and uploaded: {}'.format(reporter.status_count('success')))
|
|
856
|
+
logger.info('Total failed: {}'.format(reporter.status_count('failed')))
|
|
857
|
+
|
|
858
|
+
if self.return_error_filepath:
|
|
859
|
+
return reporter.has_errors, log_filepath
|
|
860
|
+
|
|
861
|
+
def _upload_item_and_convert(self, item_path, ann_path, from_format, conversion_func=None, **kwargs):
|
|
862
|
+
reporter = kwargs.get('reporter', None)
|
|
863
|
+
i_item = kwargs.get('i_item', None)
|
|
864
|
+
pbar = kwargs.get('pbar', None)
|
|
865
|
+
metadata = kwargs.get('metadata', None)
|
|
866
|
+
remote_path = kwargs.get('remote_path', None)
|
|
867
|
+
report_ref = item_path
|
|
868
|
+
try:
|
|
869
|
+
if isinstance(item_path, entities.Item):
|
|
870
|
+
item = item_path
|
|
871
|
+
report_ref = item.filename
|
|
872
|
+
else:
|
|
873
|
+
item = self.dataset.items.upload(local_path=item_path, item_metadata=metadata, remote_path=remote_path)
|
|
874
|
+
report_ref = item.filename
|
|
875
|
+
if from_format == AnnotationFormat.YOLO:
|
|
876
|
+
item = Converter.__get_item_shape(item=item, local_path=item_path)
|
|
877
|
+
annotations_list, errors = self.convert_file(to_format=AnnotationFormat.DATALOOP,
|
|
878
|
+
from_format=from_format,
|
|
879
|
+
item=item,
|
|
880
|
+
file_path=ann_path,
|
|
881
|
+
save_locally=False,
|
|
882
|
+
conversion_func=conversion_func,
|
|
883
|
+
upload=True,
|
|
884
|
+
pbar=pbar)
|
|
885
|
+
if errors:
|
|
886
|
+
if reporter is not None and i_item is not None:
|
|
887
|
+
reporter.set_index(ref=report_ref,
|
|
888
|
+
status='warning',
|
|
889
|
+
success=False,
|
|
890
|
+
error='partial annotations upload: \n{}'.format(errors))
|
|
891
|
+
else:
|
|
892
|
+
if reporter is not None and i_item is not None:
|
|
893
|
+
reporter.set_index(status='success',
|
|
894
|
+
success=True,
|
|
895
|
+
ref=report_ref)
|
|
896
|
+
if reporter is not None:
|
|
897
|
+
self.__update_progress(total=reporter.num_workers, of_total=i_item)
|
|
898
|
+
except Exception:
|
|
899
|
+
if reporter is not None and i_item is not None:
|
|
900
|
+
reporter.set_index(status='failed',
|
|
901
|
+
success=False,
|
|
902
|
+
error=traceback.format_exc(),
|
|
903
|
+
ref=report_ref)
|
|
904
|
+
|
|
905
|
+
def upload_local_dataset(self,
|
|
906
|
+
from_format: AnnotationFormat,
|
|
907
|
+
dataset,
|
|
908
|
+
local_items_path: str = None,
|
|
909
|
+
local_labels_path: str = None,
|
|
910
|
+
local_annotations_path: str = None,
|
|
911
|
+
only_bbox: bool = False,
|
|
912
|
+
filters=None,
|
|
913
|
+
remote_items=None
|
|
914
|
+
):
|
|
915
|
+
"""
|
|
916
|
+
Convert and upload local dataset to dataloop platform.
|
|
917
|
+
|
|
918
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
919
|
+
|
|
920
|
+
:param str from_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
921
|
+
:param dtlpy.entities.dataset.Dataset dataset: dataset entity
|
|
922
|
+
:param str local_items_path: path to items to upload
|
|
923
|
+
:param str local_annotations_path: path to annotations to upload
|
|
924
|
+
:param str local_labels_path: path to labels to upload
|
|
925
|
+
:param bool only_bbox: only for coco datasets, if True upload only bbox
|
|
926
|
+
:param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filter parameters
|
|
927
|
+
:param list remote_items: list of the items to upload
|
|
928
|
+
:return: the error log file path if there are errors
|
|
929
|
+
"""
|
|
930
|
+
|
|
931
|
+
if remote_items is None:
|
|
932
|
+
remote_items = local_items_path is None
|
|
933
|
+
|
|
934
|
+
if remote_items:
|
|
935
|
+
logger.info('Getting remote items...')
|
|
936
|
+
self.remote_items = dataset.items.list(filters=filters)
|
|
937
|
+
|
|
938
|
+
self.dataset = dataset
|
|
939
|
+
if from_format.lower() == AnnotationFormat.COCO:
|
|
940
|
+
return self._upload_coco_dataset(
|
|
941
|
+
local_items_path=local_items_path,
|
|
942
|
+
local_annotations_path=local_annotations_path,
|
|
943
|
+
only_bbox=only_bbox,
|
|
944
|
+
remote_items=remote_items
|
|
945
|
+
)
|
|
946
|
+
if from_format.lower() == AnnotationFormat.YOLO:
|
|
947
|
+
return self._upload_yolo_dataset(
|
|
948
|
+
local_items_path=local_items_path,
|
|
949
|
+
local_annotations_path=local_annotations_path,
|
|
950
|
+
labels_file_path=local_labels_path,
|
|
951
|
+
remote_items=remote_items
|
|
952
|
+
)
|
|
953
|
+
if from_format.lower() == AnnotationFormat.VOC:
|
|
954
|
+
return self._upload_voc_dataset(
|
|
955
|
+
local_items_path=local_items_path,
|
|
956
|
+
local_annotations_path=local_annotations_path,
|
|
957
|
+
labels_file_path=local_labels_path,
|
|
958
|
+
remote_items=remote_items
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
def convert_directory(self,
|
|
962
|
+
local_path: str,
|
|
963
|
+
to_format: AnnotationFormat,
|
|
964
|
+
from_format: AnnotationFormat,
|
|
965
|
+
dataset,
|
|
966
|
+
conversion_func=None):
|
|
967
|
+
"""
|
|
968
|
+
Convert annotation files in entire directory.
|
|
969
|
+
|
|
970
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
971
|
+
|
|
972
|
+
:param str local_path: path to the directory
|
|
973
|
+
:param str to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
974
|
+
:param str from_format: AnnotationFormat to convert from – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
975
|
+
:param dtlpy.entities.dataset.Dataset dataset: dataset entity
|
|
976
|
+
:param Callable conversion_func: Custom conversion service
|
|
977
|
+
:return: the error log file path if there are errors
|
|
978
|
+
"""
|
|
979
|
+
file_count = sum(len(files) for _, _, files in os.walk(local_path))
|
|
980
|
+
self.dataset = dataset
|
|
981
|
+
reporter = Reporter(
|
|
982
|
+
num_workers=file_count,
|
|
983
|
+
resource=Reporter.CONVERTER,
|
|
984
|
+
print_error_logs=self.dataset._client_api.verbose.print_error_logs,
|
|
985
|
+
client_api=self.dataset._client_api
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
pool = ThreadPool(processes=self.concurrency)
|
|
989
|
+
i_item = 0
|
|
990
|
+
for path, subdirs, files in os.walk(local_path):
|
|
991
|
+
for name in files:
|
|
992
|
+
save_to = os.path.join(os.path.split(local_path)[0], to_format)
|
|
993
|
+
save_to = path.replace(local_path, save_to)
|
|
994
|
+
os.makedirs(save_to, exist_ok=True)
|
|
995
|
+
file_path = os.path.join(path, name)
|
|
996
|
+
pool.apply_async(
|
|
997
|
+
func=self._convert_and_report,
|
|
998
|
+
kwds={
|
|
999
|
+
"to_format": to_format,
|
|
1000
|
+
"from_format": from_format,
|
|
1001
|
+
"file_path": file_path,
|
|
1002
|
+
"save_locally": True,
|
|
1003
|
+
"save_to": save_to,
|
|
1004
|
+
'conversion_func': conversion_func,
|
|
1005
|
+
'reporter': reporter,
|
|
1006
|
+
'i_item': i_item
|
|
1007
|
+
}
|
|
1008
|
+
)
|
|
1009
|
+
i_item += 1
|
|
1010
|
+
pool.close()
|
|
1011
|
+
pool.join()
|
|
1012
|
+
pool.terminate()
|
|
1013
|
+
|
|
1014
|
+
log_filepath = None
|
|
1015
|
+
if reporter.has_errors:
|
|
1016
|
+
log_filepath = reporter.generate_log_files()
|
|
1017
|
+
if log_filepath is not None:
|
|
1018
|
+
logger.warning(
|
|
1019
|
+
'Converted with some errors. Please see log in {} for more information.'.format(log_filepath))
|
|
1020
|
+
|
|
1021
|
+
logger.info('Total converted and uploaded: {}'.format(reporter.status_count('success')))
|
|
1022
|
+
logger.info('Total failed: {}'.format(reporter.status_count('failed')))
|
|
1023
|
+
|
|
1024
|
+
if self.return_error_filepath:
|
|
1025
|
+
return reporter.has_errors, log_filepath
|
|
1026
|
+
|
|
1027
|
+
def _convert_and_report(self, to_format, from_format, file_path, save_locally, save_to, conversion_func, reporter,
|
|
1028
|
+
i_item):
|
|
1029
|
+
try:
|
|
1030
|
+
annotations_list, errors = self.convert_file(
|
|
1031
|
+
to_format=to_format,
|
|
1032
|
+
from_format=from_format,
|
|
1033
|
+
file_path=file_path,
|
|
1034
|
+
save_locally=save_locally,
|
|
1035
|
+
save_to=save_to,
|
|
1036
|
+
conversion_func=conversion_func
|
|
1037
|
+
)
|
|
1038
|
+
if errors:
|
|
1039
|
+
reporter.set_index(ref=file_path, status='warning', success=False,
|
|
1040
|
+
error='partial annotations upload')
|
|
1041
|
+
else:
|
|
1042
|
+
reporter.set_index(ref=file_path, status='success', success=True)
|
|
1043
|
+
except Exception:
|
|
1044
|
+
reporter.set_index(ref=file_path, status='failed', success=False,
|
|
1045
|
+
error=traceback.format_exc())
|
|
1046
|
+
raise
|
|
1047
|
+
|
|
1048
|
+
@staticmethod
|
|
1049
|
+
def _extract_annotations_from_file(file_path, from_format, item):
|
|
1050
|
+
item_id = None
|
|
1051
|
+
annotations = None
|
|
1052
|
+
if isinstance(file_path, dict):
|
|
1053
|
+
annotations = file_path['annotations']
|
|
1054
|
+
elif isinstance(file_path, str) and os.path.isfile(file_path):
|
|
1055
|
+
with open(file_path, "r") as f:
|
|
1056
|
+
if file_path.endswith(".json"):
|
|
1057
|
+
annotations = json.load(f)
|
|
1058
|
+
if from_format.lower() == AnnotationFormat.DATALOOP:
|
|
1059
|
+
if item is None:
|
|
1060
|
+
item_id = annotations.get('_id', annotations.get('id', None))
|
|
1061
|
+
annotations = annotations["annotations"]
|
|
1062
|
+
elif from_format.lower() == AnnotationFormat.YOLO:
|
|
1063
|
+
annotations = [[float(param.replace('\n', ''))
|
|
1064
|
+
for param in line.strip().split(' ')]
|
|
1065
|
+
for line in f.readlines()]
|
|
1066
|
+
elif file_path.endswith(".xml"):
|
|
1067
|
+
annotations = Et.parse(f)
|
|
1068
|
+
annotations = [e for e in annotations.iter('object')]
|
|
1069
|
+
else:
|
|
1070
|
+
raise NotImplementedError('Unknown file format: {}'.format(file_path))
|
|
1071
|
+
else:
|
|
1072
|
+
raise NotImplementedError('Unknown file_path: {}'.format(file_path))
|
|
1073
|
+
|
|
1074
|
+
return item_id, annotations
|
|
1075
|
+
|
|
1076
|
+
def convert_file(self,
|
|
1077
|
+
to_format: str,
|
|
1078
|
+
from_format: str,
|
|
1079
|
+
file_path: str,
|
|
1080
|
+
save_locally: bool = False,
|
|
1081
|
+
save_to: str = None,
|
|
1082
|
+
conversion_func=None,
|
|
1083
|
+
item=None,
|
|
1084
|
+
pbar=None,
|
|
1085
|
+
upload: bool = False,
|
|
1086
|
+
**_):
|
|
1087
|
+
"""
|
|
1088
|
+
Convert file containing annotations.
|
|
1089
|
+
|
|
1090
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1091
|
+
|
|
1092
|
+
:param str to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1093
|
+
:param str from_format: AnnotationFormat to convert from – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1094
|
+
:param str file_path: path of the file to convert
|
|
1095
|
+
:param tqdm pbar: tqdm object that follows the work (progress bar)
|
|
1096
|
+
:param bool upload: if True upload
|
|
1097
|
+
:param bool save_locally: If True, save locally
|
|
1098
|
+
:param str save_to: path to save the result to
|
|
1099
|
+
:param Callable conversion_func: Custom conversion service
|
|
1100
|
+
:param dtlpy.entities.item.Item item: item entity
|
|
1101
|
+
:return: annotation list, errors
|
|
1102
|
+
"""
|
|
1103
|
+
item_id, annotations = self._extract_annotations_from_file(
|
|
1104
|
+
from_format=from_format,
|
|
1105
|
+
file_path=file_path,
|
|
1106
|
+
item=item
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
converted_annotations = self.convert(
|
|
1110
|
+
to_format=to_format,
|
|
1111
|
+
from_format=from_format,
|
|
1112
|
+
annotations=annotations,
|
|
1113
|
+
conversion_func=conversion_func,
|
|
1114
|
+
item=item
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
annotations_list, errors = self._sort_annotations(annotations=converted_annotations)
|
|
1118
|
+
|
|
1119
|
+
if annotations_list:
|
|
1120
|
+
if save_locally:
|
|
1121
|
+
if item_id is not None:
|
|
1122
|
+
item = self.dataset.items.get(item_id=item_id)
|
|
1123
|
+
filename = os.path.split(file_path)[-1]
|
|
1124
|
+
filename_no_ext = os.path.splitext(filename)[0]
|
|
1125
|
+
save_to = os.path.join(save_to, filename_no_ext)
|
|
1126
|
+
self.save_to_file(save_to=save_to, to_format=to_format, annotations=annotations_list, item=item)
|
|
1127
|
+
elif upload and to_format == AnnotationFormat.DATALOOP:
|
|
1128
|
+
item.annotations.upload(annotations=annotations_list)
|
|
1129
|
+
|
|
1130
|
+
if pbar is not None:
|
|
1131
|
+
pbar.update()
|
|
1132
|
+
|
|
1133
|
+
return annotations_list, errors
|
|
1134
|
+
|
|
1135
|
+
@staticmethod
|
|
1136
|
+
def _sort_annotations(annotations):
|
|
1137
|
+
errors = list()
|
|
1138
|
+
success = list()
|
|
1139
|
+
|
|
1140
|
+
for ann in annotations:
|
|
1141
|
+
if isinstance(ann, str) and 'Error converting annotation' in ann:
|
|
1142
|
+
errors.append(ann)
|
|
1143
|
+
else:
|
|
1144
|
+
success.append(ann)
|
|
1145
|
+
|
|
1146
|
+
return success, errors
|
|
1147
|
+
|
|
1148
|
+
def save_to_file(self, save_to, to_format, annotations, item=None):
|
|
1149
|
+
"""
|
|
1150
|
+
Save annotations to a file.
|
|
1151
|
+
|
|
1152
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1153
|
+
|
|
1154
|
+
:param str save_to: path to save the result to
|
|
1155
|
+
:param to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1156
|
+
:param list annotations: annotation list to convert
|
|
1157
|
+
:param dtlpy.entities.item.Item item: item entity
|
|
1158
|
+
"""
|
|
1159
|
+
# what file format
|
|
1160
|
+
if self.save_to_format is None:
|
|
1161
|
+
if to_format.lower() in [AnnotationFormat.DATALOOP, AnnotationFormat.COCO]:
|
|
1162
|
+
self.save_to_format = 'json'
|
|
1163
|
+
elif to_format.lower() in [AnnotationFormat.YOLO]:
|
|
1164
|
+
self.save_to_format = 'txt'
|
|
1165
|
+
else:
|
|
1166
|
+
self.save_to_format = 'xml'
|
|
1167
|
+
|
|
1168
|
+
# save
|
|
1169
|
+
# JSON #
|
|
1170
|
+
if self.save_to_format == 'json':
|
|
1171
|
+
# save json
|
|
1172
|
+
save_to = save_to + '.json'
|
|
1173
|
+
with open(save_to, "w") as f:
|
|
1174
|
+
json.dump(annotations, f, indent=2)
|
|
1175
|
+
|
|
1176
|
+
# TXT #
|
|
1177
|
+
elif self.save_to_format == 'txt':
|
|
1178
|
+
# save txt
|
|
1179
|
+
save_to = save_to + '.txt'
|
|
1180
|
+
with open(save_to, "w") as f:
|
|
1181
|
+
for ann in annotations:
|
|
1182
|
+
if ann is not None:
|
|
1183
|
+
f.write(' '.join([str(x) for x in ann]) + '\n')
|
|
1184
|
+
|
|
1185
|
+
# XML #
|
|
1186
|
+
elif self.save_to_format == 'xml':
|
|
1187
|
+
item = Converter.__get_item_shape(item=item)
|
|
1188
|
+
depth = item.metadata['system'].get('channels', 3)
|
|
1189
|
+
output_annotation = {
|
|
1190
|
+
'path': item.filename,
|
|
1191
|
+
'filename': os.path.basename(item.filename),
|
|
1192
|
+
'folder': os.path.basename(os.path.dirname(item.filename)),
|
|
1193
|
+
'width': item.width,
|
|
1194
|
+
'height': item.height,
|
|
1195
|
+
'depth': depth,
|
|
1196
|
+
'database': 'Unknown',
|
|
1197
|
+
'segmented': 0,
|
|
1198
|
+
'objects': annotations
|
|
1199
|
+
}
|
|
1200
|
+
save_to = save_to + '.xml'
|
|
1201
|
+
environment = Environment(loader=PackageLoader('dtlpy', 'assets'),
|
|
1202
|
+
keep_trailing_newline=True)
|
|
1203
|
+
annotation_template = environment.get_template(self.xml_template_path)
|
|
1204
|
+
with open(save_to, 'w') as file:
|
|
1205
|
+
content = annotation_template.render(**output_annotation)
|
|
1206
|
+
file.write(content)
|
|
1207
|
+
else:
|
|
1208
|
+
raise exceptions.PlatformException('400', 'Unknown file format to save to')
|
|
1209
|
+
|
|
1210
|
+
def _check_formats(self, from_format, to_format, conversion_func):
|
|
1211
|
+
if from_format.lower() not in self.known_formats and conversion_func is None:
|
|
1212
|
+
raise exceptions.PlatformException(
|
|
1213
|
+
"400",
|
|
1214
|
+
"Unknown annotation format: {}, possible formats: {}".format(
|
|
1215
|
+
from_format, self.known_formats
|
|
1216
|
+
),
|
|
1217
|
+
)
|
|
1218
|
+
if to_format.lower() not in self.known_formats and conversion_func is None:
|
|
1219
|
+
raise exceptions.PlatformException(
|
|
1220
|
+
"400",
|
|
1221
|
+
"Unknown annotation format: {}, possible formats: {}".format(
|
|
1222
|
+
to_format, self.known_formats
|
|
1223
|
+
),
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
def convert(self,
|
|
1227
|
+
annotations,
|
|
1228
|
+
from_format: str,
|
|
1229
|
+
to_format: str,
|
|
1230
|
+
conversion_func=None,
|
|
1231
|
+
item=None):
|
|
1232
|
+
"""
|
|
1233
|
+
Convert annotation list or single annotation.
|
|
1234
|
+
|
|
1235
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1236
|
+
|
|
1237
|
+
:param dtlpy.entities.item.Item item: item entity
|
|
1238
|
+
:param list or AnnotationCollection annotations: annotations list to convert
|
|
1239
|
+
:param str from_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1240
|
+
:param str to_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1241
|
+
:param Callable conversion_func: Custom conversion service
|
|
1242
|
+
:return: the annotations
|
|
1243
|
+
"""
|
|
1244
|
+
# check known format
|
|
1245
|
+
self._check_formats(from_format=from_format, to_format=to_format, conversion_func=conversion_func)
|
|
1246
|
+
|
|
1247
|
+
# check annotations param type
|
|
1248
|
+
if isinstance(annotations, entities.AnnotationCollection):
|
|
1249
|
+
annotations = annotations.annotations
|
|
1250
|
+
if not isinstance(annotations, list):
|
|
1251
|
+
annotations = [annotations]
|
|
1252
|
+
|
|
1253
|
+
if AnnotationFormat.DATALOOP not in [to_format, from_format]:
|
|
1254
|
+
raise exceptions.PlatformException(
|
|
1255
|
+
"400", "Can only convert from or to dataloop format"
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
# call method
|
|
1259
|
+
if conversion_func is None:
|
|
1260
|
+
if from_format == AnnotationFormat.DATALOOP:
|
|
1261
|
+
method = self.converter_dict[to_format]["to"]
|
|
1262
|
+
else:
|
|
1263
|
+
method = self.converter_dict[from_format]["from"]
|
|
1264
|
+
else:
|
|
1265
|
+
method = self.custom_format
|
|
1266
|
+
|
|
1267
|
+
# run all annotations
|
|
1268
|
+
for i_annotation, annotation in enumerate(annotations):
|
|
1269
|
+
self._convert_single(
|
|
1270
|
+
annotation=annotation,
|
|
1271
|
+
i_annotation=i_annotation,
|
|
1272
|
+
conversion_func=conversion_func,
|
|
1273
|
+
annotations=annotations,
|
|
1274
|
+
from_format=AnnotationFormat.DATALOOP,
|
|
1275
|
+
item=item,
|
|
1276
|
+
method=method
|
|
1277
|
+
)
|
|
1278
|
+
return annotations
|
|
1279
|
+
|
|
1280
|
+
@staticmethod
|
|
1281
|
+
def _convert_single(method, **kwargs):
|
|
1282
|
+
annotations = kwargs.get('annotations', None)
|
|
1283
|
+
i_annotation = kwargs.get('i_annotation', None)
|
|
1284
|
+
try:
|
|
1285
|
+
ann = method(**kwargs)
|
|
1286
|
+
if annotations is not None and isinstance(i_annotation, int) and ann is not None:
|
|
1287
|
+
annotations[i_annotation] = ann
|
|
1288
|
+
except Exception:
|
|
1289
|
+
if annotations is not None and isinstance(i_annotation, int):
|
|
1290
|
+
annotations[i_annotation] = 'Error converting annotation: {}'.format(traceback.format_exc())
|
|
1291
|
+
else:
|
|
1292
|
+
raise
|
|
1293
|
+
|
|
1294
|
+
@staticmethod
|
|
1295
|
+
def from_voc(annotation, **_):
|
|
1296
|
+
"""
|
|
1297
|
+
Convert from VOC format to DATALOOP format. Use this as conversion_func for functions that ask for this param.
|
|
1298
|
+
|
|
1299
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1300
|
+
|
|
1301
|
+
:param annotation: annotations to convert
|
|
1302
|
+
:return: converted Annotation entity
|
|
1303
|
+
:rtype: dtlpy.entities.annotation.Annotation
|
|
1304
|
+
"""
|
|
1305
|
+
bndbox = annotation.find('bndbox')
|
|
1306
|
+
|
|
1307
|
+
if bndbox is None:
|
|
1308
|
+
raise Exception('No bndbox field found in annotation object')
|
|
1309
|
+
|
|
1310
|
+
bottom = float(bndbox.find('ymax').text)
|
|
1311
|
+
top = float(bndbox.find('ymin').text)
|
|
1312
|
+
left = float(bndbox.find('xmin').text)
|
|
1313
|
+
right = float(bndbox.find('xmax').text)
|
|
1314
|
+
label = annotation.find('name').text
|
|
1315
|
+
|
|
1316
|
+
if annotation.find('segmented'):
|
|
1317
|
+
if annotation.find('segmented') == '1':
|
|
1318
|
+
logger.warning('Only bounding box conversion is supported in voc format. Segmentation will be ignored.')
|
|
1319
|
+
|
|
1320
|
+
attributes = list(annotation)
|
|
1321
|
+
attrs = dict()
|
|
1322
|
+
for attribute in attributes:
|
|
1323
|
+
if attribute.tag not in ['bndbox', 'name'] and len(list(attribute)) == 0:
|
|
1324
|
+
if attribute.text is not None:
|
|
1325
|
+
if attribute.tag == 'attributes':
|
|
1326
|
+
attribute_value = eval(attribute.text)
|
|
1327
|
+
if isinstance(attribute_value, list):
|
|
1328
|
+
attrs = {attribute_value[i]: i for i in range(len(attribute_value))}
|
|
1329
|
+
else:
|
|
1330
|
+
attrs.update(attribute_value)
|
|
1331
|
+
else:
|
|
1332
|
+
try:
|
|
1333
|
+
attr_value = eval(attribute.text)
|
|
1334
|
+
except:
|
|
1335
|
+
attr_value = attribute.text
|
|
1336
|
+
attrs[attribute.tag] = attr_value
|
|
1337
|
+
|
|
1338
|
+
ann_def = entities.Box(label=label, top=top, bottom=bottom, left=left, right=right)
|
|
1339
|
+
new_annotation = entities.Annotation.new(annotation_definition=ann_def)
|
|
1340
|
+
if attrs is not None:
|
|
1341
|
+
try:
|
|
1342
|
+
new_annotation.attributes = attrs
|
|
1343
|
+
except:
|
|
1344
|
+
new_annotation.attributes = list(attrs.keys())
|
|
1345
|
+
return new_annotation
|
|
1346
|
+
|
|
1347
|
+
def from_yolo(self, annotation, item=None, **kwargs):
|
|
1348
|
+
"""
|
|
1349
|
+
Convert from YOLO format to DATALOOP format. Use this as conversion_func param for functions that ask for this param.
|
|
1350
|
+
|
|
1351
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1352
|
+
|
|
1353
|
+
:param annotation: annotations to convert
|
|
1354
|
+
:param dtlpy.entities.item.Item item: item entity
|
|
1355
|
+
:param kwargs: additional params
|
|
1356
|
+
:return: converted Annotation entity
|
|
1357
|
+
:rtype: dtlpy.entities.annotation.Annotation
|
|
1358
|
+
"""
|
|
1359
|
+
(label_id, x, y, w, h) = annotation
|
|
1360
|
+
label_id = int(label_id)
|
|
1361
|
+
|
|
1362
|
+
item = Converter.__get_item_shape(item=item)
|
|
1363
|
+
height = kwargs.get('height', None)
|
|
1364
|
+
width = kwargs.get('width', None)
|
|
1365
|
+
|
|
1366
|
+
if height is None or width is None:
|
|
1367
|
+
if item is None or item.width is None or item.height is None:
|
|
1368
|
+
raise Exception('Need item width and height in order to convert yolo annotation to dataloop')
|
|
1369
|
+
height = item.height
|
|
1370
|
+
width = item.width
|
|
1371
|
+
|
|
1372
|
+
x_center = x * width
|
|
1373
|
+
y_center = y * height
|
|
1374
|
+
w = w * width
|
|
1375
|
+
h = h * height
|
|
1376
|
+
|
|
1377
|
+
top = y_center - (h / 2)
|
|
1378
|
+
bottom = y_center + (h / 2)
|
|
1379
|
+
left = x_center - (w / 2)
|
|
1380
|
+
right = x_center + (w / 2)
|
|
1381
|
+
|
|
1382
|
+
label = self._label_by_category_id(category_id=label_id)
|
|
1383
|
+
|
|
1384
|
+
ann_def = entities.Box(label=label, top=top, bottom=bottom, left=left, right=right)
|
|
1385
|
+
return entities.Annotation.new(annotation_definition=ann_def, item=item)
|
|
1386
|
+
|
|
1387
|
+
def to_yolo(self, annotation, item=None, **_):
|
|
1388
|
+
"""
|
|
1389
|
+
Convert from DATALOOP format to YOLO format. Use this as conversion_func param for functions that ask for this param.
|
|
1390
|
+
|
|
1391
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1392
|
+
|
|
1393
|
+
:param dtlpy.entities.annotation.Annotation or dict annotation: annotations to convert
|
|
1394
|
+
:param dtlpy.entities.item.Item item: item entity
|
|
1395
|
+
:param **_: additional params
|
|
1396
|
+
:return: converted Annotation
|
|
1397
|
+
:rtype: tuple
|
|
1398
|
+
"""
|
|
1399
|
+
if not isinstance(annotation, entities.Annotation):
|
|
1400
|
+
if item is None:
|
|
1401
|
+
item = self.dataset.items.get(item_id=annotation['itemId'])
|
|
1402
|
+
annotation = entities.Annotation.from_json(_json=annotation, item=item)
|
|
1403
|
+
elif item is None:
|
|
1404
|
+
item = annotation.item
|
|
1405
|
+
|
|
1406
|
+
if annotation.type != "box":
|
|
1407
|
+
raise Exception('Only box annotations can be converted')
|
|
1408
|
+
|
|
1409
|
+
item = Converter.__get_item_shape(item=item)
|
|
1410
|
+
|
|
1411
|
+
if item is None or item.width is None or item.height is None:
|
|
1412
|
+
raise Exception("Cannot get item's width and height")
|
|
1413
|
+
|
|
1414
|
+
width, height = (item.width, item.height)
|
|
1415
|
+
if item.system.get('exif', {}).get('Orientation', 0) in [5, 6, 7, 8]:
|
|
1416
|
+
width, height = (item.height, item.width)
|
|
1417
|
+
|
|
1418
|
+
dw = 1.0 / width
|
|
1419
|
+
dh = 1.0 / height
|
|
1420
|
+
x = (annotation.left + annotation.right) / 2.0
|
|
1421
|
+
y = (annotation.top + annotation.bottom) / 2.0
|
|
1422
|
+
w = annotation.right - annotation.left
|
|
1423
|
+
h = annotation.bottom - annotation.top
|
|
1424
|
+
x = x * dw
|
|
1425
|
+
w = w * dw
|
|
1426
|
+
y = y * dh
|
|
1427
|
+
h = h * dh
|
|
1428
|
+
|
|
1429
|
+
label_id = self.labels[annotation.label]
|
|
1430
|
+
|
|
1431
|
+
ann = (label_id, x, y, w, h)
|
|
1432
|
+
return ann
|
|
1433
|
+
|
|
1434
|
+
def _label_by_category_id(self, category_id):
|
|
1435
|
+
if len(self.labels) > 0:
|
|
1436
|
+
for label_name, label_index in self.labels.items():
|
|
1437
|
+
if label_index == category_id:
|
|
1438
|
+
return label_name
|
|
1439
|
+
raise Exception('label category id not found: {}'.format(category_id))
|
|
1440
|
+
|
|
1441
|
+
def from_coco(self, annotation, **kwargs):
|
|
1442
|
+
"""
|
|
1443
|
+
Convert from COCO format to DATALOOP format. Use this as conversion_func param for functions that ask for this param.
|
|
1444
|
+
|
|
1445
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1446
|
+
|
|
1447
|
+
:param annotation: annotations to convert
|
|
1448
|
+
:param kwargs: additional params
|
|
1449
|
+
:return: converted Annotation entity
|
|
1450
|
+
:rtype: dtlpy.entities.annotation.Annotation
|
|
1451
|
+
"""
|
|
1452
|
+
item = kwargs.get('item', None)
|
|
1453
|
+
_id = annotation.get('id', None)
|
|
1454
|
+
category_id = annotation.get('category_id', None)
|
|
1455
|
+
segmentation = annotation.get('segmentation', None)
|
|
1456
|
+
iscrowd = annotation.get('iscrowd', None)
|
|
1457
|
+
label = self._label_by_category_id(category_id=category_id)
|
|
1458
|
+
ann_def = None
|
|
1459
|
+
|
|
1460
|
+
if hasattr(self, '_only_bbox') and self._only_bbox:
|
|
1461
|
+
bbox = annotation.get('bbox', None)
|
|
1462
|
+
left = bbox[0]
|
|
1463
|
+
top = bbox[1]
|
|
1464
|
+
right = left + bbox[2]
|
|
1465
|
+
bottom = top + bbox[3]
|
|
1466
|
+
|
|
1467
|
+
ann_def = entities.Box(top=top, left=left, bottom=bottom, right=right, label=label)
|
|
1468
|
+
else:
|
|
1469
|
+
if iscrowd is not None and int(iscrowd) == 1:
|
|
1470
|
+
ann_def = entities.Segmentation(label=label, geo=COCOUtils.rle_to_binary_mask(rle=segmentation))
|
|
1471
|
+
else:
|
|
1472
|
+
if segmentation is not None and len(segmentation) == 1:
|
|
1473
|
+
segmentation = segmentation[0]
|
|
1474
|
+
if segmentation:
|
|
1475
|
+
ann_def = entities.Polygon(label=label,
|
|
1476
|
+
geo=COCOUtils.rle_to_binary_polygon(segmentation=segmentation))
|
|
1477
|
+
|
|
1478
|
+
elif segmentation is not None and len(segmentation) > 1:
|
|
1479
|
+
# TODO - support conversion of split annotations
|
|
1480
|
+
raise exceptions.PlatformException('400',
|
|
1481
|
+
'unable to convert.'
|
|
1482
|
+
'Converter does not support split annotations: {}'.format(
|
|
1483
|
+
_id))
|
|
1484
|
+
if not segmentation:
|
|
1485
|
+
bbox = annotation.get('bbox', None)
|
|
1486
|
+
if bbox:
|
|
1487
|
+
left = bbox[0]
|
|
1488
|
+
top = bbox[1]
|
|
1489
|
+
right = left + bbox[2]
|
|
1490
|
+
bottom = top + bbox[3]
|
|
1491
|
+
ann_def = entities.Box(top=top, left=left, bottom=bottom, right=right, label=label)
|
|
1492
|
+
else:
|
|
1493
|
+
raise Exception('Unable to convert annotation, not coordinates')
|
|
1494
|
+
|
|
1495
|
+
return entities.Annotation.new(annotation_definition=ann_def, item=item)
|
|
1496
|
+
|
|
1497
|
+
@staticmethod
|
|
1498
|
+
def to_coco(annotation, item=None, **_):
|
|
1499
|
+
"""
|
|
1500
|
+
Convert from DATALOOP format to COCO format. Use this as conversion_func param for functions that ask for this param.
|
|
1501
|
+
|
|
1502
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1503
|
+
|
|
1504
|
+
:param dtlpy.entities.annotation.Annotation or dict annotation: annotations to convert
|
|
1505
|
+
:param dtlpy.entities.item.Item item: item entity
|
|
1506
|
+
:param **_: additional params
|
|
1507
|
+
:return: converted Annotation
|
|
1508
|
+
:rtype: dict
|
|
1509
|
+
"""
|
|
1510
|
+
item = Converter.__get_item_shape(item=item)
|
|
1511
|
+
height = item.height if item is not None else None
|
|
1512
|
+
width = item.width if item is not None else None
|
|
1513
|
+
|
|
1514
|
+
if not isinstance(annotation, entities.Annotation):
|
|
1515
|
+
annotation = entities.Annotation.from_json(annotation, item=item)
|
|
1516
|
+
area = 0
|
|
1517
|
+
iscrowd = 0
|
|
1518
|
+
segmentation = [[]]
|
|
1519
|
+
if annotation.type == 'polyline':
|
|
1520
|
+
raise Exception('Unable to convert annotation of type "polyline" to coco')
|
|
1521
|
+
|
|
1522
|
+
if annotation.type in ['binary', 'segment']:
|
|
1523
|
+
if height is None or width is None:
|
|
1524
|
+
raise Exception(
|
|
1525
|
+
'Item must have height and width to convert {!r} annotation to coco'.format(annotation.type))
|
|
1526
|
+
|
|
1527
|
+
# build annotation
|
|
1528
|
+
keypoints = None
|
|
1529
|
+
if annotation.type in ['binary', 'box', 'segment', 'pose']:
|
|
1530
|
+
x = annotation.left
|
|
1531
|
+
y = annotation.top
|
|
1532
|
+
w = annotation.right - x
|
|
1533
|
+
h = annotation.bottom - y
|
|
1534
|
+
area = float(h * w)
|
|
1535
|
+
if annotation.type == 'binary':
|
|
1536
|
+
# segmentation = COCOUtils.binary_mask_to_rle(binary_mask=annotation.geo, height=height, width=width)
|
|
1537
|
+
segmentation = COCOUtils.binary_mask_to_rle_encode(binary_mask=annotation.geo)
|
|
1538
|
+
area = int(annotation.geo.sum())
|
|
1539
|
+
iscrowd = 1
|
|
1540
|
+
elif annotation.type in ['segment']:
|
|
1541
|
+
segmentation, area = COCOUtils.polygon_to_rle(geo=annotation.geo, height=height, width=width)
|
|
1542
|
+
elif annotation.type == 'pose':
|
|
1543
|
+
keypoints = list()
|
|
1544
|
+
for point in annotation.annotation_definition.points:
|
|
1545
|
+
keypoints.append(point.x)
|
|
1546
|
+
keypoints.append(point.y)
|
|
1547
|
+
if isinstance(point.attributes, list):
|
|
1548
|
+
if 'visible' in point.attributes and \
|
|
1549
|
+
("not-visible" in point.attributes or 'not_visible' in point.attributes):
|
|
1550
|
+
keypoints.append(0)
|
|
1551
|
+
elif 'not-visible' in point.attributes or 'not_visible' in point.attributes:
|
|
1552
|
+
keypoints.append(1)
|
|
1553
|
+
elif 'visible' in point.attributes:
|
|
1554
|
+
keypoints.append(2)
|
|
1555
|
+
else:
|
|
1556
|
+
keypoints.append(0)
|
|
1557
|
+
else:
|
|
1558
|
+
list_attributes = list(point.attributes.values())
|
|
1559
|
+
if 'Visible' in list_attributes:
|
|
1560
|
+
keypoints.append(2)
|
|
1561
|
+
else:
|
|
1562
|
+
keypoints.append(0)
|
|
1563
|
+
x_points = keypoints[0::3]
|
|
1564
|
+
y_points = keypoints[1::3]
|
|
1565
|
+
x0, x1, y0, y1 = np.min(x_points), np.max(x_points), np.min(y_points), np.max(y_points)
|
|
1566
|
+
area = float((x1 - x0) * (y1 - y0))
|
|
1567
|
+
else:
|
|
1568
|
+
raise Exception('Unable to convert annotation of type {} to coco'.format(annotation.type))
|
|
1569
|
+
|
|
1570
|
+
ann = dict()
|
|
1571
|
+
ann['bbox'] = [float(x), float(y), float(w), float(h)]
|
|
1572
|
+
ann["segmentation"] = segmentation
|
|
1573
|
+
ann["area"] = area
|
|
1574
|
+
ann["iscrowd"] = iscrowd
|
|
1575
|
+
if keypoints is not None:
|
|
1576
|
+
ann["keypoints"] = keypoints
|
|
1577
|
+
return ann
|
|
1578
|
+
|
|
1579
|
+
@staticmethod
|
|
1580
|
+
def to_voc(annotation, item=None, **_):
|
|
1581
|
+
"""
|
|
1582
|
+
Convert from DATALOOP format to VOC format. Use this as conversion_func param for functions that ask for this param.
|
|
1583
|
+
|
|
1584
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1585
|
+
|
|
1586
|
+
:param dtlpy.entities.annotation.Annotation or dict annotation: annotations to convert
|
|
1587
|
+
:param dtlpy.entities.item.Item item: item entity
|
|
1588
|
+
:param **_: additional params
|
|
1589
|
+
:return: converted Annotation
|
|
1590
|
+
:rtype: dict
|
|
1591
|
+
"""
|
|
1592
|
+
if not isinstance(annotation, entities.Annotation):
|
|
1593
|
+
annotation = entities.Annotation.from_json(annotation, item=item)
|
|
1594
|
+
|
|
1595
|
+
if annotation.type != 'box':
|
|
1596
|
+
raise exceptions.PlatformException('400', 'Annotation must be of type box')
|
|
1597
|
+
|
|
1598
|
+
label = annotation.label
|
|
1599
|
+
|
|
1600
|
+
attributes = dict()
|
|
1601
|
+
if annotation.attributes is not None:
|
|
1602
|
+
if isinstance(annotation.attributes, dict) and len(annotation.attributes) > 0:
|
|
1603
|
+
attributes = annotation.attributes
|
|
1604
|
+
elif isinstance(annotation.attributes, list) and len(annotation.attributes) > 0:
|
|
1605
|
+
attributes = {annotation.attributes[i]: i for i in range(len(annotation.attributes))}
|
|
1606
|
+
|
|
1607
|
+
left = annotation.left
|
|
1608
|
+
top = annotation.top
|
|
1609
|
+
right = annotation.right
|
|
1610
|
+
bottom = annotation.bottom
|
|
1611
|
+
|
|
1612
|
+
ann = {'name': label,
|
|
1613
|
+
'xmin': left,
|
|
1614
|
+
'ymin': top,
|
|
1615
|
+
'xmax': right,
|
|
1616
|
+
'ymax': bottom
|
|
1617
|
+
}
|
|
1618
|
+
if attributes:
|
|
1619
|
+
ann['attributes'] = attributes
|
|
1620
|
+
return ann
|
|
1621
|
+
|
|
1622
|
+
@staticmethod
|
|
1623
|
+
def custom_format(annotation,
|
|
1624
|
+
conversion_func,
|
|
1625
|
+
i_annotation=None,
|
|
1626
|
+
annotations=None,
|
|
1627
|
+
from_format=None,
|
|
1628
|
+
item=None, **_):
|
|
1629
|
+
"""
|
|
1630
|
+
Custom convert function.
|
|
1631
|
+
|
|
1632
|
+
**Prerequisites**: You must be an *owner* or *developer* to use this method.
|
|
1633
|
+
|
|
1634
|
+
:param dtlpy.entities.annotation.Annotation or dict annotation: annotations to convert
|
|
1635
|
+
:param Callable conversion_func: Custom conversion service
|
|
1636
|
+
:param int i_annotation: annotation index
|
|
1637
|
+
:param list annotations: list of annotations
|
|
1638
|
+
param str from_format: AnnotationFormat to convert to – AnnotationFormat.COCO, AnnotationFormat.YOLO, AnnotationFormat.VOC, AnnotationFormat.DATALOOP
|
|
1639
|
+
:param dtlpy.entities.item.Item item: item entity
|
|
1640
|
+
:return: converted Annotation
|
|
1641
|
+
"""
|
|
1642
|
+
if from_format == AnnotationFormat.DATALOOP and isinstance(annotation, dict):
|
|
1643
|
+
annotation = entities.Annotation.from_json(_json=annotation, item=item)
|
|
1644
|
+
|
|
1645
|
+
ann = conversion_func(annotation, item)
|
|
1646
|
+
|
|
1647
|
+
if isinstance(annotations[i_annotation], entities.Annotation) and annotations[i_annotation].item is None:
|
|
1648
|
+
ann.item = item
|
|
1649
|
+
|
|
1650
|
+
return ann
|