dtlpy 1.113.10__py3-none-any.whl → 1.114.13__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.
Files changed (243) hide show
  1. dtlpy/__init__.py +488 -488
  2. dtlpy/__version__.py +1 -1
  3. dtlpy/assets/__init__.py +26 -26
  4. dtlpy/assets/__pycache__/__init__.cpython-38.pyc +0 -0
  5. dtlpy/assets/code_server/config.yaml +2 -2
  6. dtlpy/assets/code_server/installation.sh +24 -24
  7. dtlpy/assets/code_server/launch.json +13 -13
  8. dtlpy/assets/code_server/settings.json +2 -2
  9. dtlpy/assets/main.py +53 -53
  10. dtlpy/assets/main_partial.py +18 -18
  11. dtlpy/assets/mock.json +11 -11
  12. dtlpy/assets/model_adapter.py +83 -83
  13. dtlpy/assets/package.json +61 -61
  14. dtlpy/assets/package_catalog.json +29 -29
  15. dtlpy/assets/package_gitignore +307 -307
  16. dtlpy/assets/service_runners/__init__.py +33 -33
  17. dtlpy/assets/service_runners/converter.py +96 -96
  18. dtlpy/assets/service_runners/multi_method.py +49 -49
  19. dtlpy/assets/service_runners/multi_method_annotation.py +54 -54
  20. dtlpy/assets/service_runners/multi_method_dataset.py +55 -55
  21. dtlpy/assets/service_runners/multi_method_item.py +52 -52
  22. dtlpy/assets/service_runners/multi_method_json.py +52 -52
  23. dtlpy/assets/service_runners/single_method.py +37 -37
  24. dtlpy/assets/service_runners/single_method_annotation.py +43 -43
  25. dtlpy/assets/service_runners/single_method_dataset.py +43 -43
  26. dtlpy/assets/service_runners/single_method_item.py +41 -41
  27. dtlpy/assets/service_runners/single_method_json.py +42 -42
  28. dtlpy/assets/service_runners/single_method_multi_input.py +45 -45
  29. dtlpy/assets/voc_annotation_template.xml +23 -23
  30. dtlpy/caches/base_cache.py +32 -32
  31. dtlpy/caches/cache.py +473 -473
  32. dtlpy/caches/dl_cache.py +201 -201
  33. dtlpy/caches/filesystem_cache.py +89 -89
  34. dtlpy/caches/redis_cache.py +84 -84
  35. dtlpy/dlp/__init__.py +20 -20
  36. dtlpy/dlp/cli_utilities.py +367 -367
  37. dtlpy/dlp/command_executor.py +764 -764
  38. dtlpy/dlp/dlp +1 -1
  39. dtlpy/dlp/dlp.bat +1 -1
  40. dtlpy/dlp/dlp.py +128 -128
  41. dtlpy/dlp/parser.py +651 -651
  42. dtlpy/entities/__init__.py +83 -83
  43. dtlpy/entities/analytic.py +311 -311
  44. dtlpy/entities/annotation.py +1879 -1879
  45. dtlpy/entities/annotation_collection.py +699 -699
  46. dtlpy/entities/annotation_definitions/__init__.py +20 -20
  47. dtlpy/entities/annotation_definitions/base_annotation_definition.py +100 -100
  48. dtlpy/entities/annotation_definitions/box.py +195 -195
  49. dtlpy/entities/annotation_definitions/classification.py +67 -67
  50. dtlpy/entities/annotation_definitions/comparison.py +72 -72
  51. dtlpy/entities/annotation_definitions/cube.py +204 -204
  52. dtlpy/entities/annotation_definitions/cube_3d.py +149 -149
  53. dtlpy/entities/annotation_definitions/description.py +32 -32
  54. dtlpy/entities/annotation_definitions/ellipse.py +124 -124
  55. dtlpy/entities/annotation_definitions/free_text.py +62 -62
  56. dtlpy/entities/annotation_definitions/gis.py +69 -69
  57. dtlpy/entities/annotation_definitions/note.py +139 -139
  58. dtlpy/entities/annotation_definitions/point.py +117 -117
  59. dtlpy/entities/annotation_definitions/polygon.py +182 -182
  60. dtlpy/entities/annotation_definitions/polyline.py +111 -111
  61. dtlpy/entities/annotation_definitions/pose.py +92 -92
  62. dtlpy/entities/annotation_definitions/ref_image.py +86 -86
  63. dtlpy/entities/annotation_definitions/segmentation.py +240 -240
  64. dtlpy/entities/annotation_definitions/subtitle.py +34 -34
  65. dtlpy/entities/annotation_definitions/text.py +85 -85
  66. dtlpy/entities/annotation_definitions/undefined_annotation.py +74 -74
  67. dtlpy/entities/app.py +220 -220
  68. dtlpy/entities/app_module.py +107 -107
  69. dtlpy/entities/artifact.py +174 -174
  70. dtlpy/entities/assignment.py +399 -399
  71. dtlpy/entities/base_entity.py +214 -214
  72. dtlpy/entities/bot.py +113 -113
  73. dtlpy/entities/codebase.py +296 -296
  74. dtlpy/entities/collection.py +38 -38
  75. dtlpy/entities/command.py +169 -169
  76. dtlpy/entities/compute.py +442 -442
  77. dtlpy/entities/dataset.py +1285 -1285
  78. dtlpy/entities/directory_tree.py +44 -44
  79. dtlpy/entities/dpk.py +470 -470
  80. dtlpy/entities/driver.py +222 -222
  81. dtlpy/entities/execution.py +397 -397
  82. dtlpy/entities/feature.py +124 -124
  83. dtlpy/entities/feature_set.py +145 -145
  84. dtlpy/entities/filters.py +641 -641
  85. dtlpy/entities/gis_item.py +107 -107
  86. dtlpy/entities/integration.py +184 -184
  87. dtlpy/entities/item.py +953 -953
  88. dtlpy/entities/label.py +123 -123
  89. dtlpy/entities/links.py +85 -85
  90. dtlpy/entities/message.py +175 -175
  91. dtlpy/entities/model.py +694 -691
  92. dtlpy/entities/node.py +1005 -1005
  93. dtlpy/entities/ontology.py +803 -803
  94. dtlpy/entities/organization.py +287 -287
  95. dtlpy/entities/package.py +657 -657
  96. dtlpy/entities/package_defaults.py +5 -5
  97. dtlpy/entities/package_function.py +185 -185
  98. dtlpy/entities/package_module.py +113 -113
  99. dtlpy/entities/package_slot.py +118 -118
  100. dtlpy/entities/paged_entities.py +290 -267
  101. dtlpy/entities/pipeline.py +593 -593
  102. dtlpy/entities/pipeline_execution.py +279 -279
  103. dtlpy/entities/project.py +394 -394
  104. dtlpy/entities/prompt_item.py +499 -499
  105. dtlpy/entities/recipe.py +301 -301
  106. dtlpy/entities/reflect_dict.py +102 -102
  107. dtlpy/entities/resource_execution.py +138 -138
  108. dtlpy/entities/service.py +958 -958
  109. dtlpy/entities/service_driver.py +117 -117
  110. dtlpy/entities/setting.py +294 -294
  111. dtlpy/entities/task.py +491 -491
  112. dtlpy/entities/time_series.py +143 -143
  113. dtlpy/entities/trigger.py +426 -426
  114. dtlpy/entities/user.py +118 -118
  115. dtlpy/entities/webhook.py +124 -124
  116. dtlpy/examples/__init__.py +19 -19
  117. dtlpy/examples/add_labels.py +135 -135
  118. dtlpy/examples/add_metadata_to_item.py +21 -21
  119. dtlpy/examples/annotate_items_using_model.py +65 -65
  120. dtlpy/examples/annotate_video_using_model_and_tracker.py +75 -75
  121. dtlpy/examples/annotations_convert_to_voc.py +9 -9
  122. dtlpy/examples/annotations_convert_to_yolo.py +9 -9
  123. dtlpy/examples/convert_annotation_types.py +51 -51
  124. dtlpy/examples/converter.py +143 -143
  125. dtlpy/examples/copy_annotations.py +22 -22
  126. dtlpy/examples/copy_folder.py +31 -31
  127. dtlpy/examples/create_annotations.py +51 -51
  128. dtlpy/examples/create_video_annotations.py +83 -83
  129. dtlpy/examples/delete_annotations.py +26 -26
  130. dtlpy/examples/filters.py +113 -113
  131. dtlpy/examples/move_item.py +23 -23
  132. dtlpy/examples/play_video_annotation.py +13 -13
  133. dtlpy/examples/show_item_and_mask.py +53 -53
  134. dtlpy/examples/triggers.py +49 -49
  135. dtlpy/examples/upload_batch_of_items.py +20 -20
  136. dtlpy/examples/upload_items_and_custom_format_annotations.py +55 -55
  137. dtlpy/examples/upload_items_with_modalities.py +43 -43
  138. dtlpy/examples/upload_segmentation_annotations_from_mask_image.py +44 -44
  139. dtlpy/examples/upload_yolo_format_annotations.py +70 -70
  140. dtlpy/exceptions.py +125 -125
  141. dtlpy/miscellaneous/__init__.py +20 -20
  142. dtlpy/miscellaneous/dict_differ.py +95 -95
  143. dtlpy/miscellaneous/git_utils.py +217 -217
  144. dtlpy/miscellaneous/json_utils.py +14 -14
  145. dtlpy/miscellaneous/list_print.py +105 -105
  146. dtlpy/miscellaneous/zipping.py +130 -130
  147. dtlpy/ml/__init__.py +20 -20
  148. dtlpy/ml/base_feature_extractor_adapter.py +27 -27
  149. dtlpy/ml/base_model_adapter.py +945 -940
  150. dtlpy/ml/metrics.py +461 -461
  151. dtlpy/ml/predictions_utils.py +274 -274
  152. dtlpy/ml/summary_writer.py +57 -57
  153. dtlpy/ml/train_utils.py +60 -60
  154. dtlpy/new_instance.py +252 -252
  155. dtlpy/repositories/__init__.py +56 -56
  156. dtlpy/repositories/analytics.py +85 -85
  157. dtlpy/repositories/annotations.py +916 -916
  158. dtlpy/repositories/apps.py +383 -383
  159. dtlpy/repositories/artifacts.py +452 -452
  160. dtlpy/repositories/assignments.py +599 -599
  161. dtlpy/repositories/bots.py +213 -213
  162. dtlpy/repositories/codebases.py +559 -559
  163. dtlpy/repositories/collections.py +332 -348
  164. dtlpy/repositories/commands.py +158 -158
  165. dtlpy/repositories/compositions.py +61 -61
  166. dtlpy/repositories/computes.py +434 -406
  167. dtlpy/repositories/datasets.py +1291 -1291
  168. dtlpy/repositories/downloader.py +895 -895
  169. dtlpy/repositories/dpks.py +433 -433
  170. dtlpy/repositories/drivers.py +266 -266
  171. dtlpy/repositories/executions.py +817 -817
  172. dtlpy/repositories/feature_sets.py +226 -226
  173. dtlpy/repositories/features.py +238 -238
  174. dtlpy/repositories/integrations.py +484 -484
  175. dtlpy/repositories/items.py +909 -915
  176. dtlpy/repositories/messages.py +94 -94
  177. dtlpy/repositories/models.py +877 -867
  178. dtlpy/repositories/nodes.py +80 -80
  179. dtlpy/repositories/ontologies.py +511 -511
  180. dtlpy/repositories/organizations.py +525 -525
  181. dtlpy/repositories/packages.py +1941 -1941
  182. dtlpy/repositories/pipeline_executions.py +448 -448
  183. dtlpy/repositories/pipelines.py +642 -642
  184. dtlpy/repositories/projects.py +539 -539
  185. dtlpy/repositories/recipes.py +399 -399
  186. dtlpy/repositories/resource_executions.py +137 -137
  187. dtlpy/repositories/schema.py +120 -120
  188. dtlpy/repositories/service_drivers.py +213 -213
  189. dtlpy/repositories/services.py +1704 -1704
  190. dtlpy/repositories/settings.py +339 -339
  191. dtlpy/repositories/tasks.py +1124 -1124
  192. dtlpy/repositories/times_series.py +278 -278
  193. dtlpy/repositories/triggers.py +536 -536
  194. dtlpy/repositories/upload_element.py +257 -257
  195. dtlpy/repositories/uploader.py +651 -651
  196. dtlpy/repositories/webhooks.py +249 -249
  197. dtlpy/services/__init__.py +22 -22
  198. dtlpy/services/aihttp_retry.py +131 -131
  199. dtlpy/services/api_client.py +1782 -1782
  200. dtlpy/services/api_reference.py +40 -40
  201. dtlpy/services/async_utils.py +133 -133
  202. dtlpy/services/calls_counter.py +44 -44
  203. dtlpy/services/check_sdk.py +68 -68
  204. dtlpy/services/cookie.py +115 -115
  205. dtlpy/services/create_logger.py +156 -156
  206. dtlpy/services/events.py +84 -84
  207. dtlpy/services/logins.py +235 -235
  208. dtlpy/services/reporter.py +256 -256
  209. dtlpy/services/service_defaults.py +91 -91
  210. dtlpy/utilities/__init__.py +20 -20
  211. dtlpy/utilities/annotations/__init__.py +16 -16
  212. dtlpy/utilities/annotations/annotation_converters.py +269 -269
  213. dtlpy/utilities/base_package_runner.py +264 -264
  214. dtlpy/utilities/converter.py +1650 -1650
  215. dtlpy/utilities/dataset_generators/__init__.py +1 -1
  216. dtlpy/utilities/dataset_generators/dataset_generator.py +670 -670
  217. dtlpy/utilities/dataset_generators/dataset_generator_tensorflow.py +23 -23
  218. dtlpy/utilities/dataset_generators/dataset_generator_torch.py +21 -21
  219. dtlpy/utilities/local_development/__init__.py +1 -1
  220. dtlpy/utilities/local_development/local_session.py +179 -179
  221. dtlpy/utilities/reports/__init__.py +2 -2
  222. dtlpy/utilities/reports/figures.py +343 -343
  223. dtlpy/utilities/reports/report.py +71 -71
  224. dtlpy/utilities/videos/__init__.py +17 -17
  225. dtlpy/utilities/videos/video_player.py +598 -598
  226. dtlpy/utilities/videos/videos.py +470 -470
  227. {dtlpy-1.113.10.data → dtlpy-1.114.13.data}/scripts/dlp +1 -1
  228. dtlpy-1.114.13.data/scripts/dlp.bat +2 -0
  229. {dtlpy-1.113.10.data → dtlpy-1.114.13.data}/scripts/dlp.py +128 -128
  230. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/LICENSE +200 -200
  231. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/METADATA +172 -172
  232. dtlpy-1.114.13.dist-info/RECORD +240 -0
  233. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/WHEEL +1 -1
  234. tests/features/environment.py +551 -550
  235. dtlpy-1.113.10.data/scripts/dlp.bat +0 -2
  236. dtlpy-1.113.10.dist-info/RECORD +0 -244
  237. tests/assets/__init__.py +0 -0
  238. tests/assets/models_flow/__init__.py +0 -0
  239. tests/assets/models_flow/failedmain.py +0 -52
  240. tests/assets/models_flow/main.py +0 -62
  241. tests/assets/models_flow/main_model.py +0 -54
  242. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/entry_points.txt +0 -0
  243. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/top_level.txt +0 -0
@@ -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