dtlpy 1.113.10__py3-none-any.whl → 1.114.13__py3-none-any.whl

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