dtlpy 1.114.17__py3-none-any.whl → 1.116.6__py3-none-any.whl

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