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,916 +1,916 @@
1
- from copy import deepcopy
2
- import traceback
3
- import logging
4
- import json
5
- import jwt
6
- import os
7
- from PIL import Image
8
- from io import BytesIO
9
- import base64
10
-
11
- from .. import entities, exceptions, miscellaneous, _api_reference
12
- from ..services.api_client import ApiClient
13
-
14
- logger = logging.getLogger(name='dtlpy')
15
-
16
-
17
- class Annotations:
18
- """
19
- Annotations Repository
20
-
21
- The Annotation class allows you to manage the annotations of data items. For information on annotations explore our
22
- documentation at:
23
- `Classification SDK <https://developers.dataloop.ai/tutorials/annotations_image/classification_point_and_pose/chapter/>`_,
24
- `Annotation Labels and Attributes <https://developers.dataloop.ai/tutorials/data_management/upload_and_manage_annotations/chapter/#set-attributes-on-annotations>`_,
25
- `Show Video with Annotations <https://developers.dataloop.ai/tutorials/annotations_video/video_annotations/chapter/>`_.
26
- """
27
-
28
- def __init__(self, client_api: ApiClient, item=None, dataset=None, dataset_id=None):
29
- self._client_api = client_api
30
- self._item = item
31
- self._dataset = dataset
32
- self._upload_batch_size = 100
33
- if dataset_id is None:
34
- if dataset is not None:
35
- dataset_id = dataset.id
36
- elif item is not None:
37
- dataset_id = item.dataset_id
38
- self._dataset_id = dataset_id
39
-
40
- ############
41
- # entities #
42
- ############
43
- @property
44
- def dataset(self):
45
- if self._dataset is None:
46
- raise exceptions.PlatformException(
47
- error='2001',
48
- message='Missing "dataset". need to set a Dataset entity or use dataset.annotations repository')
49
- assert isinstance(self._dataset, entities.Dataset)
50
- return self._dataset
51
-
52
- @dataset.setter
53
- def dataset(self, dataset: entities.Dataset):
54
- if not isinstance(dataset, entities.Dataset):
55
- raise ValueError('Must input a valid Dataset entity')
56
- self._dataset = dataset
57
-
58
- @property
59
- def item(self):
60
- if self._item is None:
61
- raise exceptions.PlatformException(
62
- error='2001',
63
- message='Missing "item". need to set an Item entity or use item.annotations repository')
64
- assert isinstance(self._item, entities.Item)
65
- return self._item
66
-
67
- @item.setter
68
- def item(self, item: entities.Item):
69
- if not isinstance(item, entities.Item):
70
- raise ValueError('Must input a valid Item entity')
71
- self._item = item
72
-
73
- ###########
74
- # methods #
75
- ###########
76
- @_api_reference.add(path='/annotations/{annotationId}', method='get')
77
- def get(self, annotation_id: str) -> entities.Annotation:
78
- """
79
- Get a single annotation.
80
-
81
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
82
- *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
83
-
84
- :param str annotation_id: The id of the annotation
85
- :return: Annotation object or None
86
- :return: Annotation object or None
87
- :rtype: dtlpy.entities.annotation.Annotation
88
-
89
- **Example**:
90
-
91
- .. code-block:: python
92
-
93
- annotation = item.annotations.get(annotation_id='annotation_id')
94
- """
95
- success, response = self._client_api.gen_request(req_type='get',
96
- path='/annotations/{}'.format(annotation_id))
97
- if success:
98
- annotation = entities.Annotation.from_json(_json=response.json(),
99
- annotations=self,
100
- dataset=self._dataset,
101
- client_api=self._client_api,
102
- item=self._item)
103
- else:
104
- raise exceptions.PlatformException(response)
105
- return annotation
106
-
107
- def _build_entities_from_response(self, response_items):
108
- pool = self._client_api.thread_pools(pool_name='entity.create')
109
- jobs = [None for _ in range(len(response_items))]
110
- # return triggers list
111
- for i_json, _json in enumerate(response_items):
112
- jobs[i_json] = pool.submit(entities.Annotation._protected_from_json,
113
- **{'client_api': self._client_api,
114
- '_json': _json,
115
- 'item': self._item,
116
- 'dataset': self._dataset,
117
- 'annotations': self})
118
-
119
- # get all results
120
- results = [j.result() for j in jobs]
121
- # log errors
122
- _ = [logger.warning(r[1]) for r in results if r[0] is False]
123
- # return good jobs
124
- return miscellaneous.List([r[1] for r in results if r[0] is True])
125
-
126
- def _list(self, filters: entities.Filters):
127
- """
128
- Get a dataset's item list. This is a browsing endpoint. For any given path, item count will be returned.
129
- The user is then expected to perform another request for every folder to actually get its item list.
130
-
131
- :param dtlpy.entities.filters.Filters filters: Filter entity or a dictionary containing filters parameters
132
-
133
- :return: json response
134
- :rtype:
135
- """
136
- # prepare request
137
- success, response = self._client_api.gen_request(req_type="POST",
138
- path="/datasets/{}/query".format(self._dataset_id),
139
- json_req=filters.prepare(),
140
- headers={'user_query': filters._user_query}
141
- )
142
- if not success:
143
- raise exceptions.PlatformException(response)
144
- return response.json()
145
-
146
- @_api_reference.add(path='/datasets/{id}/query', method='post')
147
- def list(self, filters: entities.Filters = None, page_offset: int = None, page_size: int = None):
148
- """
149
- List Annotations of a specific item. You must get the item first and then list the annotations with the desired filters.
150
-
151
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
152
- *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
153
-
154
- :param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filters parameters
155
- :param int page_offset: starting page
156
- :param int page_size: size of page
157
- :return: Pages object
158
- :rtype: dtlpy.entities.paged_entities.PagedEntities
159
-
160
- **Example**:
161
-
162
- .. code-block:: python
163
-
164
- annotations = item.annotations.list(filters=dl.Filters(
165
- resource=dl.FiltersResource.ANNOTATION,
166
- field='type',
167
- values='box'),
168
- page_size=100,
169
- page_offset=0)
170
- """
171
- if self._dataset_id is not None:
172
- if filters is None:
173
- filters = entities.Filters(resource=entities.FiltersResource.ANNOTATION)
174
- filters._user_query = 'false'
175
-
176
- if not filters.resource == entities.FiltersResource.ANNOTATION:
177
- raise exceptions.PlatformException(error='400',
178
- message='Filters resource must to be FiltersResource.ANNOTATION')
179
-
180
- if self._item is not None and not filters.has_field('itemId'):
181
- filters = deepcopy(filters)
182
- filters.page_size = 1000
183
- filters.add(field='itemId', values=self.item.id, method=entities.FiltersMethod.AND)
184
-
185
- # assert type filters
186
- if not isinstance(filters, entities.Filters):
187
- raise exceptions.PlatformException('400', 'Unknown filters type')
188
-
189
- # page size
190
- if page_size is None:
191
- # take from default
192
- page_size = filters.page_size
193
- else:
194
- filters.page_size = page_size
195
-
196
- # page offset
197
- if page_offset is None:
198
- # take from default
199
- page_offset = filters.page
200
- else:
201
- filters.page = page_offset
202
-
203
- paged = entities.PagedEntities(items_repository=self,
204
- filters=filters,
205
- page_offset=page_offset,
206
- page_size=page_size,
207
- client_api=self._client_api)
208
- paged.get_page()
209
-
210
- if self._item is not None:
211
- if paged.total_pages_count > 1:
212
- annotations = list()
213
- for page in paged:
214
- annotations += page
215
- else:
216
- annotations = paged.items
217
- return entities.AnnotationCollection(annotations=annotations, item=self._item)
218
- else:
219
- return paged
220
- else:
221
- raise exceptions.PlatformException('400',
222
- 'Please use item.annotations.list() or dataset.annotations.list() '
223
- 'to perform this action.')
224
-
225
- def show(self,
226
- image=None,
227
- thickness: int = 1,
228
- with_text: bool = False,
229
- height: float = None,
230
- width: float = None,
231
- annotation_format: entities.ViewAnnotationOptions = entities.ViewAnnotationOptions.MASK,
232
- alpha: float = 1):
233
- """
234
- Show annotations. To use this method, you must get the item first and then show the annotations with
235
- the desired filters. The method returns an array showing all the annotations.
236
-
237
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
238
- *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
239
-
240
- :param ndarray image: empty or image to draw on
241
- :param int thickness: optional - line thickness, default=1
242
- :param bool with_text: add label to annotation
243
- :param float height: item height
244
- :param float width: item width
245
- :param str annotation_format: the format that want to show ,options: list(dl.ViewAnnotationOptions)
246
- :param float alpha: opacity value [0 1], default 1
247
- :return: ndarray of the annotations
248
- :rtype: ndarray
249
-
250
- **Example**:
251
-
252
- .. code-block:: python
253
-
254
- image = item.annotations.show(image='nd array',
255
- thickness=1,
256
- with_text=False,
257
- height=100,
258
- width=100,
259
- annotation_format=dl.ViewAnnotationOptions.MASK,
260
- alpha=1)
261
- """
262
- # get item's annotations
263
- annotations = self.list()
264
-
265
- return annotations.show(image=image,
266
- width=width,
267
- height=height,
268
- thickness=thickness,
269
- alpha=alpha,
270
- with_text=with_text,
271
- annotation_format=annotation_format)
272
-
273
- def download(self,
274
- filepath: str,
275
- annotation_format: entities.ViewAnnotationOptions = entities.ViewAnnotationOptions.JSON,
276
- img_filepath: str = None,
277
- height: float = None,
278
- width: float = None,
279
- thickness: int = 1,
280
- with_text: bool = False,
281
- alpha: float = 1):
282
- """
283
- Save annotation to file.
284
-
285
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
286
- *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
287
-
288
- :param str filepath: Target download directory
289
- :param str annotation_format: the format that want to download ,options: list(dl.ViewAnnotationOptions)
290
- :param str img_filepath: img file path - needed for img_mask
291
- :param float height: optional - image height
292
- :param float width: optional - image width
293
- :param int thickness: optional - line thickness, default=1
294
- :param bool with_text: optional - draw annotation with text, default = False
295
- :param float alpha: opacity value [0 1], default 1
296
- :return: file path to where save the annotations
297
- :rtype: str
298
-
299
- **Example**:
300
-
301
- .. code-block:: python
302
-
303
- file_path = item.annotations.download(
304
- filepath='file_path',
305
- annotation_format=dl.ViewAnnotationOptions.MASK,
306
- img_filepath='img_filepath',
307
- height=100,
308
- width=100,
309
- thickness=1,
310
- with_text=False,
311
- alpha=1)
312
- """
313
- # get item's annotations
314
- annotations = self.list()
315
- if 'text' in self.item.metadata.get('system').get('mimetype', '') or 'json' in self.item.metadata.get('system').get('mimetype', ''):
316
- annotation_format = entities.ViewAnnotationOptions.JSON
317
- elif 'audio' not in self.item.metadata.get('system').get('mimetype', ''):
318
- # height/weight
319
- if height is None:
320
- if self.item.height is None:
321
- raise exceptions.PlatformException('400', 'Height must be provided')
322
- height = self.item.height
323
- if width is None:
324
- if self.item.width is None:
325
- raise exceptions.PlatformException('400', 'Width must be provided')
326
- width = self.item.width
327
-
328
- return annotations.download(filepath=filepath,
329
- img_filepath=img_filepath,
330
- width=width,
331
- height=height,
332
- thickness=thickness,
333
- with_text=with_text,
334
- annotation_format=annotation_format,
335
- alpha=alpha)
336
-
337
- def _delete_single_annotation(self, w_annotation_id):
338
- try:
339
- creator = jwt.decode(self._client_api.token, algorithms=['HS256'],
340
- verify=False, options={'verify_signature': False})['email']
341
- payload = {'username': creator}
342
- success, response = self._client_api.gen_request(req_type='delete',
343
- path='/annotations/{}'.format(w_annotation_id),
344
- json_req=payload)
345
-
346
- if not success:
347
- raise exceptions.PlatformException(response)
348
- return success
349
- except Exception:
350
- logger.exception('Failed to delete annotation')
351
- raise
352
-
353
- @_api_reference.add(path='/annotations/{annotationId}', method='delete')
354
- def delete(self, annotation: entities.Annotation = None,
355
- annotation_id: str = None,
356
- filters: entities.Filters = None) -> bool:
357
- """
358
- Remove an annotation from item.
359
-
360
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
361
- *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
362
-
363
- :param dtlpy.entities.annotation.Annotation annotation: Annotation object
364
- :param str annotation_id: The id of the annotation
365
- :param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filters parameters
366
- :return: True/False
367
- :rtype: bool
368
-
369
- **Example**:
370
-
371
- .. code-block:: python
372
-
373
- is_deleted = item.annotations.delete(annotation_id='annotation_id')
374
- """
375
- if annotation is not None:
376
- if isinstance(annotation, entities.Annotation):
377
- annotation_id = annotation.id
378
- elif isinstance(annotation, str) and annotation.lower() == 'all':
379
- if self._item is None:
380
- raise exceptions.PlatformException(error='400',
381
- message='To use "all" option repository must have an item')
382
- filters = entities.Filters(
383
- resource=entities.FiltersResource.ANNOTATION,
384
- field='itemId',
385
- values=self._item.id,
386
- method=entities.FiltersMethod.AND
387
- )
388
- else:
389
- raise exceptions.PlatformException(error='400',
390
- message='Unknown annotation type')
391
-
392
- if annotation_id is not None:
393
- if not isinstance(annotation_id, list):
394
- return self._delete_single_annotation(w_annotation_id=annotation_id)
395
- filters = entities.Filters(resource=entities.FiltersResource.ANNOTATION,
396
- field='annotationId',
397
- values=annotation_id,
398
- operator=entities.FiltersOperations.IN)
399
- filters.pop(field="type")
400
-
401
- if filters is None:
402
- raise exceptions.PlatformException(error='400',
403
- message='Must input filter, annotation id or annotation entity')
404
-
405
- if not filters.resource == entities.FiltersResource.ANNOTATION:
406
- raise exceptions.PlatformException(error='400',
407
- message='Filters resource must to be FiltersResource.ANNOTATION')
408
-
409
- if self._item is not None and not filters.has_field('itemId'):
410
- filters = deepcopy(filters)
411
- filters.add(field='itemId', values=self._item.id, method=entities.FiltersMethod.AND)
412
-
413
- if self._dataset is not None:
414
- items_repo = self._dataset.items
415
- elif self._item is not None:
416
- items_repo = self._item.dataset.items
417
- else:
418
- raise exceptions.PlatformException(
419
- error='2001',
420
- message='Missing "dataset". need to set a Dataset entity or use dataset.annotations repository')
421
-
422
- return items_repo.delete(filters=filters)
423
-
424
- @staticmethod
425
- def _update_snapshots(origin, modified):
426
- """
427
- Update the snapshots if a change occurred and return a list of the new snapshots with the flag to update.
428
- """
429
- update = False
430
- origin_snapshots = list()
431
- origin_metadata = origin['metadata'].get('system', None)
432
- if origin_metadata:
433
- origin_snapshots = origin_metadata.get('snapshots_', None)
434
- if origin_snapshots is not None:
435
- modified_snapshots = modified['metadata'].get('system', dict()).get('snapshots_', None)
436
- # if the number of the snapshots change
437
- if len(origin_snapshots) != len(modified_snapshots):
438
- origin_snapshots = modified_snapshots
439
- update = True
440
-
441
- i = 0
442
- # if some snapshot change
443
- while i < len(origin_snapshots) and not update:
444
- if origin_snapshots[i] != modified_snapshots[i]:
445
- origin_snapshots = modified_snapshots
446
- update = True
447
- break
448
- i += 1
449
-
450
- return update, origin_snapshots
451
-
452
- def _update_single_annotation(self, w_annotation, system_metadata):
453
- try:
454
- if isinstance(w_annotation, entities.Annotation):
455
- if w_annotation.id is None:
456
- raise exceptions.PlatformException(
457
- '400',
458
- 'Cannot update annotation because it was not fetched'
459
- ' from platform and therefore does not have an id'
460
- )
461
- annotation_id = w_annotation.id
462
- else:
463
- raise exceptions.PlatformException('400',
464
- 'unknown annotations type: {}'.format(type(w_annotation)))
465
-
466
- origin = w_annotation._platform_dict
467
- modified = w_annotation.to_json()
468
- # check snapshots
469
- update, updated_snapshots = self._update_snapshots(origin=origin,
470
- modified=modified)
471
-
472
- # pop the snapshots to make the diff work with out them
473
- origin.get('metadata', dict()).get('system', dict()).pop('snapshots_', None)
474
- modified.get('metadata', dict()).get('system', dict()).pop('snapshots_', None)
475
-
476
- # check diffs in the json
477
- json_req = miscellaneous.DictDiffer.diff(origin=origin,
478
- modified=modified)
479
-
480
- # add the new snapshots if exist
481
- if updated_snapshots and update:
482
- if 'metadata' not in json_req:
483
- json_req['metadata'] = dict()
484
- if 'system' not in json_req['metadata']:
485
- json_req['metadata']['system'] = dict()
486
- json_req['metadata']['system']['snapshots_'] = updated_snapshots
487
-
488
- # no changes happen
489
- if not json_req and not updated_snapshots:
490
- status = True
491
- result = w_annotation
492
- else:
493
- suc, response = self._update_annotation_req(annotation_json=json_req,
494
- system_metadata=system_metadata,
495
- annotation_id=annotation_id)
496
- if suc:
497
- result = entities.Annotation.from_json(_json=response.json(),
498
- annotations=self,
499
- dataset=self._dataset,
500
- item=self._item)
501
- w_annotation._platform_dict = result._platform_dict
502
- else:
503
- raise exceptions.PlatformException(response)
504
- status = True
505
- except Exception:
506
- status = False
507
- result = traceback.format_exc()
508
- return status, result
509
-
510
- def _update_annotation_req(self, annotation_json, system_metadata, annotation_id):
511
- url_path = '/annotations/{}'.format(annotation_id)
512
- if system_metadata:
513
- url_path += '?system=true'
514
- suc, response = self._client_api.gen_request(req_type='put',
515
- path=url_path,
516
- json_req=annotation_json)
517
- return suc, response
518
-
519
- @_api_reference.add(path='/annotations/{annotationId}', method='put')
520
- def update(self, annotations, system_metadata=False):
521
- """
522
- Update an existing annotation. For example, you may change the annotation's label and then use the update method.
523
-
524
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
525
- *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
526
-
527
- :param dtlpy.entities.annotation.Annotation annotations: Annotation object
528
- :param bool system_metadata: bool - True, if you want to change metadata system
529
-
530
- :return: True if successful or error if unsuccessful
531
- :rtype: bool
532
-
533
- **Example**:
534
-
535
- .. code-block:: python
536
-
537
- annotations = item.annotations.update(annotation='annotation')
538
- """
539
- pool = self._client_api.thread_pools(pool_name='annotation.update')
540
- if not isinstance(annotations, list):
541
- annotations = [annotations]
542
- jobs = [None for _ in range(len(annotations))]
543
- for i_ann, ann in enumerate(annotations):
544
- jobs[i_ann] = pool.submit(self._update_single_annotation,
545
- **{'w_annotation': ann,
546
- 'system_metadata': system_metadata})
547
-
548
- # get all results
549
- results = [j.result() for j in jobs]
550
- out_annotations = [r[1] for r in results if r[0] is True]
551
- out_errors = [r[1] for r in results if r[0] is False]
552
- if len(out_errors) == 0:
553
- logger.debug('Annotation/s updated successfully. {}/{}'.format(len(out_annotations), len(results)))
554
- else:
555
- logger.error(out_errors)
556
- logger.error('Annotation/s updated with {} errors'.format(len(out_errors)))
557
- return out_annotations
558
-
559
- @staticmethod
560
- def _annotation_encoding(annotation):
561
- metadata = annotation.get('metadata', dict())
562
- system = metadata.get('system', dict())
563
- snapshots = system.get('snapshots_', list())
564
- last_frame = {
565
- 'label': annotation.get('label', None),
566
- 'attributes': annotation.get('attributes', None),
567
- 'type': annotation.get('type', None),
568
- 'data': annotation.get('coordinates', None),
569
- 'fixed': snapshots[0].get('fixed', None) if (isinstance(snapshots, list) and len(snapshots) > 0) else None,
570
- 'objectVisible': snapshots[0].get('objectVisible', None) if (
571
- isinstance(snapshots, list) and len(snapshots) > 0) else None,
572
- }
573
-
574
- offset = 0
575
- for idx, frame in enumerate(deepcopy(snapshots)):
576
- frame.pop("frame", None)
577
- if frame == last_frame and not frame['fixed']:
578
- del snapshots[idx - offset]
579
- offset += 1
580
- else:
581
- last_frame = frame
582
- return annotation
583
-
584
- def _create_batches_for_upload(self, annotations, merge=False):
585
- """
586
- receives a list of annotations and split them into batches to optimize the upload
587
-
588
- :param annotations: list of all annotations
589
- :param merge: bool - merge the new binary annotations with the existing annotations
590
- :return: batch_annotations: list of list of annotation. each batch with size self._upload_batch_size
591
- """
592
- annotation_batches = list()
593
- single_batch = list()
594
- for annotation in annotations:
595
- if isinstance(annotation, str):
596
- annotation = json.loads(annotation)
597
- elif isinstance(annotation, entities.Annotation):
598
- if annotation._item is None and self._item is not None:
599
- # if annotation is without item - set one (affects the binary annotation color)
600
- annotation._item = self._item
601
- annotation = annotation.to_json()
602
- elif isinstance(annotation, dict):
603
- pass
604
- else:
605
- raise exceptions.PlatformException(error='400',
606
- message='unknown annotations type: {}'.format(type(annotation)))
607
- annotation = self._annotation_encoding(annotation)
608
- single_batch.append(annotation)
609
- if len(single_batch) >= self._upload_batch_size:
610
- annotation_batches.append(single_batch)
611
- single_batch = list()
612
- if len(single_batch) > 0:
613
- annotation_batches.append(single_batch)
614
- if merge and self.item:
615
- annotation_batches = self._merge_new_annotations(annotation_batches)
616
- annotation_batches = self._merge_to_exits_annotations(annotation_batches)
617
- return annotation_batches
618
-
619
- def _merge_binary_annotations(self, data_url1, data_url2, item_width, item_height):
620
- # Decode base64 data
621
- img_data1 = base64.b64decode(data_url1.split(",")[1])
622
- img_data2 = base64.b64decode(data_url2.split(",")[1])
623
-
624
- # Convert binary data to images
625
- img1 = Image.open(BytesIO(img_data1))
626
- img2 = Image.open(BytesIO(img_data2))
627
-
628
- # Create a new image with the target item size
629
- merged_img = Image.new('RGBA', (item_width, item_height))
630
-
631
- # Paste both images on the new canvas at their original sizes and positions
632
- # Adjust positioning logic if needed (assuming top-left corner for both images here)
633
- merged_img.paste(img1, (0, 0), img1) # Use img1 as a mask to handle transparency
634
- merged_img.paste(img2, (0, 0), img2) # Overlay img2 at the same position
635
-
636
- # Save the merged image to a buffer
637
- buffer = BytesIO()
638
- merged_img.save(buffer, format="PNG")
639
- merged_img_data = buffer.getvalue()
640
-
641
- # Encode the merged image back to a base64 string
642
- merged_data_url = "data:image/png;base64," + base64.b64encode(merged_img_data).decode()
643
-
644
- return merged_data_url
645
-
646
- def _merge_new_annotations(self, annotations_batch):
647
- """
648
- Merge the new binary annotations with the existing annotations
649
- :param annotations_batch: list of list of annotation. each batch with size self._upload_batch_size
650
- :return: merged_annotations_batch: list of list of annotation. each batch with size self._upload_batch_size
651
- """
652
- for annotations in annotations_batch:
653
- for annotation in annotations:
654
- if annotation['type'] == 'binary' and not annotation.get('clean', False):
655
- to_merge = [a for a in annotations if
656
- not a.get('clean', False) and a.get("metadata", {}).get('system', {}).get('objectId',
657
- None) ==
658
- annotation.get("metadata", {}).get('system', {}).get('objectId', None) and a['label'] ==
659
- annotation['label']]
660
- if len(to_merge) == 0:
661
- # no annotation to merge with
662
- continue
663
- for a in to_merge:
664
- if a['coordinates'] == annotation['coordinates']:
665
- continue
666
- merged_data_url = self._merge_binary_annotations(a['coordinates'], annotation['coordinates'],
667
- self.item.width, self.item.height)
668
- annotation['coordinates'] = merged_data_url
669
- a['clean'] = True
670
- return [[a for a in annotations if not a.get('clean', False)] for annotations in annotations_batch]
671
-
672
- def _merge_to_exits_annotations(self, annotations_batch):
673
- filters = entities.Filters(resource=entities.FiltersResource.ANNOTATION, field='type', values='binary')
674
- filters.add(field='itemId', values=self.item.id, method=entities.FiltersMethod.AND)
675
- exist_annotations = self.list(filters=filters).annotations or list()
676
- to_delete = list()
677
- for annotations in annotations_batch:
678
- for ann in annotations:
679
- if ann['type'] == 'binary':
680
- to_merge = [a for a in exist_annotations if
681
- a.object_id == ann.get("metadata", {}).get('system', {}).get('objectId',
682
- None) and a.label == ann[
683
- 'label']]
684
- if len(to_merge) == 0:
685
- # no annotation to merge with
686
- continue
687
- if to_merge[0].coordinates == ann['coordinates']:
688
- # same annotation
689
- continue
690
- if len(to_merge) > 1:
691
- raise exceptions.PlatformException('400', 'Multiple annotations with the same label')
692
- # merge
693
- exist_annotations.remove(to_merge[0])
694
- merged_data_url = self._merge_binary_annotations(to_merge[0].coordinates, ann['coordinates'],
695
- self.item.width, self.item.height)
696
- json_ann = to_merge[0].to_json()
697
- json_ann['coordinates'] = merged_data_url
698
- suc, response = self._update_annotation_req(annotation_json=json_ann,
699
- system_metadata=True,
700
- annotation_id=to_merge[0].id)
701
- if not suc:
702
- raise exceptions.PlatformException(response)
703
- if suc:
704
- result = entities.Annotation.from_json(_json=response.json(),
705
- annotations=self,
706
- dataset=self._dataset,
707
- item=self._item)
708
- exist_annotations.append(result)
709
- to_delete.append(ann)
710
- if len(to_delete) > 0:
711
- annotations_batch = [[a for a in annotations if a not in to_delete] for annotations in annotations_batch]
712
-
713
- return annotations_batch
714
-
715
- def _upload_single_batch(self, annotation_batch):
716
- try:
717
- suc, response = self._client_api.gen_request(req_type='post',
718
- path='/items/{}/annotations'.format(self.item.id),
719
- json_req=annotation_batch)
720
- if suc:
721
- return_annotations = response.json()
722
- if not isinstance(return_annotations, list):
723
- return_annotations = [return_annotations]
724
- else:
725
- raise exceptions.PlatformException(response)
726
-
727
- status = True
728
- result = return_annotations
729
- except Exception:
730
- status = False
731
- result = traceback.format_exc()
732
-
733
- return status, result
734
-
735
- def _upload_annotations_batches(self, annotation_batches):
736
- if len(annotation_batches) == 1:
737
- # no need for threads
738
- status, result = self._upload_single_batch(annotation_batch=annotation_batches[0])
739
- if status is False:
740
- logger.error(result)
741
- logger.error('Annotation/s uploaded with errors')
742
- # TODO need to raise errors?
743
- uploaded_annotations = result
744
- else:
745
- # threading
746
- pool = self._client_api.thread_pools(pool_name='annotation.upload')
747
- jobs = [None for _ in range(len(annotation_batches))]
748
- for i_ann, annotations_batch in enumerate(annotation_batches):
749
- jobs[i_ann] = pool.submit(self._upload_single_batch,
750
- annotation_batch=annotations_batch)
751
- # get all results
752
- results = [j.result() for j in jobs]
753
- uploaded_annotations = [ann for ann_list in results for ann in ann_list[1] if ann_list[0] is True]
754
- out_errors = [r[1] for r in results if r[0] is False]
755
- if len(out_errors) != 0:
756
- logger.error(out_errors)
757
- logger.error('Annotation/s uploaded with errors')
758
- # TODO need to raise errors?
759
- logger.info('Annotation/s uploaded successfully. num: {}'.format(len(uploaded_annotations)))
760
- return uploaded_annotations
761
-
762
- async def _async_upload_annotations(self, annotations, merge=False):
763
- """
764
- Async function to run from the uploader. will use asyncio to not break the async
765
- :param annotations: list of all annotations
766
- :param merge: bool - merge the new binary annotations with the existing annotations
767
- :return:
768
- """
769
- async with self._client_api.event_loop.semaphore('annotations.upload'):
770
- annotation_batch = self._create_batches_for_upload(annotations=annotations, merge=merge)
771
- output_annotations = list()
772
- for annotations_list in annotation_batch:
773
- success, response = await self._client_api.gen_async_request(req_type='post',
774
- path='/items/{}/annotations'
775
- .format(self.item.id),
776
- json_req=annotations_list)
777
- if success:
778
- return_annotations = response.json()
779
- if not isinstance(return_annotations, list):
780
- return_annotations = [return_annotations]
781
- output_annotations.extend(return_annotations)
782
- else:
783
- if len(output_annotations) > 0:
784
- logger.warning("Only {} annotations from {} annotations have been uploaded".
785
- format(len(output_annotations), len(annotations)))
786
- raise exceptions.PlatformException(response)
787
-
788
- result = entities.AnnotationCollection.from_json(_json=output_annotations, item=self.item)
789
- return result
790
-
791
- @_api_reference.add(path='/items/{itemId}/annotations', method='post')
792
- def upload(self, annotations, merge=False) -> entities.AnnotationCollection:
793
- """
794
- Upload a new annotation/annotations. You must first create the annotation using the annotation *builder* method.
795
-
796
- **Prerequisites**: Any user can upload annotations.
797
-
798
- :param List[dtlpy.entities.annotation.Annotation] or dtlpy.entities.annotation.Annotation annotations: list or
799
- single annotation of type Annotation
800
- :param bool merge: optional - merge the new binary annotations with the existing annotations
801
- :return: list of annotation objects
802
- :rtype: entities.AnnotationCollection
803
-
804
- **Example**:
805
-
806
- .. code-block:: python
807
-
808
- annotations = item.annotations.upload(annotations='builder')
809
- """
810
- # make list if not list
811
- if isinstance(annotations, entities.AnnotationCollection):
812
- # get the annotation from a collection
813
- annotations = annotations.annotations
814
- elif isinstance(annotations, str) and os.path.isfile(annotations):
815
- # load annotation filepath and get list of annotations
816
- with open(annotations, 'r', encoding="utf8") as f:
817
- annotations = json.load(f)
818
- annotations = annotations.get('annotations', [])
819
- # annotations = entities.AnnotationCollection.from_json_file(filepath=annotations).annotations
820
- elif isinstance(annotations, entities.Annotation) or isinstance(annotations, dict):
821
- # convert the single Annotation to a list
822
- annotations = [annotations]
823
- elif isinstance(annotations, list):
824
- pass
825
- else:
826
- exceptions.PlatformException(error='400',
827
- message='Unknown annotation format. type: {}'.format(type(annotations)))
828
- if len(annotations) == 0:
829
- logger.warning('Annotation upload receives 0 annotations. Not doing anything')
830
- out_annotations = list()
831
- else:
832
- annotation_batches = self._create_batches_for_upload(annotations=annotations, merge=merge)
833
- out_annotations = self._upload_annotations_batches(annotation_batches=annotation_batches)
834
- out_annotations = entities.AnnotationCollection.from_json(_json=out_annotations,
835
- item=self.item)
836
- return out_annotations
837
-
838
- def update_status(self,
839
- annotation: entities.Annotation = None,
840
- annotation_id: str = None,
841
- status: entities.AnnotationStatus = entities.AnnotationStatus.ISSUE
842
- ) -> entities.Annotation:
843
- """
844
- Set status on annotation.
845
-
846
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
847
- *developer* or be assigned a task that includes that item as an *annotation manager*.
848
-
849
- :param dtlpy.entities.annotation.Annotation annotation: Annotation object
850
- :param str annotation_id: optional - annotation id to set status
851
- :param str status: can be AnnotationStatus.ISSUE, APPROVED, REVIEW, CLEAR
852
- :return: Annotation object
853
- :rtype: dtlpy.entities.annotation.Annotation
854
-
855
- **Example**:
856
-
857
- .. code-block:: python
858
-
859
- annotation = item.annotations.update_status(annotation_id='annotation_id', status=dl.AnnotationStatus.ISSUE)
860
- """
861
- if annotation is None:
862
- if annotation_id is None:
863
- raise ValueError('must input on of "annotation" or "annotation_id"')
864
- annotation = self.get(annotation_id=annotation_id)
865
- if status not in list(entities.AnnotationStatus):
866
- raise ValueError('status must be on of: {}'.format(', '.join(list(entities.AnnotationStatus))))
867
- annotation.status = status
868
- return annotation.update(system_metadata=True)
869
-
870
- def builder(self):
871
- """
872
- Create Annotation collection.
873
-
874
- **Prerequisites**: You must have an item to be annotated. You must have the role of an *owner* or *developer*
875
- or be assigned a task that includes that item as an *annotation manager* or *annotator*.
876
-
877
- :return: Annotation collection object
878
- :rtype: dtlpy.entities.annotation_collection.AnnotationCollection
879
-
880
- **Example**:
881
-
882
- .. code-block:: python
883
-
884
- annotation_collection = item.annotations.builder()
885
- """
886
- return entities.AnnotationCollection(item=self.item)
887
-
888
- def task_scores(self, annotation_id: str, task_id: str, page_offset: int = 0, page_size: int = 100):
889
- """
890
- Get annotation scores in a task
891
-
892
- **Prerequisites**: You must be able to read the task
893
-
894
- :param str annotation_id: The id of the annotation
895
- :param str task_id: The id of the task
896
- :param int page_offset: starting page
897
- :param int page_size: size of page
898
- :return: json response
899
- :rtype: dict
900
- """
901
- if annotation_id is None:
902
- raise exceptions.PlatformException('400', 'annotation_id must be provided')
903
- if task_id is None:
904
- raise exceptions.PlatformException('400', 'task_id must be provided')
905
-
906
- success, response = self._client_api.gen_request(req_type='get',
907
- path='/scores/tasks/{}/annotations/{}?page={}&pageSize={}'
908
- .format(task_id, annotation_id, page_offset, page_size))
909
- if success:
910
- return response.json()
911
- else:
912
- raise exceptions.PlatformException(response)
913
-
914
- ##################
915
- # async function #
916
- ##################
1
+ from copy import deepcopy
2
+ import traceback
3
+ import logging
4
+ import json
5
+ import jwt
6
+ import os
7
+ from PIL import Image
8
+ from io import BytesIO
9
+ import base64
10
+
11
+ from .. import entities, exceptions, miscellaneous, _api_reference
12
+ from ..services.api_client import ApiClient
13
+
14
+ logger = logging.getLogger(name='dtlpy')
15
+
16
+
17
+ class Annotations:
18
+ """
19
+ Annotations Repository
20
+
21
+ The Annotation class allows you to manage the annotations of data items. For information on annotations explore our
22
+ documentation at:
23
+ `Classification SDK <https://developers.dataloop.ai/tutorials/annotations_image/classification_point_and_pose/chapter/>`_,
24
+ `Annotation Labels and Attributes <https://developers.dataloop.ai/tutorials/data_management/upload_and_manage_annotations/chapter/#set-attributes-on-annotations>`_,
25
+ `Show Video with Annotations <https://developers.dataloop.ai/tutorials/annotations_video/video_annotations/chapter/>`_.
26
+ """
27
+
28
+ def __init__(self, client_api: ApiClient, item=None, dataset=None, dataset_id=None):
29
+ self._client_api = client_api
30
+ self._item = item
31
+ self._dataset = dataset
32
+ self._upload_batch_size = 100
33
+ if dataset_id is None:
34
+ if dataset is not None:
35
+ dataset_id = dataset.id
36
+ elif item is not None:
37
+ dataset_id = item.dataset_id
38
+ self._dataset_id = dataset_id
39
+
40
+ ############
41
+ # entities #
42
+ ############
43
+ @property
44
+ def dataset(self):
45
+ if self._dataset is None:
46
+ raise exceptions.PlatformException(
47
+ error='2001',
48
+ message='Missing "dataset". need to set a Dataset entity or use dataset.annotations repository')
49
+ assert isinstance(self._dataset, entities.Dataset)
50
+ return self._dataset
51
+
52
+ @dataset.setter
53
+ def dataset(self, dataset: entities.Dataset):
54
+ if not isinstance(dataset, entities.Dataset):
55
+ raise ValueError('Must input a valid Dataset entity')
56
+ self._dataset = dataset
57
+
58
+ @property
59
+ def item(self):
60
+ if self._item is None:
61
+ raise exceptions.PlatformException(
62
+ error='2001',
63
+ message='Missing "item". need to set an Item entity or use item.annotations repository')
64
+ assert isinstance(self._item, entities.Item)
65
+ return self._item
66
+
67
+ @item.setter
68
+ def item(self, item: entities.Item):
69
+ if not isinstance(item, entities.Item):
70
+ raise ValueError('Must input a valid Item entity')
71
+ self._item = item
72
+
73
+ ###########
74
+ # methods #
75
+ ###########
76
+ @_api_reference.add(path='/annotations/{annotationId}', method='get')
77
+ def get(self, annotation_id: str) -> entities.Annotation:
78
+ """
79
+ Get a single annotation.
80
+
81
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
82
+ *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
83
+
84
+ :param str annotation_id: The id of the annotation
85
+ :return: Annotation object or None
86
+ :return: Annotation object or None
87
+ :rtype: dtlpy.entities.annotation.Annotation
88
+
89
+ **Example**:
90
+
91
+ .. code-block:: python
92
+
93
+ annotation = item.annotations.get(annotation_id='annotation_id')
94
+ """
95
+ success, response = self._client_api.gen_request(req_type='get',
96
+ path='/annotations/{}'.format(annotation_id))
97
+ if success:
98
+ annotation = entities.Annotation.from_json(_json=response.json(),
99
+ annotations=self,
100
+ dataset=self._dataset,
101
+ client_api=self._client_api,
102
+ item=self._item)
103
+ else:
104
+ raise exceptions.PlatformException(response)
105
+ return annotation
106
+
107
+ def _build_entities_from_response(self, response_items):
108
+ pool = self._client_api.thread_pools(pool_name='entity.create')
109
+ jobs = [None for _ in range(len(response_items))]
110
+ # return triggers list
111
+ for i_json, _json in enumerate(response_items):
112
+ jobs[i_json] = pool.submit(entities.Annotation._protected_from_json,
113
+ **{'client_api': self._client_api,
114
+ '_json': _json,
115
+ 'item': self._item,
116
+ 'dataset': self._dataset,
117
+ 'annotations': self})
118
+
119
+ # get all results
120
+ results = [j.result() for j in jobs]
121
+ # log errors
122
+ _ = [logger.warning(r[1]) for r in results if r[0] is False]
123
+ # return good jobs
124
+ return miscellaneous.List([r[1] for r in results if r[0] is True])
125
+
126
+ def _list(self, filters: entities.Filters):
127
+ """
128
+ Get a dataset's item list. This is a browsing endpoint. For any given path, item count will be returned.
129
+ The user is then expected to perform another request for every folder to actually get its item list.
130
+
131
+ :param dtlpy.entities.filters.Filters filters: Filter entity or a dictionary containing filters parameters
132
+
133
+ :return: json response
134
+ :rtype:
135
+ """
136
+ # prepare request
137
+ success, response = self._client_api.gen_request(req_type="POST",
138
+ path="/datasets/{}/query".format(self._dataset_id),
139
+ json_req=filters.prepare(),
140
+ headers={'user_query': filters._user_query}
141
+ )
142
+ if not success:
143
+ raise exceptions.PlatformException(response)
144
+ return response.json()
145
+
146
+ @_api_reference.add(path='/datasets/{id}/query', method='post')
147
+ def list(self, filters: entities.Filters = None, page_offset: int = None, page_size: int = None):
148
+ """
149
+ List Annotations of a specific item. You must get the item first and then list the annotations with the desired filters.
150
+
151
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
152
+ *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
153
+
154
+ :param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filters parameters
155
+ :param int page_offset: starting page
156
+ :param int page_size: size of page
157
+ :return: Pages object
158
+ :rtype: dtlpy.entities.paged_entities.PagedEntities
159
+
160
+ **Example**:
161
+
162
+ .. code-block:: python
163
+
164
+ annotations = item.annotations.list(filters=dl.Filters(
165
+ resource=dl.FiltersResource.ANNOTATION,
166
+ field='type',
167
+ values='box'),
168
+ page_size=100,
169
+ page_offset=0)
170
+ """
171
+ if self._dataset_id is not None:
172
+ if filters is None:
173
+ filters = entities.Filters(resource=entities.FiltersResource.ANNOTATION)
174
+ filters._user_query = 'false'
175
+
176
+ if not filters.resource == entities.FiltersResource.ANNOTATION:
177
+ raise exceptions.PlatformException(error='400',
178
+ message='Filters resource must to be FiltersResource.ANNOTATION')
179
+
180
+ if self._item is not None and not filters.has_field('itemId'):
181
+ filters = deepcopy(filters)
182
+ filters.page_size = 1000
183
+ filters.add(field='itemId', values=self.item.id, method=entities.FiltersMethod.AND)
184
+
185
+ # assert type filters
186
+ if not isinstance(filters, entities.Filters):
187
+ raise exceptions.PlatformException('400', 'Unknown filters type')
188
+
189
+ # page size
190
+ if page_size is None:
191
+ # take from default
192
+ page_size = filters.page_size
193
+ else:
194
+ filters.page_size = page_size
195
+
196
+ # page offset
197
+ if page_offset is None:
198
+ # take from default
199
+ page_offset = filters.page
200
+ else:
201
+ filters.page = page_offset
202
+
203
+ paged = entities.PagedEntities(items_repository=self,
204
+ filters=filters,
205
+ page_offset=page_offset,
206
+ page_size=page_size,
207
+ client_api=self._client_api)
208
+ paged.get_page()
209
+
210
+ if self._item is not None:
211
+ if paged.total_pages_count > 1:
212
+ annotations = list()
213
+ for page in paged:
214
+ annotations += page
215
+ else:
216
+ annotations = paged.items
217
+ return entities.AnnotationCollection(annotations=annotations, item=self._item)
218
+ else:
219
+ return paged
220
+ else:
221
+ raise exceptions.PlatformException('400',
222
+ 'Please use item.annotations.list() or dataset.annotations.list() '
223
+ 'to perform this action.')
224
+
225
+ def show(self,
226
+ image=None,
227
+ thickness: int = 1,
228
+ with_text: bool = False,
229
+ height: float = None,
230
+ width: float = None,
231
+ annotation_format: entities.ViewAnnotationOptions = entities.ViewAnnotationOptions.MASK,
232
+ alpha: float = 1):
233
+ """
234
+ Show annotations. To use this method, you must get the item first and then show the annotations with
235
+ the desired filters. The method returns an array showing all the annotations.
236
+
237
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
238
+ *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
239
+
240
+ :param ndarray image: empty or image to draw on
241
+ :param int thickness: optional - line thickness, default=1
242
+ :param bool with_text: add label to annotation
243
+ :param float height: item height
244
+ :param float width: item width
245
+ :param str annotation_format: the format that want to show ,options: list(dl.ViewAnnotationOptions)
246
+ :param float alpha: opacity value [0 1], default 1
247
+ :return: ndarray of the annotations
248
+ :rtype: ndarray
249
+
250
+ **Example**:
251
+
252
+ .. code-block:: python
253
+
254
+ image = item.annotations.show(image='nd array',
255
+ thickness=1,
256
+ with_text=False,
257
+ height=100,
258
+ width=100,
259
+ annotation_format=dl.ViewAnnotationOptions.MASK,
260
+ alpha=1)
261
+ """
262
+ # get item's annotations
263
+ annotations = self.list()
264
+
265
+ return annotations.show(image=image,
266
+ width=width,
267
+ height=height,
268
+ thickness=thickness,
269
+ alpha=alpha,
270
+ with_text=with_text,
271
+ annotation_format=annotation_format)
272
+
273
+ def download(self,
274
+ filepath: str,
275
+ annotation_format: entities.ViewAnnotationOptions = entities.ViewAnnotationOptions.JSON,
276
+ img_filepath: str = None,
277
+ height: float = None,
278
+ width: float = None,
279
+ thickness: int = 1,
280
+ with_text: bool = False,
281
+ alpha: float = 1):
282
+ """
283
+ Save annotation to file.
284
+
285
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
286
+ *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
287
+
288
+ :param str filepath: Target download directory
289
+ :param str annotation_format: the format that want to download ,options: list(dl.ViewAnnotationOptions)
290
+ :param str img_filepath: img file path - needed for img_mask
291
+ :param float height: optional - image height
292
+ :param float width: optional - image width
293
+ :param int thickness: optional - line thickness, default=1
294
+ :param bool with_text: optional - draw annotation with text, default = False
295
+ :param float alpha: opacity value [0 1], default 1
296
+ :return: file path to where save the annotations
297
+ :rtype: str
298
+
299
+ **Example**:
300
+
301
+ .. code-block:: python
302
+
303
+ file_path = item.annotations.download(
304
+ filepath='file_path',
305
+ annotation_format=dl.ViewAnnotationOptions.MASK,
306
+ img_filepath='img_filepath',
307
+ height=100,
308
+ width=100,
309
+ thickness=1,
310
+ with_text=False,
311
+ alpha=1)
312
+ """
313
+ # get item's annotations
314
+ annotations = self.list()
315
+ if 'text' in self.item.metadata.get('system').get('mimetype', '') or 'json' in self.item.metadata.get('system').get('mimetype', ''):
316
+ annotation_format = entities.ViewAnnotationOptions.JSON
317
+ elif 'audio' not in self.item.metadata.get('system').get('mimetype', ''):
318
+ # height/weight
319
+ if height is None:
320
+ if self.item.height is None:
321
+ raise exceptions.PlatformException('400', 'Height must be provided')
322
+ height = self.item.height
323
+ if width is None:
324
+ if self.item.width is None:
325
+ raise exceptions.PlatformException('400', 'Width must be provided')
326
+ width = self.item.width
327
+
328
+ return annotations.download(filepath=filepath,
329
+ img_filepath=img_filepath,
330
+ width=width,
331
+ height=height,
332
+ thickness=thickness,
333
+ with_text=with_text,
334
+ annotation_format=annotation_format,
335
+ alpha=alpha)
336
+
337
+ def _delete_single_annotation(self, w_annotation_id):
338
+ try:
339
+ creator = jwt.decode(self._client_api.token, algorithms=['HS256'],
340
+ verify=False, options={'verify_signature': False})['email']
341
+ payload = {'username': creator}
342
+ success, response = self._client_api.gen_request(req_type='delete',
343
+ path='/annotations/{}'.format(w_annotation_id),
344
+ json_req=payload)
345
+
346
+ if not success:
347
+ raise exceptions.PlatformException(response)
348
+ return success
349
+ except Exception:
350
+ logger.exception('Failed to delete annotation')
351
+ raise
352
+
353
+ @_api_reference.add(path='/annotations/{annotationId}', method='delete')
354
+ def delete(self, annotation: entities.Annotation = None,
355
+ annotation_id: str = None,
356
+ filters: entities.Filters = None) -> bool:
357
+ """
358
+ Remove an annotation from item.
359
+
360
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
361
+ *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
362
+
363
+ :param dtlpy.entities.annotation.Annotation annotation: Annotation object
364
+ :param str annotation_id: The id of the annotation
365
+ :param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filters parameters
366
+ :return: True/False
367
+ :rtype: bool
368
+
369
+ **Example**:
370
+
371
+ .. code-block:: python
372
+
373
+ is_deleted = item.annotations.delete(annotation_id='annotation_id')
374
+ """
375
+ if annotation is not None:
376
+ if isinstance(annotation, entities.Annotation):
377
+ annotation_id = annotation.id
378
+ elif isinstance(annotation, str) and annotation.lower() == 'all':
379
+ if self._item is None:
380
+ raise exceptions.PlatformException(error='400',
381
+ message='To use "all" option repository must have an item')
382
+ filters = entities.Filters(
383
+ resource=entities.FiltersResource.ANNOTATION,
384
+ field='itemId',
385
+ values=self._item.id,
386
+ method=entities.FiltersMethod.AND
387
+ )
388
+ else:
389
+ raise exceptions.PlatformException(error='400',
390
+ message='Unknown annotation type')
391
+
392
+ if annotation_id is not None:
393
+ if not isinstance(annotation_id, list):
394
+ return self._delete_single_annotation(w_annotation_id=annotation_id)
395
+ filters = entities.Filters(resource=entities.FiltersResource.ANNOTATION,
396
+ field='annotationId',
397
+ values=annotation_id,
398
+ operator=entities.FiltersOperations.IN)
399
+ filters.pop(field="type")
400
+
401
+ if filters is None:
402
+ raise exceptions.PlatformException(error='400',
403
+ message='Must input filter, annotation id or annotation entity')
404
+
405
+ if not filters.resource == entities.FiltersResource.ANNOTATION:
406
+ raise exceptions.PlatformException(error='400',
407
+ message='Filters resource must to be FiltersResource.ANNOTATION')
408
+
409
+ if self._item is not None and not filters.has_field('itemId'):
410
+ filters = deepcopy(filters)
411
+ filters.add(field='itemId', values=self._item.id, method=entities.FiltersMethod.AND)
412
+
413
+ if self._dataset is not None:
414
+ items_repo = self._dataset.items
415
+ elif self._item is not None:
416
+ items_repo = self._item.dataset.items
417
+ else:
418
+ raise exceptions.PlatformException(
419
+ error='2001',
420
+ message='Missing "dataset". need to set a Dataset entity or use dataset.annotations repository')
421
+
422
+ return items_repo.delete(filters=filters)
423
+
424
+ @staticmethod
425
+ def _update_snapshots(origin, modified):
426
+ """
427
+ Update the snapshots if a change occurred and return a list of the new snapshots with the flag to update.
428
+ """
429
+ update = False
430
+ origin_snapshots = list()
431
+ origin_metadata = origin['metadata'].get('system', None)
432
+ if origin_metadata:
433
+ origin_snapshots = origin_metadata.get('snapshots_', None)
434
+ if origin_snapshots is not None:
435
+ modified_snapshots = modified['metadata'].get('system', dict()).get('snapshots_', None)
436
+ # if the number of the snapshots change
437
+ if len(origin_snapshots) != len(modified_snapshots):
438
+ origin_snapshots = modified_snapshots
439
+ update = True
440
+
441
+ i = 0
442
+ # if some snapshot change
443
+ while i < len(origin_snapshots) and not update:
444
+ if origin_snapshots[i] != modified_snapshots[i]:
445
+ origin_snapshots = modified_snapshots
446
+ update = True
447
+ break
448
+ i += 1
449
+
450
+ return update, origin_snapshots
451
+
452
+ def _update_single_annotation(self, w_annotation, system_metadata):
453
+ try:
454
+ if isinstance(w_annotation, entities.Annotation):
455
+ if w_annotation.id is None:
456
+ raise exceptions.PlatformException(
457
+ '400',
458
+ 'Cannot update annotation because it was not fetched'
459
+ ' from platform and therefore does not have an id'
460
+ )
461
+ annotation_id = w_annotation.id
462
+ else:
463
+ raise exceptions.PlatformException('400',
464
+ 'unknown annotations type: {}'.format(type(w_annotation)))
465
+
466
+ origin = w_annotation._platform_dict
467
+ modified = w_annotation.to_json()
468
+ # check snapshots
469
+ update, updated_snapshots = self._update_snapshots(origin=origin,
470
+ modified=modified)
471
+
472
+ # pop the snapshots to make the diff work with out them
473
+ origin.get('metadata', dict()).get('system', dict()).pop('snapshots_', None)
474
+ modified.get('metadata', dict()).get('system', dict()).pop('snapshots_', None)
475
+
476
+ # check diffs in the json
477
+ json_req = miscellaneous.DictDiffer.diff(origin=origin,
478
+ modified=modified)
479
+
480
+ # add the new snapshots if exist
481
+ if updated_snapshots and update:
482
+ if 'metadata' not in json_req:
483
+ json_req['metadata'] = dict()
484
+ if 'system' not in json_req['metadata']:
485
+ json_req['metadata']['system'] = dict()
486
+ json_req['metadata']['system']['snapshots_'] = updated_snapshots
487
+
488
+ # no changes happen
489
+ if not json_req and not updated_snapshots:
490
+ status = True
491
+ result = w_annotation
492
+ else:
493
+ suc, response = self._update_annotation_req(annotation_json=json_req,
494
+ system_metadata=system_metadata,
495
+ annotation_id=annotation_id)
496
+ if suc:
497
+ result = entities.Annotation.from_json(_json=response.json(),
498
+ annotations=self,
499
+ dataset=self._dataset,
500
+ item=self._item)
501
+ w_annotation._platform_dict = result._platform_dict
502
+ else:
503
+ raise exceptions.PlatformException(response)
504
+ status = True
505
+ except Exception:
506
+ status = False
507
+ result = traceback.format_exc()
508
+ return status, result
509
+
510
+ def _update_annotation_req(self, annotation_json, system_metadata, annotation_id):
511
+ url_path = '/annotations/{}'.format(annotation_id)
512
+ if system_metadata:
513
+ url_path += '?system=true'
514
+ suc, response = self._client_api.gen_request(req_type='put',
515
+ path=url_path,
516
+ json_req=annotation_json)
517
+ return suc, response
518
+
519
+ @_api_reference.add(path='/annotations/{annotationId}', method='put')
520
+ def update(self, annotations, system_metadata=False):
521
+ """
522
+ Update an existing annotation. For example, you may change the annotation's label and then use the update method.
523
+
524
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
525
+ *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
526
+
527
+ :param dtlpy.entities.annotation.Annotation annotations: Annotation object
528
+ :param bool system_metadata: bool - True, if you want to change metadata system
529
+
530
+ :return: True if successful or error if unsuccessful
531
+ :rtype: bool
532
+
533
+ **Example**:
534
+
535
+ .. code-block:: python
536
+
537
+ annotations = item.annotations.update(annotation='annotation')
538
+ """
539
+ pool = self._client_api.thread_pools(pool_name='annotation.update')
540
+ if not isinstance(annotations, list):
541
+ annotations = [annotations]
542
+ jobs = [None for _ in range(len(annotations))]
543
+ for i_ann, ann in enumerate(annotations):
544
+ jobs[i_ann] = pool.submit(self._update_single_annotation,
545
+ **{'w_annotation': ann,
546
+ 'system_metadata': system_metadata})
547
+
548
+ # get all results
549
+ results = [j.result() for j in jobs]
550
+ out_annotations = [r[1] for r in results if r[0] is True]
551
+ out_errors = [r[1] for r in results if r[0] is False]
552
+ if len(out_errors) == 0:
553
+ logger.debug('Annotation/s updated successfully. {}/{}'.format(len(out_annotations), len(results)))
554
+ else:
555
+ logger.error(out_errors)
556
+ logger.error('Annotation/s updated with {} errors'.format(len(out_errors)))
557
+ return out_annotations
558
+
559
+ @staticmethod
560
+ def _annotation_encoding(annotation):
561
+ metadata = annotation.get('metadata', dict())
562
+ system = metadata.get('system', dict())
563
+ snapshots = system.get('snapshots_', list())
564
+ last_frame = {
565
+ 'label': annotation.get('label', None),
566
+ 'attributes': annotation.get('attributes', None),
567
+ 'type': annotation.get('type', None),
568
+ 'data': annotation.get('coordinates', None),
569
+ 'fixed': snapshots[0].get('fixed', None) if (isinstance(snapshots, list) and len(snapshots) > 0) else None,
570
+ 'objectVisible': snapshots[0].get('objectVisible', None) if (
571
+ isinstance(snapshots, list) and len(snapshots) > 0) else None,
572
+ }
573
+
574
+ offset = 0
575
+ for idx, frame in enumerate(deepcopy(snapshots)):
576
+ frame.pop("frame", None)
577
+ if frame == last_frame and not frame['fixed']:
578
+ del snapshots[idx - offset]
579
+ offset += 1
580
+ else:
581
+ last_frame = frame
582
+ return annotation
583
+
584
+ def _create_batches_for_upload(self, annotations, merge=False):
585
+ """
586
+ receives a list of annotations and split them into batches to optimize the upload
587
+
588
+ :param annotations: list of all annotations
589
+ :param merge: bool - merge the new binary annotations with the existing annotations
590
+ :return: batch_annotations: list of list of annotation. each batch with size self._upload_batch_size
591
+ """
592
+ annotation_batches = list()
593
+ single_batch = list()
594
+ for annotation in annotations:
595
+ if isinstance(annotation, str):
596
+ annotation = json.loads(annotation)
597
+ elif isinstance(annotation, entities.Annotation):
598
+ if annotation._item is None and self._item is not None:
599
+ # if annotation is without item - set one (affects the binary annotation color)
600
+ annotation._item = self._item
601
+ annotation = annotation.to_json()
602
+ elif isinstance(annotation, dict):
603
+ pass
604
+ else:
605
+ raise exceptions.PlatformException(error='400',
606
+ message='unknown annotations type: {}'.format(type(annotation)))
607
+ annotation = self._annotation_encoding(annotation)
608
+ single_batch.append(annotation)
609
+ if len(single_batch) >= self._upload_batch_size:
610
+ annotation_batches.append(single_batch)
611
+ single_batch = list()
612
+ if len(single_batch) > 0:
613
+ annotation_batches.append(single_batch)
614
+ if merge and self.item:
615
+ annotation_batches = self._merge_new_annotations(annotation_batches)
616
+ annotation_batches = self._merge_to_exits_annotations(annotation_batches)
617
+ return annotation_batches
618
+
619
+ def _merge_binary_annotations(self, data_url1, data_url2, item_width, item_height):
620
+ # Decode base64 data
621
+ img_data1 = base64.b64decode(data_url1.split(",")[1])
622
+ img_data2 = base64.b64decode(data_url2.split(",")[1])
623
+
624
+ # Convert binary data to images
625
+ img1 = Image.open(BytesIO(img_data1))
626
+ img2 = Image.open(BytesIO(img_data2))
627
+
628
+ # Create a new image with the target item size
629
+ merged_img = Image.new('RGBA', (item_width, item_height))
630
+
631
+ # Paste both images on the new canvas at their original sizes and positions
632
+ # Adjust positioning logic if needed (assuming top-left corner for both images here)
633
+ merged_img.paste(img1, (0, 0), img1) # Use img1 as a mask to handle transparency
634
+ merged_img.paste(img2, (0, 0), img2) # Overlay img2 at the same position
635
+
636
+ # Save the merged image to a buffer
637
+ buffer = BytesIO()
638
+ merged_img.save(buffer, format="PNG")
639
+ merged_img_data = buffer.getvalue()
640
+
641
+ # Encode the merged image back to a base64 string
642
+ merged_data_url = "data:image/png;base64," + base64.b64encode(merged_img_data).decode()
643
+
644
+ return merged_data_url
645
+
646
+ def _merge_new_annotations(self, annotations_batch):
647
+ """
648
+ Merge the new binary annotations with the existing annotations
649
+ :param annotations_batch: list of list of annotation. each batch with size self._upload_batch_size
650
+ :return: merged_annotations_batch: list of list of annotation. each batch with size self._upload_batch_size
651
+ """
652
+ for annotations in annotations_batch:
653
+ for annotation in annotations:
654
+ if annotation['type'] == 'binary' and not annotation.get('clean', False):
655
+ to_merge = [a for a in annotations if
656
+ not a.get('clean', False) and a.get("metadata", {}).get('system', {}).get('objectId',
657
+ None) ==
658
+ annotation.get("metadata", {}).get('system', {}).get('objectId', None) and a['label'] ==
659
+ annotation['label']]
660
+ if len(to_merge) == 0:
661
+ # no annotation to merge with
662
+ continue
663
+ for a in to_merge:
664
+ if a['coordinates'] == annotation['coordinates']:
665
+ continue
666
+ merged_data_url = self._merge_binary_annotations(a['coordinates'], annotation['coordinates'],
667
+ self.item.width, self.item.height)
668
+ annotation['coordinates'] = merged_data_url
669
+ a['clean'] = True
670
+ return [[a for a in annotations if not a.get('clean', False)] for annotations in annotations_batch]
671
+
672
+ def _merge_to_exits_annotations(self, annotations_batch):
673
+ filters = entities.Filters(resource=entities.FiltersResource.ANNOTATION, field='type', values='binary')
674
+ filters.add(field='itemId', values=self.item.id, method=entities.FiltersMethod.AND)
675
+ exist_annotations = self.list(filters=filters).annotations or list()
676
+ to_delete = list()
677
+ for annotations in annotations_batch:
678
+ for ann in annotations:
679
+ if ann['type'] == 'binary':
680
+ to_merge = [a for a in exist_annotations if
681
+ a.object_id == ann.get("metadata", {}).get('system', {}).get('objectId',
682
+ None) and a.label == ann[
683
+ 'label']]
684
+ if len(to_merge) == 0:
685
+ # no annotation to merge with
686
+ continue
687
+ if to_merge[0].coordinates == ann['coordinates']:
688
+ # same annotation
689
+ continue
690
+ if len(to_merge) > 1:
691
+ raise exceptions.PlatformException('400', 'Multiple annotations with the same label')
692
+ # merge
693
+ exist_annotations.remove(to_merge[0])
694
+ merged_data_url = self._merge_binary_annotations(to_merge[0].coordinates, ann['coordinates'],
695
+ self.item.width, self.item.height)
696
+ json_ann = to_merge[0].to_json()
697
+ json_ann['coordinates'] = merged_data_url
698
+ suc, response = self._update_annotation_req(annotation_json=json_ann,
699
+ system_metadata=True,
700
+ annotation_id=to_merge[0].id)
701
+ if not suc:
702
+ raise exceptions.PlatformException(response)
703
+ if suc:
704
+ result = entities.Annotation.from_json(_json=response.json(),
705
+ annotations=self,
706
+ dataset=self._dataset,
707
+ item=self._item)
708
+ exist_annotations.append(result)
709
+ to_delete.append(ann)
710
+ if len(to_delete) > 0:
711
+ annotations_batch = [[a for a in annotations if a not in to_delete] for annotations in annotations_batch]
712
+
713
+ return annotations_batch
714
+
715
+ def _upload_single_batch(self, annotation_batch):
716
+ try:
717
+ suc, response = self._client_api.gen_request(req_type='post',
718
+ path='/items/{}/annotations'.format(self.item.id),
719
+ json_req=annotation_batch)
720
+ if suc:
721
+ return_annotations = response.json()
722
+ if not isinstance(return_annotations, list):
723
+ return_annotations = [return_annotations]
724
+ else:
725
+ raise exceptions.PlatformException(response)
726
+
727
+ status = True
728
+ result = return_annotations
729
+ except Exception:
730
+ status = False
731
+ result = traceback.format_exc()
732
+
733
+ return status, result
734
+
735
+ def _upload_annotations_batches(self, annotation_batches):
736
+ if len(annotation_batches) == 1:
737
+ # no need for threads
738
+ status, result = self._upload_single_batch(annotation_batch=annotation_batches[0])
739
+ if status is False:
740
+ logger.error(result)
741
+ logger.error('Annotation/s uploaded with errors')
742
+ # TODO need to raise errors?
743
+ uploaded_annotations = result
744
+ else:
745
+ # threading
746
+ pool = self._client_api.thread_pools(pool_name='annotation.upload')
747
+ jobs = [None for _ in range(len(annotation_batches))]
748
+ for i_ann, annotations_batch in enumerate(annotation_batches):
749
+ jobs[i_ann] = pool.submit(self._upload_single_batch,
750
+ annotation_batch=annotations_batch)
751
+ # get all results
752
+ results = [j.result() for j in jobs]
753
+ uploaded_annotations = [ann for ann_list in results for ann in ann_list[1] if ann_list[0] is True]
754
+ out_errors = [r[1] for r in results if r[0] is False]
755
+ if len(out_errors) != 0:
756
+ logger.error(out_errors)
757
+ logger.error('Annotation/s uploaded with errors')
758
+ # TODO need to raise errors?
759
+ logger.info('Annotation/s uploaded successfully. num: {}'.format(len(uploaded_annotations)))
760
+ return uploaded_annotations
761
+
762
+ async def _async_upload_annotations(self, annotations, merge=False):
763
+ """
764
+ Async function to run from the uploader. will use asyncio to not break the async
765
+ :param annotations: list of all annotations
766
+ :param merge: bool - merge the new binary annotations with the existing annotations
767
+ :return:
768
+ """
769
+ async with self._client_api.event_loop.semaphore('annotations.upload'):
770
+ annotation_batch = self._create_batches_for_upload(annotations=annotations, merge=merge)
771
+ output_annotations = list()
772
+ for annotations_list in annotation_batch:
773
+ success, response = await self._client_api.gen_async_request(req_type='post',
774
+ path='/items/{}/annotations'
775
+ .format(self.item.id),
776
+ json_req=annotations_list)
777
+ if success:
778
+ return_annotations = response.json()
779
+ if not isinstance(return_annotations, list):
780
+ return_annotations = [return_annotations]
781
+ output_annotations.extend(return_annotations)
782
+ else:
783
+ if len(output_annotations) > 0:
784
+ logger.warning("Only {} annotations from {} annotations have been uploaded".
785
+ format(len(output_annotations), len(annotations)))
786
+ raise exceptions.PlatformException(response)
787
+
788
+ result = entities.AnnotationCollection.from_json(_json=output_annotations, item=self.item)
789
+ return result
790
+
791
+ @_api_reference.add(path='/items/{itemId}/annotations', method='post')
792
+ def upload(self, annotations, merge=False) -> entities.AnnotationCollection:
793
+ """
794
+ Upload a new annotation/annotations. You must first create the annotation using the annotation *builder* method.
795
+
796
+ **Prerequisites**: Any user can upload annotations.
797
+
798
+ :param List[dtlpy.entities.annotation.Annotation] or dtlpy.entities.annotation.Annotation annotations: list or
799
+ single annotation of type Annotation
800
+ :param bool merge: optional - merge the new binary annotations with the existing annotations
801
+ :return: list of annotation objects
802
+ :rtype: entities.AnnotationCollection
803
+
804
+ **Example**:
805
+
806
+ .. code-block:: python
807
+
808
+ annotations = item.annotations.upload(annotations='builder')
809
+ """
810
+ # make list if not list
811
+ if isinstance(annotations, entities.AnnotationCollection):
812
+ # get the annotation from a collection
813
+ annotations = annotations.annotations
814
+ elif isinstance(annotations, str) and os.path.isfile(annotations):
815
+ # load annotation filepath and get list of annotations
816
+ with open(annotations, 'r', encoding="utf8") as f:
817
+ annotations = json.load(f)
818
+ annotations = annotations.get('annotations', [])
819
+ # annotations = entities.AnnotationCollection.from_json_file(filepath=annotations).annotations
820
+ elif isinstance(annotations, entities.Annotation) or isinstance(annotations, dict):
821
+ # convert the single Annotation to a list
822
+ annotations = [annotations]
823
+ elif isinstance(annotations, list):
824
+ pass
825
+ else:
826
+ exceptions.PlatformException(error='400',
827
+ message='Unknown annotation format. type: {}'.format(type(annotations)))
828
+ if len(annotations) == 0:
829
+ logger.warning('Annotation upload receives 0 annotations. Not doing anything')
830
+ out_annotations = list()
831
+ else:
832
+ annotation_batches = self._create_batches_for_upload(annotations=annotations, merge=merge)
833
+ out_annotations = self._upload_annotations_batches(annotation_batches=annotation_batches)
834
+ out_annotations = entities.AnnotationCollection.from_json(_json=out_annotations,
835
+ item=self.item)
836
+ return out_annotations
837
+
838
+ def update_status(self,
839
+ annotation: entities.Annotation = None,
840
+ annotation_id: str = None,
841
+ status: entities.AnnotationStatus = entities.AnnotationStatus.ISSUE
842
+ ) -> entities.Annotation:
843
+ """
844
+ Set status on annotation.
845
+
846
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or
847
+ *developer* or be assigned a task that includes that item as an *annotation manager*.
848
+
849
+ :param dtlpy.entities.annotation.Annotation annotation: Annotation object
850
+ :param str annotation_id: optional - annotation id to set status
851
+ :param str status: can be AnnotationStatus.ISSUE, APPROVED, REVIEW, CLEAR
852
+ :return: Annotation object
853
+ :rtype: dtlpy.entities.annotation.Annotation
854
+
855
+ **Example**:
856
+
857
+ .. code-block:: python
858
+
859
+ annotation = item.annotations.update_status(annotation_id='annotation_id', status=dl.AnnotationStatus.ISSUE)
860
+ """
861
+ if annotation is None:
862
+ if annotation_id is None:
863
+ raise ValueError('must input on of "annotation" or "annotation_id"')
864
+ annotation = self.get(annotation_id=annotation_id)
865
+ if status not in list(entities.AnnotationStatus):
866
+ raise ValueError('status must be on of: {}'.format(', '.join(list(entities.AnnotationStatus))))
867
+ annotation.status = status
868
+ return annotation.update(system_metadata=True)
869
+
870
+ def builder(self):
871
+ """
872
+ Create Annotation collection.
873
+
874
+ **Prerequisites**: You must have an item to be annotated. You must have the role of an *owner* or *developer*
875
+ or be assigned a task that includes that item as an *annotation manager* or *annotator*.
876
+
877
+ :return: Annotation collection object
878
+ :rtype: dtlpy.entities.annotation_collection.AnnotationCollection
879
+
880
+ **Example**:
881
+
882
+ .. code-block:: python
883
+
884
+ annotation_collection = item.annotations.builder()
885
+ """
886
+ return entities.AnnotationCollection(item=self.item)
887
+
888
+ def task_scores(self, annotation_id: str, task_id: str, page_offset: int = 0, page_size: int = 100):
889
+ """
890
+ Get annotation scores in a task
891
+
892
+ **Prerequisites**: You must be able to read the task
893
+
894
+ :param str annotation_id: The id of the annotation
895
+ :param str task_id: The id of the task
896
+ :param int page_offset: starting page
897
+ :param int page_size: size of page
898
+ :return: json response
899
+ :rtype: dict
900
+ """
901
+ if annotation_id is None:
902
+ raise exceptions.PlatformException('400', 'annotation_id must be provided')
903
+ if task_id is None:
904
+ raise exceptions.PlatformException('400', 'task_id must be provided')
905
+
906
+ success, response = self._client_api.gen_request(req_type='get',
907
+ path='/scores/tasks/{}/annotations/{}?page={}&pageSize={}'
908
+ .format(task_id, annotation_id, page_offset, page_size))
909
+ if success:
910
+ return response.json()
911
+ else:
912
+ raise exceptions.PlatformException(response)
913
+
914
+ ##################
915
+ # async function #
916
+ ##################