dtlpy 1.115.44__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 -347
  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 -292
  73. dtlpy/entities/collection.py +38 -38
  74. dtlpy/entities/command.py +169 -169
  75. dtlpy/entities/compute.py +449 -449
  76. dtlpy/entities/dataset.py +1299 -1299
  77. dtlpy/entities/directory_tree.py +44 -44
  78. dtlpy/entities/dpk.py +470 -470
  79. dtlpy/entities/driver.py +235 -235
  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 -798
  84. dtlpy/entities/gis_item.py +107 -107
  85. dtlpy/entities/integration.py +184 -184
  86. dtlpy/entities/item.py +959 -959
  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 -505
  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 -963
  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 -1230
  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 -152
  164. dtlpy/repositories/compositions.py +61 -61
  165. dtlpy/repositories/computes.py +439 -439
  166. dtlpy/repositories/datasets.py +1504 -1504
  167. dtlpy/repositories/downloader.py +976 -923
  168. dtlpy/repositories/dpks.py +433 -433
  169. dtlpy/repositories/drivers.py +482 -482
  170. dtlpy/repositories/executions.py +815 -815
  171. dtlpy/repositories/feature_sets.py +226 -226
  172. dtlpy/repositories/features.py +255 -255
  173. dtlpy/repositories/integrations.py +484 -484
  174. dtlpy/repositories/items.py +912 -912
  175. dtlpy/repositories/messages.py +94 -94
  176. dtlpy/repositories/models.py +1000 -1000
  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 -661
  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 -1785
  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.115.44.data → dtlpy-1.116.6.data}/scripts/dlp +1 -1
  227. dtlpy-1.116.6.data/scripts/dlp.bat +2 -0
  228. {dtlpy-1.115.44.data → dtlpy-1.116.6.data}/scripts/dlp.py +128 -128
  229. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/METADATA +186 -186
  230. dtlpy-1.116.6.dist-info/RECORD +239 -0
  231. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/WHEEL +1 -1
  232. {dtlpy-1.115.44.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.115.44.data/scripts/dlp.bat +0 -2
  236. dtlpy-1.115.44.dist-info/RECORD +0 -240
  237. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/entry_points.txt +0 -0
  238. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/top_level.txt +0 -0
@@ -1,1879 +1,1879 @@
1
- import numpy as np
2
- import traceback
3
- import logging
4
- import datetime
5
- import webvtt
6
- import copy
7
- import attr
8
- import json
9
- import os
10
- import warnings
11
-
12
- from PIL import Image
13
- from enum import Enum
14
-
15
- from .. import entities, PlatformException, repositories, ApiClient, exceptions
16
-
17
- logger = logging.getLogger(name='dtlpy')
18
-
19
-
20
- class ExportVersion(str, Enum):
21
- V1 = "V1"
22
- V2 = "V2"
23
-
24
-
25
- class AnnotationStatus(str, Enum):
26
- ISSUE = "issue"
27
- APPROVED = "approved"
28
- REVIEW = "review"
29
- CLEAR = "clear"
30
-
31
-
32
- class AnnotationType(str, Enum):
33
- BOX = "box"
34
- CUBE = "cube"
35
- CUBE3D = "cube_3d"
36
- CLASSIFICATION = "class"
37
- COMPARISON = "comparison"
38
- ELLIPSE = "ellipse"
39
- NOTE = "note"
40
- POINT = "point"
41
- POLYGON = "segment"
42
- POLYLINE = "polyline"
43
- POSE = "pose"
44
- SEGMENTATION = "binary"
45
- SUBTITLE = "subtitle"
46
- TEXT = "text_mark"
47
- GIS = "gis"
48
- SEMANTIC_3D = "ref_semantic_3d"
49
- POLYLINE_3D = "polyline_3d"
50
- FREE_TEXT = "text"
51
- REF_IMAGE = "ref_image"
52
-
53
-
54
- class ViewAnnotationOptions(str, Enum):
55
- """ The Annotations file types to download (JSON, MASK, INSTANCE, ANNOTATION_ON_IMAGE, VTT, OBJECT_ID).
56
-
57
- .. list-table::
58
- :widths: 15 150
59
- :header-rows: 1
60
-
61
- * - State
62
- - Description
63
- * - JSON
64
- - Dataloop json format
65
- * - MASK
66
- - PNG file that contains drawing annotations on it
67
- * - INSTANCE
68
- - An image file that contains 2D annotations
69
- * - ANNOTATION_ON_IMAGE
70
- - The source image with the annotations drawing in it
71
- * - VTT
72
- - An text file contains supplementary information about a web video
73
- * - OBJECT_ID
74
- - An image file that contains 2D annotations
75
-
76
- """
77
- JSON = "json"
78
- MASK = "mask"
79
- INSTANCE = "instance"
80
- ANNOTATION_ON_IMAGE = "img_mask"
81
- VTT = "vtt"
82
- OBJECT_ID = "object_id"
83
-
84
-
85
- @attr.s
86
- class Annotation(entities.BaseEntity):
87
- """
88
- Annotations object
89
- """
90
- # platform
91
- id = attr.ib()
92
- url = attr.ib(repr=False)
93
- item_url = attr.ib(repr=False)
94
- _item = attr.ib(repr=False)
95
- item_id = attr.ib()
96
- creator = attr.ib()
97
- created_at = attr.ib()
98
- updated_by = attr.ib(repr=False)
99
- updated_at = attr.ib(repr=False)
100
- type = attr.ib()
101
- source = attr.ib(repr=False)
102
- dataset_url = attr.ib(repr=False)
103
-
104
- # api
105
- _platform_dict = attr.ib(repr=False)
106
- # meta
107
- metadata = attr.ib(repr=False)
108
- fps = attr.ib(repr=False)
109
- hash = attr.ib(default=None, repr=False)
110
- dataset_id = attr.ib(default=None, repr=False)
111
- status = attr.ib(default=None, repr=False)
112
- object_id = attr.ib(default=None, repr=False)
113
- automated = attr.ib(default=None, repr=False)
114
- item_height = attr.ib(default=None)
115
- item_width = attr.ib(default=None)
116
- label_suggestions = attr.ib(default=None)
117
-
118
- # annotation definition
119
- _annotation_definition = attr.ib(default=None, repr=False, type=entities.BaseAnnotationDefinition)
120
-
121
- # snapshots
122
- frames = attr.ib(default=None, repr=False)
123
- current_frame = attr.ib(default=0, repr=False)
124
-
125
- # video attributes
126
- _end_frame = attr.ib(default=0, repr=False)
127
- _end_time = attr.ib(default=0, repr=False)
128
- _start_frame = attr.ib(default=0)
129
- _start_time = attr.ib(default=0)
130
-
131
- # sdk
132
- _dataset = attr.ib(repr=False, default=None)
133
- _datasets = attr.ib(repr=False, default=None)
134
- _annotations = attr.ib(repr=False, default=None)
135
- __client_api = attr.ib(default=None, repr=False)
136
- _items = attr.ib(repr=False, default=None)
137
- _recipe_1_attributes = attr.ib(repr=False, default=None)
138
-
139
- ############
140
- # Platform #
141
- ############
142
-
143
- @property
144
- def annotation_definition(self):
145
- return self._annotation_definition
146
-
147
- @annotation_definition.setter
148
- def annotation_definition(self, ann_def):
149
- if ann_def is not None:
150
- ann_def._annotation = self
151
- self._annotation_definition = ann_def
152
-
153
- @property
154
- def createdAt(self):
155
- return self.created_at
156
-
157
- @property
158
- def updatedAt(self):
159
- return self.updated_at
160
-
161
- @property
162
- def updatedBy(self):
163
- return self.updated_by
164
-
165
- @property
166
- def _client_api(self) -> ApiClient:
167
- if self.__client_api is None:
168
- if self._item is None:
169
- raise PlatformException('400',
170
- 'This action cannot be performed without an item entity. Please set item')
171
- else:
172
- self.__client_api = self._item._client_api
173
- assert isinstance(self.__client_api, ApiClient)
174
- return self.__client_api
175
-
176
- @property
177
- def dataset(self):
178
- if self._dataset is None:
179
- if self._item is not None:
180
- # get from item
181
- self._dataset = self._item.dataset
182
- else:
183
- # get directly
184
- self._dataset = self.datasets.get(dataset_id=self.dataset_id)
185
- assert isinstance(self._dataset, entities.Dataset)
186
- return self._dataset
187
-
188
- @property
189
- def item(self):
190
- if self._item is None:
191
- self._item = self.items.get(item_id=self.item_id)
192
- assert isinstance(self._item, entities.Item)
193
- return self._item
194
-
195
- @property
196
- def annotations(self):
197
- if self._annotations is None:
198
- self._annotations = repositories.Annotations(client_api=self._client_api, item=self._item)
199
- assert isinstance(self._annotations, repositories.Annotations)
200
- return self._annotations
201
-
202
- @property
203
- def datasets(self):
204
- if self._datasets is None:
205
- self._datasets = repositories.Datasets(client_api=self._client_api)
206
- assert isinstance(self._datasets, repositories.Datasets)
207
- return self._datasets
208
-
209
- @property
210
- def items(self):
211
- if self._items is None:
212
- if self._datasets is not None:
213
- self._items = self._dataset.items
214
- elif self._item is not None:
215
- self._items = self._item.items
216
- else:
217
- self._items = repositories.Items(client_api=self._client_api, dataset=self._dataset)
218
- assert isinstance(self._items, repositories.Items)
219
- return self._items
220
-
221
- #########################
222
- # Annotation Properties #
223
- #########################
224
- @property
225
- def parent_id(self):
226
- try:
227
- parent_id = self.metadata['system']['parentId']
228
- except KeyError:
229
- parent_id = None
230
- return parent_id
231
-
232
- @parent_id.setter
233
- def parent_id(self, parent_id):
234
- if 'system' not in self.metadata:
235
- self.metadata['system'] = dict()
236
- self.metadata['system']['parentId'] = parent_id
237
-
238
- @property
239
- def coordinates(self):
240
- color = None
241
- if self.type in ['binary']:
242
- color = self.color
243
- coordinates = self.annotation_definition.to_coordinates(color=color)
244
- return coordinates
245
-
246
- @property
247
- def start_frame(self):
248
- return self._start_frame
249
-
250
- @start_frame.setter
251
- def start_frame(self, val):
252
- if not isinstance(val, float) and not isinstance(val, int):
253
- raise ValueError('Must input a valid number')
254
- self._start_frame = round(val)
255
- self._start_time = val / self.fps if self.fps else 0
256
- self.frames.start = self._start_frame
257
-
258
- @property
259
- def end_frame(self):
260
- return self._end_frame
261
-
262
- @end_frame.setter
263
- def end_frame(self, val):
264
- if not isinstance(val, float) and not isinstance(val, int):
265
- raise ValueError('Must input a valid number')
266
- self._end_frame = round(val)
267
- self._end_time = val / self.fps if self.fps else 0
268
- self.frames.end = self._end_frame
269
-
270
- @property
271
- def start_time(self):
272
- return self._start_time
273
-
274
- @start_time.setter
275
- def start_time(self, val):
276
- if not isinstance(val, float) and not isinstance(val, int):
277
- raise ValueError('Must input a valid number')
278
- self._start_frame = round(val * self.fps if self.fps else 0)
279
- self._start_time = val
280
- self.frames.start = self._start_frame
281
-
282
- @property
283
- def end_time(self):
284
- return self._end_time
285
-
286
- @end_time.setter
287
- def end_time(self, val):
288
- if not isinstance(val, float) and not isinstance(val, int):
289
- raise ValueError('Must input a valid number')
290
- self._end_frame = round(val * self.fps if self.fps else 0)
291
- self._end_time = val
292
- self.frames.end = self._end_frame
293
-
294
- @property
295
- def x(self):
296
- return self.annotation_definition.x
297
-
298
- @property
299
- def y(self):
300
- return self.annotation_definition.y
301
-
302
- @property
303
- def rx(self):
304
- if self.annotation_definition.type == 'ellipse':
305
- return self.annotation_definition.rx
306
- else:
307
- return None
308
-
309
- @property
310
- def ry(self):
311
- if self.annotation_definition.type == 'ellipse':
312
- return self.annotation_definition.ry
313
- else:
314
- return None
315
-
316
- @property
317
- def angle(self):
318
- if self.annotation_definition.type in ['ellipse', 'cube', 'box']:
319
- return self.annotation_definition.angle
320
- else:
321
- return None
322
-
323
- @property
324
- def messages(self):
325
- if hasattr(self.annotation_definition, 'messages'):
326
- return self.annotation_definition.messages
327
- else:
328
- return None
329
-
330
- @messages.setter
331
- def messages(self, messages):
332
- if self.type == 'note':
333
- self.annotation_definition.messages = messages
334
- else:
335
- raise PlatformException('400', 'Annotation of type {} does not have attribute messages'.format(self.type))
336
-
337
- def add_message(self, body: str = None):
338
- if self.type == 'note':
339
- return self.annotation_definition.add_message(body=body)
340
- else:
341
- raise PlatformException('400', 'Annotation of type {} does not have method add_message'.format(self.type))
342
-
343
- @property
344
- def geo(self):
345
- return self.annotation_definition.geo
346
-
347
- @geo.setter
348
- def geo(self, geo):
349
- self.annotation_definition.geo = geo
350
-
351
- @property
352
- def top(self):
353
- return self.annotation_definition.top
354
-
355
- @top.setter
356
- def top(self, top):
357
- self.annotation_definition.top = top
358
-
359
- @property
360
- def bottom(self):
361
- return self.annotation_definition.bottom
362
-
363
- @bottom.setter
364
- def bottom(self, bottom):
365
- self.annotation_definition.bottom = bottom
366
-
367
- @property
368
- def left(self):
369
- return self.annotation_definition.left
370
-
371
- @left.setter
372
- def left(self, left):
373
- self.annotation_definition.left = left
374
-
375
- @property
376
- def right(self):
377
- return self.annotation_definition.right
378
-
379
- @right.setter
380
- def right(self, right):
381
- self.annotation_definition.right = right
382
-
383
- @property
384
- def height(self):
385
- return self.annotation_definition.height
386
-
387
- @height.setter
388
- def height(self, height):
389
- self.annotation_definition.height = height
390
-
391
- @property
392
- def width(self):
393
- return self.annotation_definition.width
394
-
395
- @width.setter
396
- def width(self, width):
397
- self.annotation_definition.width = width
398
-
399
- @property
400
- def description(self):
401
- return self.annotation_definition.description
402
-
403
- @description.setter
404
- def description(self, description):
405
- if description is None:
406
- description = ""
407
- if not isinstance(description, str):
408
- raise ValueError("Description must get string")
409
- self.annotation_definition.description = description
410
-
411
- @property
412
- def last_frame(self):
413
- if len(self.frames.actual_keys()) == 0:
414
- return 0
415
- return max(self.frames.actual_keys())
416
-
417
- @property
418
- def label(self):
419
- return self.annotation_definition.label
420
-
421
- @label.setter
422
- def label(self, label):
423
- self.annotation_definition.label = label
424
-
425
- @property
426
- def attributes(self):
427
- return self.annotation_definition.attributes
428
-
429
- @attributes.setter
430
- def attributes(self, attributes):
431
- self.annotation_definition.attributes = attributes
432
-
433
- @property
434
- def color(self):
435
- # if "dataset" is not in self - this will always get the dataset
436
- try:
437
- colors = self.dataset._get_ontology().color_map
438
- except (exceptions.BadRequest, exceptions.NotFound):
439
- colors = None
440
- logger.warning('Cant get dataset for annotation color. using default.')
441
- if colors is not None and self.label in colors:
442
- color = colors[self.label]
443
- else:
444
- if self.type == 'binary' and self.annotation_definition._color is not None:
445
- color = self.annotation_definition._color
446
- else:
447
- color = (255, 255, 255)
448
- return color
449
-
450
- @color.setter
451
- def color(self, color):
452
- if self.type == 'binary':
453
- if not isinstance(color, tuple) or len(color) != 3:
454
- raise ValueError("Color must get tuple of length 3")
455
- self.annotation_definition._color = color
456
- else:
457
- raise exceptions.BadRequest(
458
- status_code='400',
459
- message='Invalid annotation type - Updating color is only available to binary annotation'
460
- )
461
-
462
- ####################
463
- # frame attributes #
464
- ####################
465
- @property
466
- def frame_num(self):
467
- if len(self.frames.actual_keys()) > 0:
468
- return self.current_frame
469
- else:
470
- return self.start_frame
471
-
472
- @frame_num.setter
473
- def frame_num(self, frame_num):
474
- if frame_num != self.current_frame:
475
- self.frames[self.current_frame].frame_num = frame_num
476
- self.frames[frame_num] = self.frames[self.current_frame]
477
- self.frames.pop(self.current_frame)
478
-
479
- @property
480
- def fixed(self):
481
- if len(self.frames.actual_keys()) > 0:
482
- return self.frames[self.current_frame].fixed
483
- else:
484
- return False
485
-
486
- @fixed.setter
487
- def fixed(self, fixed):
488
- if len(self.frames.actual_keys()) > 0:
489
- self.frames[self.current_frame].fixed = fixed
490
-
491
- @property
492
- def object_visible(self):
493
- if len(self.frames.actual_keys()) > 0:
494
- return self.frames[self.current_frame].object_visible
495
- else:
496
- return False
497
-
498
- @object_visible.setter
499
- def object_visible(self, object_visible):
500
- if len(self.frames.actual_keys()) > 0:
501
- self.frames[self.current_frame].object_visible = object_visible
502
-
503
- @property
504
- def is_video(self):
505
- if len(self.frames.actual_keys()) == 0:
506
- return False
507
- else:
508
- return True
509
-
510
- ##################
511
- # entity methods #
512
- ##################
513
- def update_status(self, status: AnnotationStatus = AnnotationStatus.ISSUE):
514
- """
515
- Set status on annotation
516
-
517
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or *developer* or be assigned a task that includes that item as an *annotation manager*.
518
-
519
- :param str status: can be AnnotationStatus.ISSUE, AnnotationStatus.APPROVED, AnnotationStatus.REVIEW, AnnotationStatus.CLEAR
520
- :return: Annotation object
521
- :rtype: dtlpy.entities.annotation.Annotation
522
-
523
- **Example**:
524
-
525
- .. code-block:: python
526
-
527
- annotation = annotation.update_status(status=dl.AnnotationStatus.ISSUE)
528
- """
529
- return self.annotations.update_status(annotation=self, status=status)
530
-
531
- def delete(self):
532
- """
533
- Remove an annotation from item
534
-
535
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
536
-
537
- :return: True if success
538
- :rtype: bool
539
-
540
- **Example**:
541
-
542
- .. code-block:: python
543
-
544
- is_deleted = annotation.delete()
545
- """
546
- if self.id is None:
547
- raise PlatformException(
548
- '400',
549
- 'Cannot delete annotation because it was not fetched from platform and therefore does not have an id'
550
- )
551
- return self.annotations.delete(annotation_id=self.id)
552
-
553
- def update(self, system_metadata=False):
554
- """
555
- Update an existing annotation in host.
556
-
557
- **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
558
-
559
- :param system_metadata: True, if you want to change metadata system
560
- :return: Annotation object
561
- :rtype: dtlpy.entities.annotation.Annotation
562
-
563
- **Example**:
564
-
565
- .. code-block:: python
566
-
567
- annotation = annotation.update()
568
- """
569
- return self.annotations.update(annotations=self,
570
- system_metadata=system_metadata)[0]
571
-
572
- def upload(self):
573
- """
574
- Create a new annotation in host
575
-
576
- **Prerequisites**: Any user can upload annotations.
577
-
578
- :return: Annotation entity
579
- :rtype: dtlpy.entities.annotation.Annotation
580
- """
581
- return self.annotations.upload(annotations=self)[0]
582
-
583
- def download(self,
584
- filepath: str,
585
- annotation_format: ViewAnnotationOptions = ViewAnnotationOptions.JSON,
586
- height: float = None,
587
- width: float = None,
588
- thickness: int = 1,
589
- with_text: bool = False,
590
- alpha: float = 1):
591
- """
592
- Save annotation to file
593
-
594
- **Prerequisites**: Any user can upload annotations.
595
-
596
- :param str filepath: local path to where annotation will be downloaded to
597
- :param list annotation_format: options: list(dl.ViewAnnotationOptions)
598
- :param float height: image height
599
- :param float width: image width
600
- :param int thickness: line thickness
601
- :param bool with_text: get mask with text
602
- :param float alpha: opacity value [0 1], default 1
603
- :return: filepath
604
- :rtype: str
605
-
606
- **Example**:
607
-
608
- .. code-block:: python
609
-
610
- filepath = annotation.download(filepath='filepath', annotation_format=dl.ViewAnnotationOptions.MASK)
611
- """
612
- _, ext = os.path.splitext(filepath)
613
- if ext == '':
614
- if annotation_format == ViewAnnotationOptions.JSON:
615
- ext = '.json'
616
- else:
617
- if self.is_video:
618
- ext = '.mp4'
619
- else:
620
- ext = '.png'
621
- filepath = os.path.join(filepath, self.id + ext)
622
- os.makedirs(os.path.dirname(filepath), exist_ok=True)
623
- if annotation_format == ViewAnnotationOptions.JSON:
624
- with open(filepath, 'w') as f:
625
- json.dump(self.to_json(), f, indent=2)
626
- elif annotation_format == entities.ViewAnnotationOptions.VTT:
627
- _, ext = os.path.splitext(filepath)
628
- if ext != '.vtt':
629
- raise PlatformException(error='400', message='file extension must be vtt')
630
- vtt = webvtt.WebVTT()
631
- if self.type in ['subtitle']:
632
- s = str(datetime.timedelta(seconds=self.start_time))
633
- if len(s.split('.')) == 1:
634
- s += '.000'
635
- e = str(datetime.timedelta(seconds=self.end_time))
636
- if len(e.split('.')) == 1:
637
- e += '.000'
638
- caption = webvtt.Caption(
639
- '{}'.format(s),
640
- '{}'.format(e),
641
- '{}'.format(self.coordinates['text'])
642
- )
643
- vtt.captions.append(caption)
644
- vtt.save(filepath)
645
- else:
646
- if self.is_video:
647
- entities.AnnotationCollection(item=self.item, annotations=[self])._video_maker(
648
- input_filepath=None,
649
- output_filepath=filepath,
650
- annotation_format=annotation_format,
651
- thickness=thickness,
652
- alpha=alpha,
653
- with_text=with_text
654
- )
655
- else:
656
- mask = self.show(thickness=thickness,
657
- alpha=alpha,
658
- with_text=with_text,
659
- height=height,
660
- width=width,
661
- annotation_format=annotation_format)
662
- img = Image.fromarray(mask.astype(np.uint8))
663
- img.save(filepath)
664
- return filepath
665
-
666
- def set_frame(self, frame):
667
- """
668
- Set annotation to frame state
669
-
670
- **Prerequisites**: Any user can upload annotations.
671
-
672
- :param int frame: frame number
673
- :return: True if success
674
- :rtype: bool
675
-
676
- **Example**:
677
-
678
- .. code-block:: python
679
-
680
- success = annotation.set_frame(frame=10)
681
- """
682
- if frame in self.frames:
683
- self.current_frame = frame
684
- self.annotation_definition = self.frames[frame].annotation_definition
685
- return True
686
- else:
687
- return False
688
-
689
- ############
690
- # Plotting #
691
- ############
692
- def show(self,
693
- image=None,
694
- thickness=None,
695
- with_text=False,
696
- height=None,
697
- width=None,
698
- annotation_format: ViewAnnotationOptions = ViewAnnotationOptions.MASK,
699
- color=None,
700
- label_instance_dict=None,
701
- alpha=1,
702
- frame_num=None
703
- ):
704
- """
705
- Show annotations
706
- mark the annotation of the image array and return it
707
-
708
- **Prerequisites**: Any user can upload annotations.
709
-
710
- :param image: empty or image to draw on
711
- :param int thickness: line thickness
712
- :param bool with_text: add label to annotation
713
- :param float height: height
714
- :param float width: width
715
- :param dl.ViewAnnotationOptions annotation_format: list(dl.ViewAnnotationOptions)
716
- :param tuple color: optional - color tuple
717
- :param label_instance_dict: the instance labels
718
- :param float alpha: opacity value [0 1], default 1
719
- :param int frame_num: for video annotation, show specific fame
720
- :return: list or single ndarray of the annotations
721
-
722
- **Exampls**:
723
-
724
- .. code-block:: python
725
-
726
- image = annotation.show(image='ndarray',
727
- thickness=1,
728
- annotation_format=dl.VIEW_ANNOTATION_OPTIONS_MASK,
729
- )
730
- """
731
- try:
732
- import cv2
733
- except (ImportError, ModuleNotFoundError):
734
- logger.error(
735
- 'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
736
- raise
737
- # sanity check for alpha
738
- if alpha > 1 or alpha < 0:
739
- raise ValueError(
740
- 'input variable alpha in annotation.show() should be between 0 and 1. got: {!r}'.format(alpha))
741
- if height is None:
742
- if self._item is None or self._item.height is None:
743
- if image is None:
744
- raise PlatformException(error='400', message='must provide item width and height')
745
- else:
746
- height = image.shape[0]
747
- else:
748
- height = self._item.height
749
- if width is None:
750
- if self._item is None or self._item.width is None:
751
- if image is None:
752
- raise PlatformException(error='400', message='must provide item width and height')
753
- else:
754
- width = image.shape[1]
755
- else:
756
- width = self._item.width
757
-
758
- # try getting instance map from dataset, otherwise set to default
759
- if annotation_format in [entities.ViewAnnotationOptions.INSTANCE, entities.ViewAnnotationOptions.OBJECT_ID] and \
760
- label_instance_dict is None:
761
- if self._dataset is not None:
762
- label_instance_dict = self._dataset.instance_map
763
- else:
764
- if self._item is not None and self._item._dataset is not None:
765
- label_instance_dict = self._item._dataset.instance_map
766
- if label_instance_dict is None:
767
- logger.warning("Couldn't set label_instance_dict in annotation.show(). All labels will be mapped to 1")
768
- label_instance_dict = dict()
769
-
770
- in_video_range = True
771
- if frame_num is not None:
772
- in_video_range = self.set_frame(frame_num)
773
-
774
- if self.is_video and not in_video_range:
775
- if image is None:
776
- image = self._get_default_mask(annotation_format=annotation_format,
777
- width=width,
778
- height=height,
779
- label_instance_dict=label_instance_dict)
780
- return image
781
- else:
782
- image = self._show_single_frame(image=image,
783
- thickness=thickness,
784
- alpha=alpha,
785
- with_text=with_text,
786
- height=height,
787
- width=width,
788
- annotation_format=annotation_format,
789
- color=color,
790
- label_instance_dict=label_instance_dict)
791
- return image
792
-
793
- def _show_single_frame(self,
794
- image=None,
795
- thickness=None,
796
- with_text=False,
797
- height=None,
798
- width=None,
799
- annotation_format: ViewAnnotationOptions = ViewAnnotationOptions.MASK,
800
- color=None,
801
- label_instance_dict=None,
802
- alpha=None):
803
- """
804
- Show annotations
805
- mark the annotation of the single frame array and return it
806
- :param image: empty or image to draw on
807
- :param thickness: line thickness
808
- :param with_text: add label to annotation
809
- :param height: height
810
- :param width: width
811
- :param annotation_format: list(dl.ViewAnnotationOptions)
812
- :param color: optional - color tuple
813
- :param label_instance_dict: the instance labels
814
- :param alpha: opacity value [0 1], default 1
815
- :return: ndarray of the annotations
816
- """
817
- try:
818
- import cv2
819
- except (ImportError, ModuleNotFoundError):
820
- logger.error(
821
- 'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
822
- raise
823
- # height/width
824
- if self.annotation_definition.type == 'cube_3d':
825
- logger.warning('the show for 3d_cube is not supported.')
826
- return image
827
- if annotation_format == entities.ViewAnnotationOptions.MASK:
828
- # create an empty mask
829
- if image is None:
830
- image = self._get_default_mask(annotation_format=annotation_format,
831
- width=width,
832
- height=height,
833
- label_instance_dict=label_instance_dict)
834
- else:
835
- if len(image.shape) == 2:
836
- # image is gray
837
- image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGBA)
838
- elif image.shape[2] == 3:
839
- image = cv2.cvtColor(image, cv2.COLOR_RGB2RGBA)
840
- elif image.shape[2] == 4:
841
- pass
842
- else:
843
- raise PlatformException(
844
- error='1001',
845
- message='Unknown image shape. expected depth: gray or RGB or RGBA. got: {}'.format(image.shape))
846
- elif annotation_format == entities.ViewAnnotationOptions.ANNOTATION_ON_IMAGE:
847
- if image is None:
848
- raise PlatformException(error='1001',
849
- message='Must input image with ANNOTATION_ON_IMAGE view option.')
850
- elif annotation_format in [entities.ViewAnnotationOptions.INSTANCE, entities.ViewAnnotationOptions.OBJECT_ID]:
851
- if image is None:
852
- # create an empty mask
853
- image = self._get_default_mask(annotation_format=annotation_format,
854
- width=width,
855
- height=height,
856
- label_instance_dict=label_instance_dict)
857
- else:
858
- if len(image.shape) != 2:
859
- raise PlatformException(
860
- error='1001',
861
- message='Image shape must be 2d array when trying to draw instance on image')
862
- # create a dictionary of labels and ids
863
- else:
864
- raise PlatformException(error='1001',
865
- message='unknown annotations format: "{}". known formats: "{}"'.format(
866
- annotation_format, '", "'.join(list(entities.ViewAnnotationOptions))))
867
- # color
868
- if color is None:
869
- if annotation_format in [entities.ViewAnnotationOptions.MASK,
870
- entities.ViewAnnotationOptions.INSTANCE,
871
- entities.ViewAnnotationOptions.OBJECT_ID]:
872
- color = self._get_color_for_show(annotation_format=annotation_format,
873
- alpha=alpha,
874
- label_instance_dict=label_instance_dict)
875
- else:
876
- raise PlatformException(error='1001',
877
- message='unknown annotations format: {}. known formats: "{}"'.format(
878
- annotation_format, '", "'.join(list(entities.ViewAnnotationOptions))))
879
-
880
- return self.annotation_definition.show(image=image,
881
- thickness=thickness,
882
- with_text=with_text,
883
- height=height,
884
- width=width,
885
- annotation_format=annotation_format,
886
- color=color,
887
- alpha=alpha)
888
-
889
- def _get_color_for_show(self, annotation_format, alpha=1, label_instance_dict=None):
890
- if annotation_format == entities.ViewAnnotationOptions.MASK:
891
- color = self.color
892
- if len(color) == 3:
893
- color = color + (int(alpha * 255),)
894
- elif annotation_format == entities.ViewAnnotationOptions.INSTANCE:
895
- color = label_instance_dict.get(self.label, 1)
896
- elif annotation_format == entities.ViewAnnotationOptions.OBJECT_ID:
897
- if self.object_id is None:
898
- raise ValueError('Try to show object_id but annotation has no value. annotation id: {}'.format(self.id))
899
- color = int(self.object_id)
900
- else:
901
- raise ValueError('cant find color for the annotation format: {}'.format(annotation_format))
902
- return color
903
-
904
- def _get_default_mask(self, annotation_format, height, width, label_instance_dict):
905
- """
906
- Create a default mask take from the colors or instance map. tag should '$default'.
907
- Default value is zeros
908
-
909
- :param annotation_format: dl.AnnotationOptions to show
910
- :param height: item height
911
- :param width: item width
912
- :return:
913
- """
914
- if annotation_format == entities.ViewAnnotationOptions.MASK:
915
- try:
916
- colors = self.dataset._get_ontology().color_map
917
- except exceptions.BadRequest:
918
- colors = None
919
- logger.warning('Cant get dataset for annotation color. using default.')
920
- if colors is not None and '$default' in colors:
921
- default_color = colors['$default']
922
- else:
923
- default_color = None
924
- if default_color is None:
925
- default_color = (0, 0, 0, 0)
926
- if len(default_color) == 3:
927
- default_color += (255,)
928
- if len(default_color) != 4:
929
- raise ValueError('default color for show() mask should be with len 3 or 4 (RGB/RGBA)')
930
- image = np.full(shape=(height, width, 4), fill_value=default_color, dtype=np.uint8)
931
- elif annotation_format in [entities.ViewAnnotationOptions.INSTANCE,
932
- entities.ViewAnnotationOptions.OBJECT_ID]:
933
- default_color = 0
934
- if '$default' in label_instance_dict:
935
- default_color = label_instance_dict['$default']
936
- image = np.full(shape=(height, width), fill_value=default_color, dtype=np.uint8)
937
- else:
938
- raise ValueError('cant create default mask for annotation_format: {}'.format(annotation_format))
939
- return image
940
-
941
- #######
942
- # I/O #
943
- #######
944
- @classmethod
945
- def new(cls,
946
- item=None,
947
- annotation_definition=None,
948
- object_id=None,
949
- automated=True,
950
- metadata=None,
951
- frame_num=None,
952
- parent_id=None,
953
- start_time=None,
954
- item_height=None,
955
- item_width=None,
956
- end_time=None):
957
- """
958
- Create a new annotation object annotations
959
-
960
- **Prerequisites**: Any user can upload annotations.
961
-
962
- :param dtlpy.entities.item.Items item: item to annotate
963
- :param annotation_definition: annotation type object
964
- :param str object_id: object_id
965
- :param bool automated: is automated
966
- :param dict metadata: metadata
967
- :param int frame_num: optional - first frame number if video annotation
968
- :param str parent_id: add parent annotation ID
969
- :param start_time: optional - start time if video annotation
970
- :param float item_height: annotation item's height
971
- :param float item_width: annotation item's width
972
- :param end_time: optional - end time if video annotation
973
- :return: annotation object
974
- :rtype: dtlpy.entities.annotation.Annotation
975
-
976
- **Example**:
977
-
978
- .. code-block:: python
979
-
980
- annotation = annotation.new(item='item_entity,
981
- annotation_definition=dl.Box(top=10,left=10,bottom=100, right=100,label='labelName'))
982
- )
983
- """
984
- if frame_num is None:
985
- frame_num = 0
986
-
987
- if object_id is not None:
988
- if isinstance(object_id, int):
989
- object_id = '{}'.format(object_id)
990
- elif not isinstance(object_id, str) or not object_id.isnumeric():
991
- raise ValueError('Object id must be an int or a string containing only numbers.')
992
-
993
- # init annotations
994
- if metadata is None:
995
- metadata = dict()
996
-
997
- # add parent
998
- if parent_id is not None:
999
- if 'system' not in metadata:
1000
- metadata['system'] = dict()
1001
- metadata['system']['parentId'] = parent_id
1002
-
1003
- # add note status to metadata
1004
- if annotation_definition is not None and annotation_definition.type == 'note':
1005
- if 'system' not in metadata:
1006
- metadata['system'] = dict()
1007
- metadata['system']['status'] = annotation_definition.status
1008
-
1009
- # handle fps
1010
- fps = None
1011
- if item is not None:
1012
- if item.fps is not None:
1013
- fps = item.fps
1014
- elif item.mimetype is not None and 'audio' in item.mimetype:
1015
- fps = 1000
1016
-
1017
- # get type
1018
- ann_type = None
1019
- if annotation_definition is not None:
1020
- ann_type = annotation_definition.type
1021
-
1022
- # dataset
1023
- dataset_url = None
1024
- dataset_id = None
1025
- if item is not None:
1026
- dataset_url = item.dataset_url
1027
- dataset_id = item.dataset_id
1028
-
1029
- if start_time is None:
1030
- if fps is not None and frame_num is not None:
1031
- start_time = frame_num / fps if fps != 0 else 0
1032
- else:
1033
- start_time = 0
1034
- if end_time is None:
1035
- end_time = start_time
1036
-
1037
- if frame_num is None:
1038
- frame_num = 0
1039
-
1040
- # frames
1041
- frames = entities.ReflectDict(
1042
- value_type=FrameAnnotation,
1043
- on_access=Annotation.on_access,
1044
- start=frame_num,
1045
- end=frame_num
1046
- )
1047
-
1048
- res = cls(
1049
- # platform
1050
- id=None,
1051
- url=None,
1052
- item_url=None,
1053
- item=item,
1054
- item_id=None,
1055
- creator=None,
1056
- created_at=None,
1057
- updated_by=None,
1058
- updated_at=None,
1059
- object_id=object_id,
1060
- type=ann_type,
1061
- dataset_url=dataset_url,
1062
- dataset_id=dataset_id,
1063
- item_height=item_height,
1064
- item_width=item_width,
1065
-
1066
- # meta
1067
- metadata=metadata,
1068
- fps=fps,
1069
- status=None,
1070
- automated=automated,
1071
-
1072
- # snapshots
1073
- frames=frames,
1074
-
1075
- # video only attributes
1076
- end_frame=frame_num,
1077
- end_time=end_time,
1078
- start_frame=frame_num,
1079
- start_time=start_time,
1080
-
1081
- # temp
1082
- platform_dict=dict(),
1083
- source='sdk'
1084
- )
1085
-
1086
- if annotation_definition:
1087
- res.annotation_definition = annotation_definition
1088
-
1089
- if annotation_definition and annotation_definition.attributes is not None:
1090
- res.attributes = annotation_definition.attributes
1091
-
1092
- return res
1093
-
1094
- def add_frames(self,
1095
- annotation_definition,
1096
- frame_num=None,
1097
- end_frame_num=None,
1098
- start_time=None,
1099
- end_time=None,
1100
- fixed=True,
1101
- object_visible=True):
1102
- """
1103
- Add a frames state to annotation
1104
-
1105
- **Prerequisites**: Any user can upload annotations.
1106
-
1107
- :param annotation_definition: annotation type object - must be same type as annotation
1108
- :param int frame_num: first frame number
1109
- :param int end_frame_num: last frame number
1110
- :param start_time: starting time for video
1111
- :param end_time: ending time for video
1112
- :param bool fixed: is fixed
1113
- :param bool object_visible: does the annotated object is visible
1114
- :return:
1115
-
1116
- **Example**:
1117
-
1118
- .. code-block:: python
1119
-
1120
- annotation.add_frames(frame_num=10,
1121
- annotation_definition=dl.Box(top=10,left=10,bottom=100, right=100,label='labelName'))
1122
- )
1123
- """
1124
- # handle fps
1125
- if self.fps is None:
1126
- if self._item is not None:
1127
- if self._item.fps is not None:
1128
- self.fps = self._item.fps
1129
- if self.fps is None:
1130
- raise PlatformException('400', 'Annotation must have fps in order to perform this action')
1131
-
1132
- if frame_num is None:
1133
- frame_num = int(np.round(start_time * self.fps))
1134
-
1135
- if end_frame_num is None:
1136
- if end_time is not None:
1137
- end_frame_num = int(np.round(end_time * self.fps))
1138
- else:
1139
- end_frame_num = frame_num
1140
-
1141
- for frame in range(frame_num, end_frame_num + 1):
1142
- self.add_frame(annotation_definition=annotation_definition,
1143
- frame_num=frame,
1144
- fixed=fixed,
1145
- object_visible=object_visible)
1146
-
1147
- def add_frame(self,
1148
- annotation_definition,
1149
- frame_num=None,
1150
- fixed=True,
1151
- object_visible=True):
1152
- """
1153
- Add a frame state to annotation
1154
-
1155
- :param annotation_definition: annotation type object - must be same type as annotation
1156
- :param int frame_num: frame number
1157
- :param bool fixed: is fixed
1158
- :param bool object_visible: does the annotated object is visible
1159
- :return: True if success
1160
- :rtype: bool
1161
-
1162
- **Example**:
1163
-
1164
- .. code-block:: python
1165
-
1166
- success = annotation.add_frame(frame_num=10,
1167
- annotation_definition=dl.Box(top=10,left=10,bottom=100, right=100,label='labelName'))
1168
- )
1169
- """
1170
- # handle fps
1171
- if self.fps is None:
1172
- if self._item is not None:
1173
- if self._item.fps is not None:
1174
- self.fps = self._item.fps
1175
- if self.fps is None:
1176
- raise PlatformException('400', 'Annotation must have fps in order to perform this action')
1177
-
1178
- # if this is first frame
1179
- if self.annotation_definition is None:
1180
-
1181
- if frame_num is None:
1182
- frame_num = 0
1183
-
1184
- frame = FrameAnnotation.new(annotation_definition=annotation_definition,
1185
- frame_num=frame_num,
1186
- fixed=fixed,
1187
- object_visible=object_visible,
1188
- annotation=self)
1189
-
1190
- self.frames[frame_num] = frame
1191
- self.set_frame(frame_num)
1192
- self.current_frame = frame_num
1193
- self.end_frame = frame_num
1194
- self.type = annotation_definition.type
1195
-
1196
- return True
1197
-
1198
- # check if type matches annotation
1199
- if not isinstance(annotation_definition, type(self.annotation_definition)):
1200
- raise PlatformException('400', 'All frames in annotation must have same type')
1201
-
1202
- # find frame number
1203
- if frame_num is None:
1204
- frame_num = self.last_frame + 1
1205
- elif frame_num < self.start_frame:
1206
- self.start_frame = frame_num
1207
-
1208
- # add frame to annotation
1209
- if not self.is_video:
1210
- # create first frame from annotation definition
1211
- frame = FrameAnnotation.new(annotation_definition=self.annotation_definition,
1212
- frame_num=self.last_frame,
1213
- fixed=fixed,
1214
- object_visible=object_visible,
1215
- annotation=self)
1216
-
1217
- self.frames[self.start_frame] = frame
1218
-
1219
- # create new time annotations
1220
- frame = FrameAnnotation.new(annotation_definition=annotation_definition,
1221
- frame_num=frame_num,
1222
- fixed=fixed,
1223
- object_visible=object_visible,
1224
- annotation=self)
1225
-
1226
- self.frames[frame_num] = frame
1227
- if not self.frames[frame_num].fixed and self.start_frame < frame_num:
1228
- frame_last = self.frames[frame_num - 1].to_snapshot()
1229
- frame_current = self.frames[frame_num].to_snapshot()
1230
- frame_last.pop('frame')
1231
- frame_current.pop('frame')
1232
- if frame_last == frame_current:
1233
- self.frames[frame_num]._interpolation = True
1234
- self.end_frame = max(self.last_frame, frame_num)
1235
-
1236
- return True
1237
-
1238
- @staticmethod
1239
- def _protected_from_json(_json,
1240
- item=None,
1241
- client_api=None,
1242
- annotations=None,
1243
- is_video=None,
1244
- fps=None,
1245
- item_metadata=None,
1246
- dataset=None):
1247
- """
1248
- Same as from_json but with try-except to catch if error
1249
-
1250
- :param _json: platform json
1251
- :param item: item
1252
- :param client_api: ApiClient entity
1253
- :param annotations:
1254
- :param is_video:
1255
- :param fps:
1256
- :param item_metadata:
1257
- :param dataset
1258
- :return: annotation object
1259
- """
1260
- try:
1261
- annotation = Annotation.from_json(_json=_json,
1262
- item=item,
1263
- client_api=client_api,
1264
- annotations=annotations,
1265
- is_video=is_video,
1266
- fps=fps,
1267
- item_metadata=item_metadata,
1268
- dataset=dataset)
1269
- status = True
1270
- except Exception:
1271
- annotation = traceback.format_exc()
1272
- status = False
1273
- return status, annotation
1274
-
1275
- @classmethod
1276
- def from_json(cls,
1277
- _json,
1278
- item=None,
1279
- client_api=None,
1280
- annotations=None,
1281
- is_video=None,
1282
- fps=None,
1283
- item_metadata=None,
1284
- dataset=None,
1285
- is_audio=None):
1286
- """
1287
- Create an annotation object from platform json
1288
-
1289
- :param dict _json: platform json
1290
- :param dtlpy.entities.item.Item item: item
1291
- :param client_api: ApiClient entity
1292
- :param annotations:
1293
- :param bool is_video: is video
1294
- :param fps: video fps
1295
- :param item_metadata: item metadata
1296
- :param dataset: dataset entity
1297
- :param bool is_audio: is audio
1298
- :return: annotation object
1299
- :rtype: dtlpy.entities.annotation.Annotation
1300
- """
1301
- if item_metadata is None:
1302
- item_metadata = dict()
1303
-
1304
- if is_video is None:
1305
- if item is None:
1306
- if _json['metadata'].get('system', {}).get('endFrame', 0) > 0:
1307
- is_video = True
1308
- else:
1309
- is_video = False
1310
- else:
1311
- # get item type
1312
- if item.mimetype is not None and 'video' in item.mimetype:
1313
- is_video = True
1314
-
1315
- if is_audio is None:
1316
- if item is None:
1317
- is_audio = False
1318
- else:
1319
- # get item type
1320
- if item.mimetype is not None and 'audio' in item.mimetype:
1321
- is_audio = True
1322
-
1323
- item_url = _json.get('item', item.url if item is not None else None)
1324
- item_id = _json.get('itemId', item.id if item is not None else None)
1325
- dataset_url = _json.get('dataset', item.dataset_url if item is not None else None)
1326
- dataset_id = _json.get('datasetId', item.dataset_id if item is not None else None)
1327
-
1328
- if item is not None:
1329
- if item.id != item_id:
1330
- logger.warning('Annotation has been fetched from a item that is not belong to it')
1331
- item = None
1332
-
1333
- if dataset is not None:
1334
- if dataset.id != dataset_id:
1335
- logger.warning('Annotation has been fetched from a dataset that is not belong to it')
1336
- dataset = None
1337
-
1338
- # get id
1339
- annotation_id = None
1340
- if 'id' in _json:
1341
- annotation_id = _json['id']
1342
- elif '_id' in _json:
1343
- annotation_id = _json['_id']
1344
-
1345
- metadata = _json.get('metadata', dict())
1346
-
1347
- # get metadata, status, attributes and object id
1348
- object_id = None
1349
- status = None
1350
- if 'system' in metadata and metadata['system'] is not None:
1351
- object_id = _json['metadata']['system'].get('objectId', object_id)
1352
- status = _json['metadata']['system'].get('status', status)
1353
-
1354
- named_attributes = metadata.get('system', dict()).get('attributes', None)
1355
- recipe_1_attributes = _json.get('attributes', None)
1356
-
1357
- first_frame_attributes = recipe_1_attributes
1358
- first_frame_coordinates = list()
1359
- first_frame_number = 0
1360
- first_frame_start_time = 0
1361
- automated = None
1362
- end_frame = None
1363
- start_time = 0
1364
- start_frame = 0
1365
- end_time = None
1366
- annotation_definition = None
1367
-
1368
- ############
1369
- # if video #
1370
- ############
1371
- if is_video or is_audio:
1372
- # get fps
1373
- if item is not None and item.fps is not None:
1374
- fps = item.fps
1375
- if fps is None:
1376
- if is_video:
1377
- fps = item_metadata.get('fps', 25)
1378
- else:
1379
- item_metadata.get('fps', 1000)
1380
-
1381
- # get video-only attributes
1382
- end_time = 1.5
1383
- # get first frame attribute
1384
- first_frame_attributes = first_frame_attributes
1385
- # get first frame coordinates
1386
- first_frame_coordinates = _json.get('coordinates', first_frame_coordinates)
1387
- if 'system' in metadata:
1388
- # get first frame number
1389
- first_frame_number = _json['metadata']['system'].get('frame', first_frame_number)
1390
- # get first frame start time
1391
- start_time = _json['metadata']['system'].get('startTime', first_frame_start_time)
1392
- # get first frame number
1393
- start_frame = _json['metadata']['system'].get('frame', start_frame)
1394
- automated = _json['metadata']['system'].get('automated', automated)
1395
- end_frame = _json['metadata']['system'].get('endFrame', end_frame)
1396
- end_time = _json['metadata']['system'].get('endTime', end_time)
1397
- ################
1398
- # if not video #
1399
- ################
1400
- if not is_video or is_audio:
1401
- # get coordinates
1402
- coordinates = _json.get('coordinates', list())
1403
- # set video only attributes
1404
- if not is_audio:
1405
- end_time = 0
1406
- # get automated
1407
- if 'system' in metadata and metadata['system'] is not None:
1408
- automated = metadata['system'].get('automated', automated)
1409
- # set annotation definition
1410
- def_dict = {'type': _json['type'],
1411
- 'coordinates': coordinates,
1412
- 'label': _json['label'],
1413
- 'attributes': named_attributes}
1414
- annotation_definition = FrameAnnotation.json_to_annotation_definition(def_dict)
1415
-
1416
- frames = entities.ReflectDict(
1417
- value_type=FrameAnnotation,
1418
- on_access=Annotation.on_access,
1419
- start=start_frame,
1420
- end=end_frame
1421
- )
1422
-
1423
- # init annotation
1424
- annotation = cls(
1425
- # temp
1426
- platform_dict=copy.deepcopy(_json),
1427
- # platform
1428
- id=annotation_id,
1429
- url=_json.get('url', None),
1430
- item_url=item_url,
1431
- item=item,
1432
- item_id=item_id,
1433
- dataset=dataset,
1434
- dataset_url=dataset_url,
1435
- dataset_id=dataset_id,
1436
- creator=_json['creator'],
1437
- created_at=_json['createdAt'],
1438
- updated_by=_json['updatedBy'],
1439
- updated_at=_json['updatedAt'],
1440
- hash=_json.get('hash', None),
1441
- object_id=object_id,
1442
- type=_json['type'],
1443
- item_width=item_metadata.get('width', None),
1444
- item_height=item_metadata.get('height', None),
1445
- # meta
1446
- metadata=metadata,
1447
- fps=fps,
1448
- status=status,
1449
- # snapshots
1450
- frames=frames,
1451
- # video attributes
1452
- automated=automated,
1453
- end_frame=end_frame,
1454
- end_time=end_time,
1455
- start_frame=start_frame,
1456
- annotations=annotations,
1457
- start_time=start_time,
1458
- label_suggestions=_json.get('labelSuggestions', None),
1459
- source=_json.get('source', None),
1460
- recipe_1_attributes=recipe_1_attributes,
1461
- )
1462
- annotation.annotation_definition = annotation_definition
1463
- annotation.__client_api = client_api
1464
- if annotation_definition:
1465
- description = _json.get('description', None)
1466
- if description is not None:
1467
- annotation.description = description
1468
-
1469
- #################
1470
- # if has frames #
1471
- #################
1472
- if is_video:
1473
- # set first frame
1474
- snapshot = {
1475
- 'attributes': first_frame_attributes,
1476
- 'coordinates': first_frame_coordinates,
1477
- 'fixed': True,
1478
- 'objectVisible': True,
1479
- 'frame': first_frame_number,
1480
- 'label': _json['label'],
1481
- 'type': annotation.type,
1482
- 'namedAttributes': named_attributes
1483
- }
1484
-
1485
- # add first frame
1486
- frame = FrameAnnotation.from_snapshot(
1487
- _json=snapshot,
1488
- annotation=annotation,
1489
- fps=fps
1490
- )
1491
- annotation.frames[frame.frame_num] = frame
1492
- annotation.annotation_definition = frame.annotation_definition
1493
-
1494
- # put snapshots if there are any
1495
- for snapshot in _json['metadata']['system'].get('snapshots_', []):
1496
- frame = FrameAnnotation.from_snapshot(
1497
- _json=snapshot,
1498
- annotation=annotation,
1499
- fps=fps
1500
- )
1501
-
1502
- annotation.frames[frame.frame_num] = frame
1503
-
1504
- annotation.annotation_definition = annotation.frames[min(frames.actual_keys())].annotation_definition
1505
- if annotation.annotation_definition:
1506
- description = _json.get('description', None)
1507
- if description is not None:
1508
- annotation.description = description
1509
-
1510
- return annotation
1511
-
1512
- @staticmethod
1513
- def on_access(reflect_dict, actual_key: int, requested_key: int, val):
1514
- val = copy.copy(val)
1515
- val._interpolation = True
1516
- val.fixed = False
1517
- val.frame_num = requested_key
1518
- reflect_dict[requested_key] = val
1519
- return val
1520
-
1521
- def to_json(self):
1522
- """
1523
- Convert annotation object to a platform json representatio
1524
-
1525
- :return: platform json
1526
- :rtype: dict
1527
- """
1528
- if len(self.frames.actual_keys()) > 0:
1529
- self.set_frame(min(self.frames.actual_keys()))
1530
- _json = attr.asdict(self,
1531
- filter=attr.filters.include(attr.fields(Annotation).id,
1532
- attr.fields(Annotation).url,
1533
- attr.fields(Annotation).metadata,
1534
- attr.fields(Annotation).creator,
1535
- attr.fields(Annotation).hash,
1536
- attr.fields(Annotation).metadata))
1537
-
1538
- # property attributes
1539
- item_id = self.item_id
1540
- if item_id is None and self._item is not None:
1541
- item_id = self._item.id
1542
-
1543
- _json['itemId'] = item_id
1544
- _json['item'] = self.item_url
1545
- _json['label'] = self.label
1546
-
1547
- # Need to put back after transition to attributes 2.0
1548
- # _json['attributes'] = self.attributes
1549
-
1550
- _json['dataset'] = self.dataset_url
1551
-
1552
- _json['createdAt'] = self.created_at
1553
- _json['updatedBy'] = self.updated_by
1554
- _json['updatedAt'] = self.updated_at
1555
- _json['source'] = self.source
1556
-
1557
- if self.description is not None:
1558
- _json['description'] = self.description
1559
-
1560
- if self.label_suggestions:
1561
- _json['labelSuggestions'] = self.label_suggestions
1562
-
1563
- if self._item is not None and self.dataset_id is None:
1564
- _json['datasetId'] = self._item.dataset_id
1565
- else:
1566
- _json['datasetId'] = self.dataset_id
1567
-
1568
- _json['type'] = self.type
1569
- if self.type != 'class':
1570
- _json['coordinates'] = self.coordinates
1571
-
1572
- # add system metadata
1573
- if _json['metadata'].get('system', None) is None:
1574
- _json['metadata']['system'] = dict()
1575
- if self.automated is not None:
1576
- _json['metadata']['system']['automated'] = self.automated
1577
- if self.object_id is not None:
1578
- _json['metadata']['system']['objectId'] = self.object_id
1579
- if self.status is not None:
1580
- # if status is CLEAR need to set status to None so it will be deleted in backend
1581
- _json['metadata']['system']['status'] = self.status if self.status != AnnotationStatus.CLEAR else None
1582
-
1583
- if isinstance(self.annotation_definition, entities.Description):
1584
- _json['metadata']['system']['system'] = True
1585
-
1586
- _json['metadata']['system']['attributes'] = self.attributes if self.attributes is not None else dict()
1587
- _json['attributes'] = self._recipe_1_attributes
1588
-
1589
-
1590
- # add frame info
1591
- if self.is_video or (self.end_time and self.end_time > 0) or (self.end_frame and self.end_frame > 0):
1592
- # get all snapshots but the first one
1593
- snapshots = list()
1594
- frame_numbers = self.frames.actual_keys() if len(self.frames.actual_keys()) else []
1595
- for frame_num in sorted(frame_numbers):
1596
- if frame_num <= self.frames.start:
1597
- continue
1598
- if frame_num > self.frames.end:
1599
- break
1600
- if not self.frames[frame_num]._interpolation or self.frames[frame_num].fixed:
1601
- snapshots.append(self.frames[frame_num].to_snapshot())
1602
- self.frames[frame_num]._interpolation = False
1603
- # add metadata to json
1604
- _json['metadata']['system']['frame'] = self.start_frame
1605
- _json['metadata']['system']['startTime'] = self.start_time
1606
- _json['metadata']['system']['endTime'] = self.end_time
1607
- if self.end_frame is not None:
1608
- _json['metadata']['system']['endFrame'] = self.end_frame
1609
-
1610
- # add snapshots only if classification
1611
- if self.type not in ['subtitle']:
1612
- _json['metadata']['system']['snapshots_'] = snapshots
1613
-
1614
- return _json
1615
-
1616
- def task_scores(self, task_id: str, page_offset: int = None, page_size: int = None):
1617
- """
1618
- Get the scores of the annotation in a specific task.
1619
- :param task_id: The ID of the task.
1620
- :param page_offset: The page offset.
1621
- :param page_size: The page size.
1622
- :return: page of scores
1623
- """
1624
- return self.annotations.task_scores(annotation_id=self.id ,task_id=task_id, page_offset=page_offset, page_size=page_size)
1625
-
1626
-
1627
-
1628
- @attr.s
1629
- class FrameAnnotation(entities.BaseEntity):
1630
- """
1631
- FrameAnnotation object
1632
- """
1633
- # parent annotation
1634
- annotation = attr.ib()
1635
-
1636
- # annotations
1637
- annotation_definition = attr.ib()
1638
-
1639
- # multi
1640
- frame_num = attr.ib()
1641
- fixed = attr.ib()
1642
- object_visible = attr.ib()
1643
-
1644
- # temp
1645
- _interpolation = attr.ib(repr=False, default=False)
1646
- _recipe_1_attributes = attr.ib(repr=False, default=None)
1647
-
1648
- ################################
1649
- # parent annotation attributes #
1650
- ################################
1651
-
1652
- @property
1653
- def status(self):
1654
- return self.annotation.status
1655
-
1656
- @property
1657
- def timestamp(self):
1658
- if self.annotation.fps is not None and self.frame_num is not None:
1659
- return self.frame_num / self.annotation.fps if self.annotation.fps != 0 else None
1660
-
1661
- ####################################
1662
- # annotation definition attributes #
1663
- ####################################
1664
- @property
1665
- def type(self):
1666
- return self.annotation.type
1667
-
1668
- @property
1669
- def label(self):
1670
- return self.annotation_definition.label
1671
-
1672
- @label.setter
1673
- def label(self, label):
1674
- self.annotation_definition.label = label
1675
-
1676
- @property
1677
- def attributes(self):
1678
- return self.annotation_definition.attributes
1679
-
1680
- @attributes.setter
1681
- def attributes(self, attributes):
1682
- self.annotation_definition.attributes = attributes
1683
-
1684
- @property
1685
- def geo(self):
1686
- return self.annotation_definition.geo
1687
-
1688
- @property
1689
- def top(self):
1690
- return self.annotation_definition.top
1691
-
1692
- @property
1693
- def bottom(self):
1694
- return self.annotation_definition.bottom
1695
-
1696
- @property
1697
- def left(self):
1698
- return self.annotation_definition.left
1699
-
1700
- @property
1701
- def right(self):
1702
- return self.annotation_definition.right
1703
-
1704
- @property
1705
- def color(self):
1706
- if self.annotation.item is None:
1707
- return 255, 255, 255
1708
- else:
1709
- label = None
1710
- for label in self.annotation.item.dataset.labels:
1711
- if label.tag.lower() == self.label.lower():
1712
- return label.rgb
1713
- if label is None:
1714
- return 255, 255, 255
1715
-
1716
- @property
1717
- def coordinates(self):
1718
- coordinates = self.annotation_definition.to_coordinates(color=self.color)
1719
- return coordinates
1720
-
1721
- @property
1722
- def x(self):
1723
- return self.annotation_definition.x
1724
-
1725
- @property
1726
- def y(self):
1727
- return self.annotation_definition.y
1728
-
1729
- @property
1730
- def rx(self):
1731
- if self.annotation_definition.type == 'ellipse':
1732
- return self.annotation_definition.rx
1733
- else:
1734
- return None
1735
-
1736
- @property
1737
- def ry(self):
1738
- if self.annotation_definition.type == 'ellipse':
1739
- return self.annotation_definition.ry
1740
- else:
1741
- return None
1742
-
1743
- @property
1744
- def angle(self):
1745
- if self.annotation_definition.type in ['ellipse', 'box']:
1746
- return self.annotation_definition.angle
1747
- else:
1748
- return None
1749
-
1750
- ######################
1751
- # annotation methods #
1752
- ######################
1753
- def show(self, **kwargs):
1754
- """
1755
- Show annotation as ndarray
1756
- :param kwargs: see annotation definition
1757
- :return: ndarray of the annotation
1758
- """
1759
- return self.annotation_definition.show(**kwargs)
1760
-
1761
- @staticmethod
1762
- def json_to_annotation_definition(_json):
1763
- if 'namedAttributes' in _json:
1764
- _json['attributes'] = _json['namedAttributes']
1765
- else:
1766
- if not isinstance(_json.get('attributes'), dict):
1767
- _json['attributes'] = None
1768
- if _json['type'] == 'segment':
1769
- annotation = entities.Polygon.from_json(_json)
1770
- elif _json['type'] == 'polyline':
1771
- annotation = entities.Polyline.from_json(_json)
1772
- elif _json['type'] == 'box':
1773
- annotation = entities.Box.from_json(_json)
1774
- elif _json['type'] == 'cube':
1775
- annotation = entities.Cube.from_json(_json)
1776
- elif _json['type'] == 'cube_3d':
1777
- annotation = entities.Cube3d.from_json(_json)
1778
- elif _json['type'] == 'point':
1779
- annotation = entities.Point.from_json(_json)
1780
- elif _json['type'] == 'binary':
1781
- annotation = entities.Segmentation.from_json(_json)
1782
- elif _json['type'] == 'class':
1783
- annotation = entities.Classification.from_json(_json)
1784
- elif _json['type'] == 'subtitle':
1785
- annotation = entities.Subtitle.from_json(_json)
1786
- elif _json['type'] == 'ellipse':
1787
- annotation = entities.Ellipse.from_json(_json)
1788
- elif _json['type'] == 'comparison':
1789
- annotation = entities.Comparison.from_json(_json)
1790
- elif _json['type'] == 'note':
1791
- annotation = entities.Note.from_json(_json)
1792
- elif _json['type'] == 'pose':
1793
- annotation = entities.Pose.from_json(_json)
1794
- elif _json['type'] == 'gis':
1795
- annotation = entities.Gis.from_json(_json)
1796
- else:
1797
- annotation = entities.UndefinedAnnotationType.from_json(_json)
1798
- return annotation
1799
-
1800
- #######
1801
- # I/O #
1802
- #######
1803
- @classmethod
1804
- def new(cls, annotation, annotation_definition, frame_num, fixed, object_visible=True):
1805
- """
1806
- new frame state to annotation
1807
-
1808
- :param annotation: annotation
1809
- :param annotation_definition: annotation type object - must be same type as annotation
1810
- :param frame_num: frame number
1811
- :param fixed: is fixed
1812
- :param object_visible: does the annotated object is visible
1813
- :return: FrameAnnotation object
1814
- """
1815
- frame = cls(
1816
- # annotations
1817
- annotation=annotation,
1818
- annotation_definition=annotation_definition,
1819
-
1820
- # multi
1821
- frame_num=frame_num,
1822
- fixed=fixed,
1823
- object_visible=object_visible,
1824
- )
1825
- if annotation_definition.attributes:
1826
- frame.attributes = annotation_definition.attributes
1827
- return frame
1828
-
1829
- @classmethod
1830
- def from_snapshot(cls, annotation, _json, fps):
1831
- """
1832
- new frame state to annotation
1833
-
1834
- :param annotation: annotation
1835
- :param _json: annotation type object - must be same type as annotation
1836
- :param fps: frame number
1837
- :return: FrameAnnotation object
1838
- """
1839
- # get annotation class
1840
- _json['type'] = annotation.type
1841
- attrs = _json.get('attributes', None)
1842
- annotation_definition = cls.json_to_annotation_definition(_json=_json)
1843
-
1844
- frame_num = _json.get('frame', annotation.last_frame + 1)
1845
-
1846
- return cls(
1847
- # annotations
1848
- annotation=annotation,
1849
- annotation_definition=annotation_definition,
1850
-
1851
- # multi
1852
- frame_num=frame_num,
1853
- fixed=_json.get('fixed', False),
1854
- object_visible=_json.get('objectVisible', True),
1855
- recipe_1_attributes=attrs,
1856
- )
1857
-
1858
- def to_snapshot(self):
1859
-
1860
- snapshot_dict = {
1861
- 'frame': self.frame_num,
1862
- 'fixed': self.fixed,
1863
- 'label': self.label,
1864
- 'type': self.type,
1865
- 'objectVisible': self.object_visible,
1866
- 'data': self.coordinates
1867
- }
1868
-
1869
- if self.annotation_definition.description is not None:
1870
- snapshot_dict['description'] = self.annotation_definition.description
1871
-
1872
- if self.attributes is not None:
1873
- snapshot_dict['namedAttributes'] = self.attributes
1874
-
1875
- if self._recipe_1_attributes is not None:
1876
- snapshot_dict['attributes'] = self._recipe_1_attributes
1877
-
1878
-
1879
- return snapshot_dict
1
+ import numpy as np
2
+ import traceback
3
+ import logging
4
+ import datetime
5
+ import webvtt
6
+ import copy
7
+ import attr
8
+ import json
9
+ import os
10
+ import warnings
11
+
12
+ from PIL import Image
13
+ from enum import Enum
14
+
15
+ from .. import entities, PlatformException, repositories, ApiClient, exceptions
16
+
17
+ logger = logging.getLogger(name='dtlpy')
18
+
19
+
20
+ class ExportVersion(str, Enum):
21
+ V1 = "V1"
22
+ V2 = "V2"
23
+
24
+
25
+ class AnnotationStatus(str, Enum):
26
+ ISSUE = "issue"
27
+ APPROVED = "approved"
28
+ REVIEW = "review"
29
+ CLEAR = "clear"
30
+
31
+
32
+ class AnnotationType(str, Enum):
33
+ BOX = "box"
34
+ CUBE = "cube"
35
+ CUBE3D = "cube_3d"
36
+ CLASSIFICATION = "class"
37
+ COMPARISON = "comparison"
38
+ ELLIPSE = "ellipse"
39
+ NOTE = "note"
40
+ POINT = "point"
41
+ POLYGON = "segment"
42
+ POLYLINE = "polyline"
43
+ POSE = "pose"
44
+ SEGMENTATION = "binary"
45
+ SUBTITLE = "subtitle"
46
+ TEXT = "text_mark"
47
+ GIS = "gis"
48
+ SEMANTIC_3D = "ref_semantic_3d"
49
+ POLYLINE_3D = "polyline_3d"
50
+ FREE_TEXT = "text"
51
+ REF_IMAGE = "ref_image"
52
+
53
+
54
+ class ViewAnnotationOptions(str, Enum):
55
+ """ The Annotations file types to download (JSON, MASK, INSTANCE, ANNOTATION_ON_IMAGE, VTT, OBJECT_ID).
56
+
57
+ .. list-table::
58
+ :widths: 15 150
59
+ :header-rows: 1
60
+
61
+ * - State
62
+ - Description
63
+ * - JSON
64
+ - Dataloop json format
65
+ * - MASK
66
+ - PNG file that contains drawing annotations on it
67
+ * - INSTANCE
68
+ - An image file that contains 2D annotations
69
+ * - ANNOTATION_ON_IMAGE
70
+ - The source image with the annotations drawing in it
71
+ * - VTT
72
+ - An text file contains supplementary information about a web video
73
+ * - OBJECT_ID
74
+ - An image file that contains 2D annotations
75
+
76
+ """
77
+ JSON = "json"
78
+ MASK = "mask"
79
+ INSTANCE = "instance"
80
+ ANNOTATION_ON_IMAGE = "img_mask"
81
+ VTT = "vtt"
82
+ OBJECT_ID = "object_id"
83
+
84
+
85
+ @attr.s
86
+ class Annotation(entities.BaseEntity):
87
+ """
88
+ Annotations object
89
+ """
90
+ # platform
91
+ id = attr.ib()
92
+ url = attr.ib(repr=False)
93
+ item_url = attr.ib(repr=False)
94
+ _item = attr.ib(repr=False)
95
+ item_id = attr.ib()
96
+ creator = attr.ib()
97
+ created_at = attr.ib()
98
+ updated_by = attr.ib(repr=False)
99
+ updated_at = attr.ib(repr=False)
100
+ type = attr.ib()
101
+ source = attr.ib(repr=False)
102
+ dataset_url = attr.ib(repr=False)
103
+
104
+ # api
105
+ _platform_dict = attr.ib(repr=False)
106
+ # meta
107
+ metadata = attr.ib(repr=False)
108
+ fps = attr.ib(repr=False)
109
+ hash = attr.ib(default=None, repr=False)
110
+ dataset_id = attr.ib(default=None, repr=False)
111
+ status = attr.ib(default=None, repr=False)
112
+ object_id = attr.ib(default=None, repr=False)
113
+ automated = attr.ib(default=None, repr=False)
114
+ item_height = attr.ib(default=None)
115
+ item_width = attr.ib(default=None)
116
+ label_suggestions = attr.ib(default=None)
117
+
118
+ # annotation definition
119
+ _annotation_definition = attr.ib(default=None, repr=False, type=entities.BaseAnnotationDefinition)
120
+
121
+ # snapshots
122
+ frames = attr.ib(default=None, repr=False)
123
+ current_frame = attr.ib(default=0, repr=False)
124
+
125
+ # video attributes
126
+ _end_frame = attr.ib(default=0, repr=False)
127
+ _end_time = attr.ib(default=0, repr=False)
128
+ _start_frame = attr.ib(default=0)
129
+ _start_time = attr.ib(default=0)
130
+
131
+ # sdk
132
+ _dataset = attr.ib(repr=False, default=None)
133
+ _datasets = attr.ib(repr=False, default=None)
134
+ _annotations = attr.ib(repr=False, default=None)
135
+ __client_api = attr.ib(default=None, repr=False)
136
+ _items = attr.ib(repr=False, default=None)
137
+ _recipe_1_attributes = attr.ib(repr=False, default=None)
138
+
139
+ ############
140
+ # Platform #
141
+ ############
142
+
143
+ @property
144
+ def annotation_definition(self):
145
+ return self._annotation_definition
146
+
147
+ @annotation_definition.setter
148
+ def annotation_definition(self, ann_def):
149
+ if ann_def is not None:
150
+ ann_def._annotation = self
151
+ self._annotation_definition = ann_def
152
+
153
+ @property
154
+ def createdAt(self):
155
+ return self.created_at
156
+
157
+ @property
158
+ def updatedAt(self):
159
+ return self.updated_at
160
+
161
+ @property
162
+ def updatedBy(self):
163
+ return self.updated_by
164
+
165
+ @property
166
+ def _client_api(self) -> ApiClient:
167
+ if self.__client_api is None:
168
+ if self._item is None:
169
+ raise PlatformException('400',
170
+ 'This action cannot be performed without an item entity. Please set item')
171
+ else:
172
+ self.__client_api = self._item._client_api
173
+ assert isinstance(self.__client_api, ApiClient)
174
+ return self.__client_api
175
+
176
+ @property
177
+ def dataset(self):
178
+ if self._dataset is None:
179
+ if self._item is not None:
180
+ # get from item
181
+ self._dataset = self._item.dataset
182
+ else:
183
+ # get directly
184
+ self._dataset = self.datasets.get(dataset_id=self.dataset_id)
185
+ assert isinstance(self._dataset, entities.Dataset)
186
+ return self._dataset
187
+
188
+ @property
189
+ def item(self):
190
+ if self._item is None:
191
+ self._item = self.items.get(item_id=self.item_id)
192
+ assert isinstance(self._item, entities.Item)
193
+ return self._item
194
+
195
+ @property
196
+ def annotations(self):
197
+ if self._annotations is None:
198
+ self._annotations = repositories.Annotations(client_api=self._client_api, item=self._item)
199
+ assert isinstance(self._annotations, repositories.Annotations)
200
+ return self._annotations
201
+
202
+ @property
203
+ def datasets(self):
204
+ if self._datasets is None:
205
+ self._datasets = repositories.Datasets(client_api=self._client_api)
206
+ assert isinstance(self._datasets, repositories.Datasets)
207
+ return self._datasets
208
+
209
+ @property
210
+ def items(self):
211
+ if self._items is None:
212
+ if self._datasets is not None:
213
+ self._items = self._dataset.items
214
+ elif self._item is not None:
215
+ self._items = self._item.items
216
+ else:
217
+ self._items = repositories.Items(client_api=self._client_api, dataset=self._dataset)
218
+ assert isinstance(self._items, repositories.Items)
219
+ return self._items
220
+
221
+ #########################
222
+ # Annotation Properties #
223
+ #########################
224
+ @property
225
+ def parent_id(self):
226
+ try:
227
+ parent_id = self.metadata['system']['parentId']
228
+ except KeyError:
229
+ parent_id = None
230
+ return parent_id
231
+
232
+ @parent_id.setter
233
+ def parent_id(self, parent_id):
234
+ if 'system' not in self.metadata:
235
+ self.metadata['system'] = dict()
236
+ self.metadata['system']['parentId'] = parent_id
237
+
238
+ @property
239
+ def coordinates(self):
240
+ color = None
241
+ if self.type in ['binary']:
242
+ color = self.color
243
+ coordinates = self.annotation_definition.to_coordinates(color=color)
244
+ return coordinates
245
+
246
+ @property
247
+ def start_frame(self):
248
+ return self._start_frame
249
+
250
+ @start_frame.setter
251
+ def start_frame(self, val):
252
+ if not isinstance(val, float) and not isinstance(val, int):
253
+ raise ValueError('Must input a valid number')
254
+ self._start_frame = round(val)
255
+ self._start_time = val / self.fps if self.fps else 0
256
+ self.frames.start = self._start_frame
257
+
258
+ @property
259
+ def end_frame(self):
260
+ return self._end_frame
261
+
262
+ @end_frame.setter
263
+ def end_frame(self, val):
264
+ if not isinstance(val, float) and not isinstance(val, int):
265
+ raise ValueError('Must input a valid number')
266
+ self._end_frame = round(val)
267
+ self._end_time = val / self.fps if self.fps else 0
268
+ self.frames.end = self._end_frame
269
+
270
+ @property
271
+ def start_time(self):
272
+ return self._start_time
273
+
274
+ @start_time.setter
275
+ def start_time(self, val):
276
+ if not isinstance(val, float) and not isinstance(val, int):
277
+ raise ValueError('Must input a valid number')
278
+ self._start_frame = round(val * self.fps if self.fps else 0)
279
+ self._start_time = val
280
+ self.frames.start = self._start_frame
281
+
282
+ @property
283
+ def end_time(self):
284
+ return self._end_time
285
+
286
+ @end_time.setter
287
+ def end_time(self, val):
288
+ if not isinstance(val, float) and not isinstance(val, int):
289
+ raise ValueError('Must input a valid number')
290
+ self._end_frame = round(val * self.fps if self.fps else 0)
291
+ self._end_time = val
292
+ self.frames.end = self._end_frame
293
+
294
+ @property
295
+ def x(self):
296
+ return self.annotation_definition.x
297
+
298
+ @property
299
+ def y(self):
300
+ return self.annotation_definition.y
301
+
302
+ @property
303
+ def rx(self):
304
+ if self.annotation_definition.type == 'ellipse':
305
+ return self.annotation_definition.rx
306
+ else:
307
+ return None
308
+
309
+ @property
310
+ def ry(self):
311
+ if self.annotation_definition.type == 'ellipse':
312
+ return self.annotation_definition.ry
313
+ else:
314
+ return None
315
+
316
+ @property
317
+ def angle(self):
318
+ if self.annotation_definition.type in ['ellipse', 'cube', 'box']:
319
+ return self.annotation_definition.angle
320
+ else:
321
+ return None
322
+
323
+ @property
324
+ def messages(self):
325
+ if hasattr(self.annotation_definition, 'messages'):
326
+ return self.annotation_definition.messages
327
+ else:
328
+ return None
329
+
330
+ @messages.setter
331
+ def messages(self, messages):
332
+ if self.type == 'note':
333
+ self.annotation_definition.messages = messages
334
+ else:
335
+ raise PlatformException('400', 'Annotation of type {} does not have attribute messages'.format(self.type))
336
+
337
+ def add_message(self, body: str = None):
338
+ if self.type == 'note':
339
+ return self.annotation_definition.add_message(body=body)
340
+ else:
341
+ raise PlatformException('400', 'Annotation of type {} does not have method add_message'.format(self.type))
342
+
343
+ @property
344
+ def geo(self):
345
+ return self.annotation_definition.geo
346
+
347
+ @geo.setter
348
+ def geo(self, geo):
349
+ self.annotation_definition.geo = geo
350
+
351
+ @property
352
+ def top(self):
353
+ return self.annotation_definition.top
354
+
355
+ @top.setter
356
+ def top(self, top):
357
+ self.annotation_definition.top = top
358
+
359
+ @property
360
+ def bottom(self):
361
+ return self.annotation_definition.bottom
362
+
363
+ @bottom.setter
364
+ def bottom(self, bottom):
365
+ self.annotation_definition.bottom = bottom
366
+
367
+ @property
368
+ def left(self):
369
+ return self.annotation_definition.left
370
+
371
+ @left.setter
372
+ def left(self, left):
373
+ self.annotation_definition.left = left
374
+
375
+ @property
376
+ def right(self):
377
+ return self.annotation_definition.right
378
+
379
+ @right.setter
380
+ def right(self, right):
381
+ self.annotation_definition.right = right
382
+
383
+ @property
384
+ def height(self):
385
+ return self.annotation_definition.height
386
+
387
+ @height.setter
388
+ def height(self, height):
389
+ self.annotation_definition.height = height
390
+
391
+ @property
392
+ def width(self):
393
+ return self.annotation_definition.width
394
+
395
+ @width.setter
396
+ def width(self, width):
397
+ self.annotation_definition.width = width
398
+
399
+ @property
400
+ def description(self):
401
+ return self.annotation_definition.description
402
+
403
+ @description.setter
404
+ def description(self, description):
405
+ if description is None:
406
+ description = ""
407
+ if not isinstance(description, str):
408
+ raise ValueError("Description must get string")
409
+ self.annotation_definition.description = description
410
+
411
+ @property
412
+ def last_frame(self):
413
+ if len(self.frames.actual_keys()) == 0:
414
+ return 0
415
+ return max(self.frames.actual_keys())
416
+
417
+ @property
418
+ def label(self):
419
+ return self.annotation_definition.label
420
+
421
+ @label.setter
422
+ def label(self, label):
423
+ self.annotation_definition.label = label
424
+
425
+ @property
426
+ def attributes(self):
427
+ return self.annotation_definition.attributes
428
+
429
+ @attributes.setter
430
+ def attributes(self, attributes):
431
+ self.annotation_definition.attributes = attributes
432
+
433
+ @property
434
+ def color(self):
435
+ # if "dataset" is not in self - this will always get the dataset
436
+ try:
437
+ colors = self.dataset._get_ontology().color_map
438
+ except (exceptions.BadRequest, exceptions.NotFound):
439
+ colors = None
440
+ logger.warning('Cant get dataset for annotation color. using default.')
441
+ if colors is not None and self.label in colors:
442
+ color = colors[self.label]
443
+ else:
444
+ if self.type == 'binary' and self.annotation_definition._color is not None:
445
+ color = self.annotation_definition._color
446
+ else:
447
+ color = (255, 255, 255)
448
+ return color
449
+
450
+ @color.setter
451
+ def color(self, color):
452
+ if self.type == 'binary':
453
+ if not isinstance(color, tuple) or len(color) != 3:
454
+ raise ValueError("Color must get tuple of length 3")
455
+ self.annotation_definition._color = color
456
+ else:
457
+ raise exceptions.BadRequest(
458
+ status_code='400',
459
+ message='Invalid annotation type - Updating color is only available to binary annotation'
460
+ )
461
+
462
+ ####################
463
+ # frame attributes #
464
+ ####################
465
+ @property
466
+ def frame_num(self):
467
+ if len(self.frames.actual_keys()) > 0:
468
+ return self.current_frame
469
+ else:
470
+ return self.start_frame
471
+
472
+ @frame_num.setter
473
+ def frame_num(self, frame_num):
474
+ if frame_num != self.current_frame:
475
+ self.frames[self.current_frame].frame_num = frame_num
476
+ self.frames[frame_num] = self.frames[self.current_frame]
477
+ self.frames.pop(self.current_frame)
478
+
479
+ @property
480
+ def fixed(self):
481
+ if len(self.frames.actual_keys()) > 0:
482
+ return self.frames[self.current_frame].fixed
483
+ else:
484
+ return False
485
+
486
+ @fixed.setter
487
+ def fixed(self, fixed):
488
+ if len(self.frames.actual_keys()) > 0:
489
+ self.frames[self.current_frame].fixed = fixed
490
+
491
+ @property
492
+ def object_visible(self):
493
+ if len(self.frames.actual_keys()) > 0:
494
+ return self.frames[self.current_frame].object_visible
495
+ else:
496
+ return False
497
+
498
+ @object_visible.setter
499
+ def object_visible(self, object_visible):
500
+ if len(self.frames.actual_keys()) > 0:
501
+ self.frames[self.current_frame].object_visible = object_visible
502
+
503
+ @property
504
+ def is_video(self):
505
+ if len(self.frames.actual_keys()) == 0:
506
+ return False
507
+ else:
508
+ return True
509
+
510
+ ##################
511
+ # entity methods #
512
+ ##################
513
+ def update_status(self, status: AnnotationStatus = AnnotationStatus.ISSUE):
514
+ """
515
+ Set status on annotation
516
+
517
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or *developer* or be assigned a task that includes that item as an *annotation manager*.
518
+
519
+ :param str status: can be AnnotationStatus.ISSUE, AnnotationStatus.APPROVED, AnnotationStatus.REVIEW, AnnotationStatus.CLEAR
520
+ :return: Annotation object
521
+ :rtype: dtlpy.entities.annotation.Annotation
522
+
523
+ **Example**:
524
+
525
+ .. code-block:: python
526
+
527
+ annotation = annotation.update_status(status=dl.AnnotationStatus.ISSUE)
528
+ """
529
+ return self.annotations.update_status(annotation=self, status=status)
530
+
531
+ def delete(self):
532
+ """
533
+ Remove an annotation from item
534
+
535
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
536
+
537
+ :return: True if success
538
+ :rtype: bool
539
+
540
+ **Example**:
541
+
542
+ .. code-block:: python
543
+
544
+ is_deleted = annotation.delete()
545
+ """
546
+ if self.id is None:
547
+ raise PlatformException(
548
+ '400',
549
+ 'Cannot delete annotation because it was not fetched from platform and therefore does not have an id'
550
+ )
551
+ return self.annotations.delete(annotation_id=self.id)
552
+
553
+ def update(self, system_metadata=False):
554
+ """
555
+ Update an existing annotation in host.
556
+
557
+ **Prerequisites**: You must have an item that has been annotated. You must have the role of an *owner* or *developer* or be assigned a task that includes that item as an *annotation manager* or *annotator*.
558
+
559
+ :param system_metadata: True, if you want to change metadata system
560
+ :return: Annotation object
561
+ :rtype: dtlpy.entities.annotation.Annotation
562
+
563
+ **Example**:
564
+
565
+ .. code-block:: python
566
+
567
+ annotation = annotation.update()
568
+ """
569
+ return self.annotations.update(annotations=self,
570
+ system_metadata=system_metadata)[0]
571
+
572
+ def upload(self):
573
+ """
574
+ Create a new annotation in host
575
+
576
+ **Prerequisites**: Any user can upload annotations.
577
+
578
+ :return: Annotation entity
579
+ :rtype: dtlpy.entities.annotation.Annotation
580
+ """
581
+ return self.annotations.upload(annotations=self)[0]
582
+
583
+ def download(self,
584
+ filepath: str,
585
+ annotation_format: ViewAnnotationOptions = ViewAnnotationOptions.JSON,
586
+ height: float = None,
587
+ width: float = None,
588
+ thickness: int = 1,
589
+ with_text: bool = False,
590
+ alpha: float = 1):
591
+ """
592
+ Save annotation to file
593
+
594
+ **Prerequisites**: Any user can upload annotations.
595
+
596
+ :param str filepath: local path to where annotation will be downloaded to
597
+ :param list annotation_format: options: list(dl.ViewAnnotationOptions)
598
+ :param float height: image height
599
+ :param float width: image width
600
+ :param int thickness: line thickness
601
+ :param bool with_text: get mask with text
602
+ :param float alpha: opacity value [0 1], default 1
603
+ :return: filepath
604
+ :rtype: str
605
+
606
+ **Example**:
607
+
608
+ .. code-block:: python
609
+
610
+ filepath = annotation.download(filepath='filepath', annotation_format=dl.ViewAnnotationOptions.MASK)
611
+ """
612
+ _, ext = os.path.splitext(filepath)
613
+ if ext == '':
614
+ if annotation_format == ViewAnnotationOptions.JSON:
615
+ ext = '.json'
616
+ else:
617
+ if self.is_video:
618
+ ext = '.mp4'
619
+ else:
620
+ ext = '.png'
621
+ filepath = os.path.join(filepath, self.id + ext)
622
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
623
+ if annotation_format == ViewAnnotationOptions.JSON:
624
+ with open(filepath, 'w') as f:
625
+ json.dump(self.to_json(), f, indent=2)
626
+ elif annotation_format == entities.ViewAnnotationOptions.VTT:
627
+ _, ext = os.path.splitext(filepath)
628
+ if ext != '.vtt':
629
+ raise PlatformException(error='400', message='file extension must be vtt')
630
+ vtt = webvtt.WebVTT()
631
+ if self.type in ['subtitle']:
632
+ s = str(datetime.timedelta(seconds=self.start_time))
633
+ if len(s.split('.')) == 1:
634
+ s += '.000'
635
+ e = str(datetime.timedelta(seconds=self.end_time))
636
+ if len(e.split('.')) == 1:
637
+ e += '.000'
638
+ caption = webvtt.Caption(
639
+ '{}'.format(s),
640
+ '{}'.format(e),
641
+ '{}'.format(self.coordinates['text'])
642
+ )
643
+ vtt.captions.append(caption)
644
+ vtt.save(filepath)
645
+ else:
646
+ if self.is_video:
647
+ entities.AnnotationCollection(item=self.item, annotations=[self])._video_maker(
648
+ input_filepath=None,
649
+ output_filepath=filepath,
650
+ annotation_format=annotation_format,
651
+ thickness=thickness,
652
+ alpha=alpha,
653
+ with_text=with_text
654
+ )
655
+ else:
656
+ mask = self.show(thickness=thickness,
657
+ alpha=alpha,
658
+ with_text=with_text,
659
+ height=height,
660
+ width=width,
661
+ annotation_format=annotation_format)
662
+ img = Image.fromarray(mask.astype(np.uint8))
663
+ img.save(filepath)
664
+ return filepath
665
+
666
+ def set_frame(self, frame):
667
+ """
668
+ Set annotation to frame state
669
+
670
+ **Prerequisites**: Any user can upload annotations.
671
+
672
+ :param int frame: frame number
673
+ :return: True if success
674
+ :rtype: bool
675
+
676
+ **Example**:
677
+
678
+ .. code-block:: python
679
+
680
+ success = annotation.set_frame(frame=10)
681
+ """
682
+ if frame in self.frames:
683
+ self.current_frame = frame
684
+ self.annotation_definition = self.frames[frame].annotation_definition
685
+ return True
686
+ else:
687
+ return False
688
+
689
+ ############
690
+ # Plotting #
691
+ ############
692
+ def show(self,
693
+ image=None,
694
+ thickness=None,
695
+ with_text=False,
696
+ height=None,
697
+ width=None,
698
+ annotation_format: ViewAnnotationOptions = ViewAnnotationOptions.MASK,
699
+ color=None,
700
+ label_instance_dict=None,
701
+ alpha=1,
702
+ frame_num=None
703
+ ):
704
+ """
705
+ Show annotations
706
+ mark the annotation of the image array and return it
707
+
708
+ **Prerequisites**: Any user can upload annotations.
709
+
710
+ :param image: empty or image to draw on
711
+ :param int thickness: line thickness
712
+ :param bool with_text: add label to annotation
713
+ :param float height: height
714
+ :param float width: width
715
+ :param dl.ViewAnnotationOptions annotation_format: list(dl.ViewAnnotationOptions)
716
+ :param tuple color: optional - color tuple
717
+ :param label_instance_dict: the instance labels
718
+ :param float alpha: opacity value [0 1], default 1
719
+ :param int frame_num: for video annotation, show specific fame
720
+ :return: list or single ndarray of the annotations
721
+
722
+ **Exampls**:
723
+
724
+ .. code-block:: python
725
+
726
+ image = annotation.show(image='ndarray',
727
+ thickness=1,
728
+ annotation_format=dl.VIEW_ANNOTATION_OPTIONS_MASK,
729
+ )
730
+ """
731
+ try:
732
+ import cv2
733
+ except (ImportError, ModuleNotFoundError):
734
+ logger.error(
735
+ 'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
736
+ raise
737
+ # sanity check for alpha
738
+ if alpha > 1 or alpha < 0:
739
+ raise ValueError(
740
+ 'input variable alpha in annotation.show() should be between 0 and 1. got: {!r}'.format(alpha))
741
+ if height is None:
742
+ if self._item is None or self._item.height is None:
743
+ if image is None:
744
+ raise PlatformException(error='400', message='must provide item width and height')
745
+ else:
746
+ height = image.shape[0]
747
+ else:
748
+ height = self._item.height
749
+ if width is None:
750
+ if self._item is None or self._item.width is None:
751
+ if image is None:
752
+ raise PlatformException(error='400', message='must provide item width and height')
753
+ else:
754
+ width = image.shape[1]
755
+ else:
756
+ width = self._item.width
757
+
758
+ # try getting instance map from dataset, otherwise set to default
759
+ if annotation_format in [entities.ViewAnnotationOptions.INSTANCE, entities.ViewAnnotationOptions.OBJECT_ID] and \
760
+ label_instance_dict is None:
761
+ if self._dataset is not None:
762
+ label_instance_dict = self._dataset.instance_map
763
+ else:
764
+ if self._item is not None and self._item._dataset is not None:
765
+ label_instance_dict = self._item._dataset.instance_map
766
+ if label_instance_dict is None:
767
+ logger.warning("Couldn't set label_instance_dict in annotation.show(). All labels will be mapped to 1")
768
+ label_instance_dict = dict()
769
+
770
+ in_video_range = True
771
+ if frame_num is not None:
772
+ in_video_range = self.set_frame(frame_num)
773
+
774
+ if self.is_video and not in_video_range:
775
+ if image is None:
776
+ image = self._get_default_mask(annotation_format=annotation_format,
777
+ width=width,
778
+ height=height,
779
+ label_instance_dict=label_instance_dict)
780
+ return image
781
+ else:
782
+ image = self._show_single_frame(image=image,
783
+ thickness=thickness,
784
+ alpha=alpha,
785
+ with_text=with_text,
786
+ height=height,
787
+ width=width,
788
+ annotation_format=annotation_format,
789
+ color=color,
790
+ label_instance_dict=label_instance_dict)
791
+ return image
792
+
793
+ def _show_single_frame(self,
794
+ image=None,
795
+ thickness=None,
796
+ with_text=False,
797
+ height=None,
798
+ width=None,
799
+ annotation_format: ViewAnnotationOptions = ViewAnnotationOptions.MASK,
800
+ color=None,
801
+ label_instance_dict=None,
802
+ alpha=None):
803
+ """
804
+ Show annotations
805
+ mark the annotation of the single frame array and return it
806
+ :param image: empty or image to draw on
807
+ :param thickness: line thickness
808
+ :param with_text: add label to annotation
809
+ :param height: height
810
+ :param width: width
811
+ :param annotation_format: list(dl.ViewAnnotationOptions)
812
+ :param color: optional - color tuple
813
+ :param label_instance_dict: the instance labels
814
+ :param alpha: opacity value [0 1], default 1
815
+ :return: ndarray of the annotations
816
+ """
817
+ try:
818
+ import cv2
819
+ except (ImportError, ModuleNotFoundError):
820
+ logger.error(
821
+ 'Import Error! Cant import cv2. Annotations operations will be limited. import manually and fix errors')
822
+ raise
823
+ # height/width
824
+ if self.annotation_definition.type == 'cube_3d':
825
+ logger.warning('the show for 3d_cube is not supported.')
826
+ return image
827
+ if annotation_format == entities.ViewAnnotationOptions.MASK:
828
+ # create an empty mask
829
+ if image is None:
830
+ image = self._get_default_mask(annotation_format=annotation_format,
831
+ width=width,
832
+ height=height,
833
+ label_instance_dict=label_instance_dict)
834
+ else:
835
+ if len(image.shape) == 2:
836
+ # image is gray
837
+ image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGBA)
838
+ elif image.shape[2] == 3:
839
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2RGBA)
840
+ elif image.shape[2] == 4:
841
+ pass
842
+ else:
843
+ raise PlatformException(
844
+ error='1001',
845
+ message='Unknown image shape. expected depth: gray or RGB or RGBA. got: {}'.format(image.shape))
846
+ elif annotation_format == entities.ViewAnnotationOptions.ANNOTATION_ON_IMAGE:
847
+ if image is None:
848
+ raise PlatformException(error='1001',
849
+ message='Must input image with ANNOTATION_ON_IMAGE view option.')
850
+ elif annotation_format in [entities.ViewAnnotationOptions.INSTANCE, entities.ViewAnnotationOptions.OBJECT_ID]:
851
+ if image is None:
852
+ # create an empty mask
853
+ image = self._get_default_mask(annotation_format=annotation_format,
854
+ width=width,
855
+ height=height,
856
+ label_instance_dict=label_instance_dict)
857
+ else:
858
+ if len(image.shape) != 2:
859
+ raise PlatformException(
860
+ error='1001',
861
+ message='Image shape must be 2d array when trying to draw instance on image')
862
+ # create a dictionary of labels and ids
863
+ else:
864
+ raise PlatformException(error='1001',
865
+ message='unknown annotations format: "{}". known formats: "{}"'.format(
866
+ annotation_format, '", "'.join(list(entities.ViewAnnotationOptions))))
867
+ # color
868
+ if color is None:
869
+ if annotation_format in [entities.ViewAnnotationOptions.MASK,
870
+ entities.ViewAnnotationOptions.INSTANCE,
871
+ entities.ViewAnnotationOptions.OBJECT_ID]:
872
+ color = self._get_color_for_show(annotation_format=annotation_format,
873
+ alpha=alpha,
874
+ label_instance_dict=label_instance_dict)
875
+ else:
876
+ raise PlatformException(error='1001',
877
+ message='unknown annotations format: {}. known formats: "{}"'.format(
878
+ annotation_format, '", "'.join(list(entities.ViewAnnotationOptions))))
879
+
880
+ return self.annotation_definition.show(image=image,
881
+ thickness=thickness,
882
+ with_text=with_text,
883
+ height=height,
884
+ width=width,
885
+ annotation_format=annotation_format,
886
+ color=color,
887
+ alpha=alpha)
888
+
889
+ def _get_color_for_show(self, annotation_format, alpha=1, label_instance_dict=None):
890
+ if annotation_format == entities.ViewAnnotationOptions.MASK:
891
+ color = self.color
892
+ if len(color) == 3:
893
+ color = color + (int(alpha * 255),)
894
+ elif annotation_format == entities.ViewAnnotationOptions.INSTANCE:
895
+ color = label_instance_dict.get(self.label, 1)
896
+ elif annotation_format == entities.ViewAnnotationOptions.OBJECT_ID:
897
+ if self.object_id is None:
898
+ raise ValueError('Try to show object_id but annotation has no value. annotation id: {}'.format(self.id))
899
+ color = int(self.object_id)
900
+ else:
901
+ raise ValueError('cant find color for the annotation format: {}'.format(annotation_format))
902
+ return color
903
+
904
+ def _get_default_mask(self, annotation_format, height, width, label_instance_dict):
905
+ """
906
+ Create a default mask take from the colors or instance map. tag should '$default'.
907
+ Default value is zeros
908
+
909
+ :param annotation_format: dl.AnnotationOptions to show
910
+ :param height: item height
911
+ :param width: item width
912
+ :return:
913
+ """
914
+ if annotation_format == entities.ViewAnnotationOptions.MASK:
915
+ try:
916
+ colors = self.dataset._get_ontology().color_map
917
+ except exceptions.BadRequest:
918
+ colors = None
919
+ logger.warning('Cant get dataset for annotation color. using default.')
920
+ if colors is not None and '$default' in colors:
921
+ default_color = colors['$default']
922
+ else:
923
+ default_color = None
924
+ if default_color is None:
925
+ default_color = (0, 0, 0, 0)
926
+ if len(default_color) == 3:
927
+ default_color += (255,)
928
+ if len(default_color) != 4:
929
+ raise ValueError('default color for show() mask should be with len 3 or 4 (RGB/RGBA)')
930
+ image = np.full(shape=(height, width, 4), fill_value=default_color, dtype=np.uint8)
931
+ elif annotation_format in [entities.ViewAnnotationOptions.INSTANCE,
932
+ entities.ViewAnnotationOptions.OBJECT_ID]:
933
+ default_color = 0
934
+ if '$default' in label_instance_dict:
935
+ default_color = label_instance_dict['$default']
936
+ image = np.full(shape=(height, width), fill_value=default_color, dtype=np.uint8)
937
+ else:
938
+ raise ValueError('cant create default mask for annotation_format: {}'.format(annotation_format))
939
+ return image
940
+
941
+ #######
942
+ # I/O #
943
+ #######
944
+ @classmethod
945
+ def new(cls,
946
+ item=None,
947
+ annotation_definition=None,
948
+ object_id=None,
949
+ automated=True,
950
+ metadata=None,
951
+ frame_num=None,
952
+ parent_id=None,
953
+ start_time=None,
954
+ item_height=None,
955
+ item_width=None,
956
+ end_time=None):
957
+ """
958
+ Create a new annotation object annotations
959
+
960
+ **Prerequisites**: Any user can upload annotations.
961
+
962
+ :param dtlpy.entities.item.Items item: item to annotate
963
+ :param annotation_definition: annotation type object
964
+ :param str object_id: object_id
965
+ :param bool automated: is automated
966
+ :param dict metadata: metadata
967
+ :param int frame_num: optional - first frame number if video annotation
968
+ :param str parent_id: add parent annotation ID
969
+ :param start_time: optional - start time if video annotation
970
+ :param float item_height: annotation item's height
971
+ :param float item_width: annotation item's width
972
+ :param end_time: optional - end time if video annotation
973
+ :return: annotation object
974
+ :rtype: dtlpy.entities.annotation.Annotation
975
+
976
+ **Example**:
977
+
978
+ .. code-block:: python
979
+
980
+ annotation = annotation.new(item='item_entity,
981
+ annotation_definition=dl.Box(top=10,left=10,bottom=100, right=100,label='labelName'))
982
+ )
983
+ """
984
+ if frame_num is None:
985
+ frame_num = 0
986
+
987
+ if object_id is not None:
988
+ if isinstance(object_id, int):
989
+ object_id = '{}'.format(object_id)
990
+ elif not isinstance(object_id, str) or not object_id.isnumeric():
991
+ raise ValueError('Object id must be an int or a string containing only numbers.')
992
+
993
+ # init annotations
994
+ if metadata is None:
995
+ metadata = dict()
996
+
997
+ # add parent
998
+ if parent_id is not None:
999
+ if 'system' not in metadata:
1000
+ metadata['system'] = dict()
1001
+ metadata['system']['parentId'] = parent_id
1002
+
1003
+ # add note status to metadata
1004
+ if annotation_definition is not None and annotation_definition.type == 'note':
1005
+ if 'system' not in metadata:
1006
+ metadata['system'] = dict()
1007
+ metadata['system']['status'] = annotation_definition.status
1008
+
1009
+ # handle fps
1010
+ fps = None
1011
+ if item is not None:
1012
+ if item.fps is not None:
1013
+ fps = item.fps
1014
+ elif item.mimetype is not None and 'audio' in item.mimetype:
1015
+ fps = 1000
1016
+
1017
+ # get type
1018
+ ann_type = None
1019
+ if annotation_definition is not None:
1020
+ ann_type = annotation_definition.type
1021
+
1022
+ # dataset
1023
+ dataset_url = None
1024
+ dataset_id = None
1025
+ if item is not None:
1026
+ dataset_url = item.dataset_url
1027
+ dataset_id = item.dataset_id
1028
+
1029
+ if start_time is None:
1030
+ if fps is not None and frame_num is not None:
1031
+ start_time = frame_num / fps if fps != 0 else 0
1032
+ else:
1033
+ start_time = 0
1034
+ if end_time is None:
1035
+ end_time = start_time
1036
+
1037
+ if frame_num is None:
1038
+ frame_num = 0
1039
+
1040
+ # frames
1041
+ frames = entities.ReflectDict(
1042
+ value_type=FrameAnnotation,
1043
+ on_access=Annotation.on_access,
1044
+ start=frame_num,
1045
+ end=frame_num
1046
+ )
1047
+
1048
+ res = cls(
1049
+ # platform
1050
+ id=None,
1051
+ url=None,
1052
+ item_url=None,
1053
+ item=item,
1054
+ item_id=None,
1055
+ creator=None,
1056
+ created_at=None,
1057
+ updated_by=None,
1058
+ updated_at=None,
1059
+ object_id=object_id,
1060
+ type=ann_type,
1061
+ dataset_url=dataset_url,
1062
+ dataset_id=dataset_id,
1063
+ item_height=item_height,
1064
+ item_width=item_width,
1065
+
1066
+ # meta
1067
+ metadata=metadata,
1068
+ fps=fps,
1069
+ status=None,
1070
+ automated=automated,
1071
+
1072
+ # snapshots
1073
+ frames=frames,
1074
+
1075
+ # video only attributes
1076
+ end_frame=frame_num,
1077
+ end_time=end_time,
1078
+ start_frame=frame_num,
1079
+ start_time=start_time,
1080
+
1081
+ # temp
1082
+ platform_dict=dict(),
1083
+ source='sdk'
1084
+ )
1085
+
1086
+ if annotation_definition:
1087
+ res.annotation_definition = annotation_definition
1088
+
1089
+ if annotation_definition and annotation_definition.attributes is not None:
1090
+ res.attributes = annotation_definition.attributes
1091
+
1092
+ return res
1093
+
1094
+ def add_frames(self,
1095
+ annotation_definition,
1096
+ frame_num=None,
1097
+ end_frame_num=None,
1098
+ start_time=None,
1099
+ end_time=None,
1100
+ fixed=True,
1101
+ object_visible=True):
1102
+ """
1103
+ Add a frames state to annotation
1104
+
1105
+ **Prerequisites**: Any user can upload annotations.
1106
+
1107
+ :param annotation_definition: annotation type object - must be same type as annotation
1108
+ :param int frame_num: first frame number
1109
+ :param int end_frame_num: last frame number
1110
+ :param start_time: starting time for video
1111
+ :param end_time: ending time for video
1112
+ :param bool fixed: is fixed
1113
+ :param bool object_visible: does the annotated object is visible
1114
+ :return:
1115
+
1116
+ **Example**:
1117
+
1118
+ .. code-block:: python
1119
+
1120
+ annotation.add_frames(frame_num=10,
1121
+ annotation_definition=dl.Box(top=10,left=10,bottom=100, right=100,label='labelName'))
1122
+ )
1123
+ """
1124
+ # handle fps
1125
+ if self.fps is None:
1126
+ if self._item is not None:
1127
+ if self._item.fps is not None:
1128
+ self.fps = self._item.fps
1129
+ if self.fps is None:
1130
+ raise PlatformException('400', 'Annotation must have fps in order to perform this action')
1131
+
1132
+ if frame_num is None:
1133
+ frame_num = int(np.round(start_time * self.fps))
1134
+
1135
+ if end_frame_num is None:
1136
+ if end_time is not None:
1137
+ end_frame_num = int(np.round(end_time * self.fps))
1138
+ else:
1139
+ end_frame_num = frame_num
1140
+
1141
+ for frame in range(frame_num, end_frame_num + 1):
1142
+ self.add_frame(annotation_definition=annotation_definition,
1143
+ frame_num=frame,
1144
+ fixed=fixed,
1145
+ object_visible=object_visible)
1146
+
1147
+ def add_frame(self,
1148
+ annotation_definition,
1149
+ frame_num=None,
1150
+ fixed=True,
1151
+ object_visible=True):
1152
+ """
1153
+ Add a frame state to annotation
1154
+
1155
+ :param annotation_definition: annotation type object - must be same type as annotation
1156
+ :param int frame_num: frame number
1157
+ :param bool fixed: is fixed
1158
+ :param bool object_visible: does the annotated object is visible
1159
+ :return: True if success
1160
+ :rtype: bool
1161
+
1162
+ **Example**:
1163
+
1164
+ .. code-block:: python
1165
+
1166
+ success = annotation.add_frame(frame_num=10,
1167
+ annotation_definition=dl.Box(top=10,left=10,bottom=100, right=100,label='labelName'))
1168
+ )
1169
+ """
1170
+ # handle fps
1171
+ if self.fps is None:
1172
+ if self._item is not None:
1173
+ if self._item.fps is not None:
1174
+ self.fps = self._item.fps
1175
+ if self.fps is None:
1176
+ raise PlatformException('400', 'Annotation must have fps in order to perform this action')
1177
+
1178
+ # if this is first frame
1179
+ if self.annotation_definition is None:
1180
+
1181
+ if frame_num is None:
1182
+ frame_num = 0
1183
+
1184
+ frame = FrameAnnotation.new(annotation_definition=annotation_definition,
1185
+ frame_num=frame_num,
1186
+ fixed=fixed,
1187
+ object_visible=object_visible,
1188
+ annotation=self)
1189
+
1190
+ self.frames[frame_num] = frame
1191
+ self.set_frame(frame_num)
1192
+ self.current_frame = frame_num
1193
+ self.end_frame = frame_num
1194
+ self.type = annotation_definition.type
1195
+
1196
+ return True
1197
+
1198
+ # check if type matches annotation
1199
+ if not isinstance(annotation_definition, type(self.annotation_definition)):
1200
+ raise PlatformException('400', 'All frames in annotation must have same type')
1201
+
1202
+ # find frame number
1203
+ if frame_num is None:
1204
+ frame_num = self.last_frame + 1
1205
+ elif frame_num < self.start_frame:
1206
+ self.start_frame = frame_num
1207
+
1208
+ # add frame to annotation
1209
+ if not self.is_video:
1210
+ # create first frame from annotation definition
1211
+ frame = FrameAnnotation.new(annotation_definition=self.annotation_definition,
1212
+ frame_num=self.last_frame,
1213
+ fixed=fixed,
1214
+ object_visible=object_visible,
1215
+ annotation=self)
1216
+
1217
+ self.frames[self.start_frame] = frame
1218
+
1219
+ # create new time annotations
1220
+ frame = FrameAnnotation.new(annotation_definition=annotation_definition,
1221
+ frame_num=frame_num,
1222
+ fixed=fixed,
1223
+ object_visible=object_visible,
1224
+ annotation=self)
1225
+
1226
+ self.frames[frame_num] = frame
1227
+ if not self.frames[frame_num].fixed and self.start_frame < frame_num:
1228
+ frame_last = self.frames[frame_num - 1].to_snapshot()
1229
+ frame_current = self.frames[frame_num].to_snapshot()
1230
+ frame_last.pop('frame')
1231
+ frame_current.pop('frame')
1232
+ if frame_last == frame_current:
1233
+ self.frames[frame_num]._interpolation = True
1234
+ self.end_frame = max(self.last_frame, frame_num)
1235
+
1236
+ return True
1237
+
1238
+ @staticmethod
1239
+ def _protected_from_json(_json,
1240
+ item=None,
1241
+ client_api=None,
1242
+ annotations=None,
1243
+ is_video=None,
1244
+ fps=None,
1245
+ item_metadata=None,
1246
+ dataset=None):
1247
+ """
1248
+ Same as from_json but with try-except to catch if error
1249
+
1250
+ :param _json: platform json
1251
+ :param item: item
1252
+ :param client_api: ApiClient entity
1253
+ :param annotations:
1254
+ :param is_video:
1255
+ :param fps:
1256
+ :param item_metadata:
1257
+ :param dataset
1258
+ :return: annotation object
1259
+ """
1260
+ try:
1261
+ annotation = Annotation.from_json(_json=_json,
1262
+ item=item,
1263
+ client_api=client_api,
1264
+ annotations=annotations,
1265
+ is_video=is_video,
1266
+ fps=fps,
1267
+ item_metadata=item_metadata,
1268
+ dataset=dataset)
1269
+ status = True
1270
+ except Exception:
1271
+ annotation = traceback.format_exc()
1272
+ status = False
1273
+ return status, annotation
1274
+
1275
+ @classmethod
1276
+ def from_json(cls,
1277
+ _json,
1278
+ item=None,
1279
+ client_api=None,
1280
+ annotations=None,
1281
+ is_video=None,
1282
+ fps=None,
1283
+ item_metadata=None,
1284
+ dataset=None,
1285
+ is_audio=None):
1286
+ """
1287
+ Create an annotation object from platform json
1288
+
1289
+ :param dict _json: platform json
1290
+ :param dtlpy.entities.item.Item item: item
1291
+ :param client_api: ApiClient entity
1292
+ :param annotations:
1293
+ :param bool is_video: is video
1294
+ :param fps: video fps
1295
+ :param item_metadata: item metadata
1296
+ :param dataset: dataset entity
1297
+ :param bool is_audio: is audio
1298
+ :return: annotation object
1299
+ :rtype: dtlpy.entities.annotation.Annotation
1300
+ """
1301
+ if item_metadata is None:
1302
+ item_metadata = dict()
1303
+
1304
+ if is_video is None:
1305
+ if item is None:
1306
+ if _json['metadata'].get('system', {}).get('endFrame', 0) > 0:
1307
+ is_video = True
1308
+ else:
1309
+ is_video = False
1310
+ else:
1311
+ # get item type
1312
+ if item.mimetype is not None and 'video' in item.mimetype:
1313
+ is_video = True
1314
+
1315
+ if is_audio is None:
1316
+ if item is None:
1317
+ is_audio = False
1318
+ else:
1319
+ # get item type
1320
+ if item.mimetype is not None and 'audio' in item.mimetype:
1321
+ is_audio = True
1322
+
1323
+ item_url = _json.get('item', item.url if item is not None else None)
1324
+ item_id = _json.get('itemId', item.id if item is not None else None)
1325
+ dataset_url = _json.get('dataset', item.dataset_url if item is not None else None)
1326
+ dataset_id = _json.get('datasetId', item.dataset_id if item is not None else None)
1327
+
1328
+ if item is not None:
1329
+ if item.id != item_id:
1330
+ logger.warning('Annotation has been fetched from a item that is not belong to it')
1331
+ item = None
1332
+
1333
+ if dataset is not None:
1334
+ if dataset.id != dataset_id:
1335
+ logger.warning('Annotation has been fetched from a dataset that is not belong to it')
1336
+ dataset = None
1337
+
1338
+ # get id
1339
+ annotation_id = None
1340
+ if 'id' in _json:
1341
+ annotation_id = _json['id']
1342
+ elif '_id' in _json:
1343
+ annotation_id = _json['_id']
1344
+
1345
+ metadata = _json.get('metadata', dict())
1346
+
1347
+ # get metadata, status, attributes and object id
1348
+ object_id = None
1349
+ status = None
1350
+ if 'system' in metadata and metadata['system'] is not None:
1351
+ object_id = _json['metadata']['system'].get('objectId', object_id)
1352
+ status = _json['metadata']['system'].get('status', status)
1353
+
1354
+ named_attributes = metadata.get('system', dict()).get('attributes', None)
1355
+ recipe_1_attributes = _json.get('attributes', None)
1356
+
1357
+ first_frame_attributes = recipe_1_attributes
1358
+ first_frame_coordinates = list()
1359
+ first_frame_number = 0
1360
+ first_frame_start_time = 0
1361
+ automated = None
1362
+ end_frame = None
1363
+ start_time = 0
1364
+ start_frame = 0
1365
+ end_time = None
1366
+ annotation_definition = None
1367
+
1368
+ ############
1369
+ # if video #
1370
+ ############
1371
+ if is_video or is_audio:
1372
+ # get fps
1373
+ if item is not None and item.fps is not None:
1374
+ fps = item.fps
1375
+ if fps is None:
1376
+ if is_video:
1377
+ fps = item_metadata.get('fps', 25)
1378
+ else:
1379
+ item_metadata.get('fps', 1000)
1380
+
1381
+ # get video-only attributes
1382
+ end_time = 1.5
1383
+ # get first frame attribute
1384
+ first_frame_attributes = first_frame_attributes
1385
+ # get first frame coordinates
1386
+ first_frame_coordinates = _json.get('coordinates', first_frame_coordinates)
1387
+ if 'system' in metadata:
1388
+ # get first frame number
1389
+ first_frame_number = _json['metadata']['system'].get('frame', first_frame_number)
1390
+ # get first frame start time
1391
+ start_time = _json['metadata']['system'].get('startTime', first_frame_start_time)
1392
+ # get first frame number
1393
+ start_frame = _json['metadata']['system'].get('frame', start_frame)
1394
+ automated = _json['metadata']['system'].get('automated', automated)
1395
+ end_frame = _json['metadata']['system'].get('endFrame', end_frame)
1396
+ end_time = _json['metadata']['system'].get('endTime', end_time)
1397
+ ################
1398
+ # if not video #
1399
+ ################
1400
+ if not is_video or is_audio:
1401
+ # get coordinates
1402
+ coordinates = _json.get('coordinates', list())
1403
+ # set video only attributes
1404
+ if not is_audio:
1405
+ end_time = 0
1406
+ # get automated
1407
+ if 'system' in metadata and metadata['system'] is not None:
1408
+ automated = metadata['system'].get('automated', automated)
1409
+ # set annotation definition
1410
+ def_dict = {'type': _json['type'],
1411
+ 'coordinates': coordinates,
1412
+ 'label': _json['label'],
1413
+ 'attributes': named_attributes}
1414
+ annotation_definition = FrameAnnotation.json_to_annotation_definition(def_dict)
1415
+
1416
+ frames = entities.ReflectDict(
1417
+ value_type=FrameAnnotation,
1418
+ on_access=Annotation.on_access,
1419
+ start=start_frame,
1420
+ end=end_frame
1421
+ )
1422
+
1423
+ # init annotation
1424
+ annotation = cls(
1425
+ # temp
1426
+ platform_dict=copy.deepcopy(_json),
1427
+ # platform
1428
+ id=annotation_id,
1429
+ url=_json.get('url', None),
1430
+ item_url=item_url,
1431
+ item=item,
1432
+ item_id=item_id,
1433
+ dataset=dataset,
1434
+ dataset_url=dataset_url,
1435
+ dataset_id=dataset_id,
1436
+ creator=_json['creator'],
1437
+ created_at=_json['createdAt'],
1438
+ updated_by=_json['updatedBy'],
1439
+ updated_at=_json['updatedAt'],
1440
+ hash=_json.get('hash', None),
1441
+ object_id=object_id,
1442
+ type=_json['type'],
1443
+ item_width=item_metadata.get('width', None),
1444
+ item_height=item_metadata.get('height', None),
1445
+ # meta
1446
+ metadata=metadata,
1447
+ fps=fps,
1448
+ status=status,
1449
+ # snapshots
1450
+ frames=frames,
1451
+ # video attributes
1452
+ automated=automated,
1453
+ end_frame=end_frame,
1454
+ end_time=end_time,
1455
+ start_frame=start_frame,
1456
+ annotations=annotations,
1457
+ start_time=start_time,
1458
+ label_suggestions=_json.get('labelSuggestions', None),
1459
+ source=_json.get('source', None),
1460
+ recipe_1_attributes=recipe_1_attributes,
1461
+ )
1462
+ annotation.annotation_definition = annotation_definition
1463
+ annotation.__client_api = client_api
1464
+ if annotation_definition:
1465
+ description = _json.get('description', None)
1466
+ if description is not None:
1467
+ annotation.description = description
1468
+
1469
+ #################
1470
+ # if has frames #
1471
+ #################
1472
+ if is_video:
1473
+ # set first frame
1474
+ snapshot = {
1475
+ 'attributes': first_frame_attributes,
1476
+ 'coordinates': first_frame_coordinates,
1477
+ 'fixed': True,
1478
+ 'objectVisible': True,
1479
+ 'frame': first_frame_number,
1480
+ 'label': _json['label'],
1481
+ 'type': annotation.type,
1482
+ 'namedAttributes': named_attributes
1483
+ }
1484
+
1485
+ # add first frame
1486
+ frame = FrameAnnotation.from_snapshot(
1487
+ _json=snapshot,
1488
+ annotation=annotation,
1489
+ fps=fps
1490
+ )
1491
+ annotation.frames[frame.frame_num] = frame
1492
+ annotation.annotation_definition = frame.annotation_definition
1493
+
1494
+ # put snapshots if there are any
1495
+ for snapshot in _json['metadata']['system'].get('snapshots_', []):
1496
+ frame = FrameAnnotation.from_snapshot(
1497
+ _json=snapshot,
1498
+ annotation=annotation,
1499
+ fps=fps
1500
+ )
1501
+
1502
+ annotation.frames[frame.frame_num] = frame
1503
+
1504
+ annotation.annotation_definition = annotation.frames[min(frames.actual_keys())].annotation_definition
1505
+ if annotation.annotation_definition:
1506
+ description = _json.get('description', None)
1507
+ if description is not None:
1508
+ annotation.description = description
1509
+
1510
+ return annotation
1511
+
1512
+ @staticmethod
1513
+ def on_access(reflect_dict, actual_key: int, requested_key: int, val):
1514
+ val = copy.copy(val)
1515
+ val._interpolation = True
1516
+ val.fixed = False
1517
+ val.frame_num = requested_key
1518
+ reflect_dict[requested_key] = val
1519
+ return val
1520
+
1521
+ def to_json(self):
1522
+ """
1523
+ Convert annotation object to a platform json representatio
1524
+
1525
+ :return: platform json
1526
+ :rtype: dict
1527
+ """
1528
+ if len(self.frames.actual_keys()) > 0:
1529
+ self.set_frame(min(self.frames.actual_keys()))
1530
+ _json = attr.asdict(self,
1531
+ filter=attr.filters.include(attr.fields(Annotation).id,
1532
+ attr.fields(Annotation).url,
1533
+ attr.fields(Annotation).metadata,
1534
+ attr.fields(Annotation).creator,
1535
+ attr.fields(Annotation).hash,
1536
+ attr.fields(Annotation).metadata))
1537
+
1538
+ # property attributes
1539
+ item_id = self.item_id
1540
+ if item_id is None and self._item is not None:
1541
+ item_id = self._item.id
1542
+
1543
+ _json['itemId'] = item_id
1544
+ _json['item'] = self.item_url
1545
+ _json['label'] = self.label
1546
+
1547
+ # Need to put back after transition to attributes 2.0
1548
+ # _json['attributes'] = self.attributes
1549
+
1550
+ _json['dataset'] = self.dataset_url
1551
+
1552
+ _json['createdAt'] = self.created_at
1553
+ _json['updatedBy'] = self.updated_by
1554
+ _json['updatedAt'] = self.updated_at
1555
+ _json['source'] = self.source
1556
+
1557
+ if self.description is not None:
1558
+ _json['description'] = self.description
1559
+
1560
+ if self.label_suggestions:
1561
+ _json['labelSuggestions'] = self.label_suggestions
1562
+
1563
+ if self._item is not None and self.dataset_id is None:
1564
+ _json['datasetId'] = self._item.dataset_id
1565
+ else:
1566
+ _json['datasetId'] = self.dataset_id
1567
+
1568
+ _json['type'] = self.type
1569
+ if self.type != 'class':
1570
+ _json['coordinates'] = self.coordinates
1571
+
1572
+ # add system metadata
1573
+ if _json['metadata'].get('system', None) is None:
1574
+ _json['metadata']['system'] = dict()
1575
+ if self.automated is not None:
1576
+ _json['metadata']['system']['automated'] = self.automated
1577
+ if self.object_id is not None:
1578
+ _json['metadata']['system']['objectId'] = self.object_id
1579
+ if self.status is not None:
1580
+ # if status is CLEAR need to set status to None so it will be deleted in backend
1581
+ _json['metadata']['system']['status'] = self.status if self.status != AnnotationStatus.CLEAR else None
1582
+
1583
+ if isinstance(self.annotation_definition, entities.Description):
1584
+ _json['metadata']['system']['system'] = True
1585
+
1586
+ _json['metadata']['system']['attributes'] = self.attributes if self.attributes is not None else dict()
1587
+ _json['attributes'] = self._recipe_1_attributes
1588
+
1589
+
1590
+ # add frame info
1591
+ if self.is_video or (self.end_time and self.end_time > 0) or (self.end_frame and self.end_frame > 0):
1592
+ # get all snapshots but the first one
1593
+ snapshots = list()
1594
+ frame_numbers = self.frames.actual_keys() if len(self.frames.actual_keys()) else []
1595
+ for frame_num in sorted(frame_numbers):
1596
+ if frame_num <= self.frames.start:
1597
+ continue
1598
+ if frame_num > self.frames.end:
1599
+ break
1600
+ if not self.frames[frame_num]._interpolation or self.frames[frame_num].fixed:
1601
+ snapshots.append(self.frames[frame_num].to_snapshot())
1602
+ self.frames[frame_num]._interpolation = False
1603
+ # add metadata to json
1604
+ _json['metadata']['system']['frame'] = self.start_frame
1605
+ _json['metadata']['system']['startTime'] = self.start_time
1606
+ _json['metadata']['system']['endTime'] = self.end_time
1607
+ if self.end_frame is not None:
1608
+ _json['metadata']['system']['endFrame'] = self.end_frame
1609
+
1610
+ # add snapshots only if classification
1611
+ if self.type not in ['subtitle']:
1612
+ _json['metadata']['system']['snapshots_'] = snapshots
1613
+
1614
+ return _json
1615
+
1616
+ def task_scores(self, task_id: str, page_offset: int = None, page_size: int = None):
1617
+ """
1618
+ Get the scores of the annotation in a specific task.
1619
+ :param task_id: The ID of the task.
1620
+ :param page_offset: The page offset.
1621
+ :param page_size: The page size.
1622
+ :return: page of scores
1623
+ """
1624
+ return self.annotations.task_scores(annotation_id=self.id ,task_id=task_id, page_offset=page_offset, page_size=page_size)
1625
+
1626
+
1627
+
1628
+ @attr.s
1629
+ class FrameAnnotation(entities.BaseEntity):
1630
+ """
1631
+ FrameAnnotation object
1632
+ """
1633
+ # parent annotation
1634
+ annotation = attr.ib()
1635
+
1636
+ # annotations
1637
+ annotation_definition = attr.ib()
1638
+
1639
+ # multi
1640
+ frame_num = attr.ib()
1641
+ fixed = attr.ib()
1642
+ object_visible = attr.ib()
1643
+
1644
+ # temp
1645
+ _interpolation = attr.ib(repr=False, default=False)
1646
+ _recipe_1_attributes = attr.ib(repr=False, default=None)
1647
+
1648
+ ################################
1649
+ # parent annotation attributes #
1650
+ ################################
1651
+
1652
+ @property
1653
+ def status(self):
1654
+ return self.annotation.status
1655
+
1656
+ @property
1657
+ def timestamp(self):
1658
+ if self.annotation.fps is not None and self.frame_num is not None:
1659
+ return self.frame_num / self.annotation.fps if self.annotation.fps != 0 else None
1660
+
1661
+ ####################################
1662
+ # annotation definition attributes #
1663
+ ####################################
1664
+ @property
1665
+ def type(self):
1666
+ return self.annotation.type
1667
+
1668
+ @property
1669
+ def label(self):
1670
+ return self.annotation_definition.label
1671
+
1672
+ @label.setter
1673
+ def label(self, label):
1674
+ self.annotation_definition.label = label
1675
+
1676
+ @property
1677
+ def attributes(self):
1678
+ return self.annotation_definition.attributes
1679
+
1680
+ @attributes.setter
1681
+ def attributes(self, attributes):
1682
+ self.annotation_definition.attributes = attributes
1683
+
1684
+ @property
1685
+ def geo(self):
1686
+ return self.annotation_definition.geo
1687
+
1688
+ @property
1689
+ def top(self):
1690
+ return self.annotation_definition.top
1691
+
1692
+ @property
1693
+ def bottom(self):
1694
+ return self.annotation_definition.bottom
1695
+
1696
+ @property
1697
+ def left(self):
1698
+ return self.annotation_definition.left
1699
+
1700
+ @property
1701
+ def right(self):
1702
+ return self.annotation_definition.right
1703
+
1704
+ @property
1705
+ def color(self):
1706
+ if self.annotation.item is None:
1707
+ return 255, 255, 255
1708
+ else:
1709
+ label = None
1710
+ for label in self.annotation.item.dataset.labels:
1711
+ if label.tag.lower() == self.label.lower():
1712
+ return label.rgb
1713
+ if label is None:
1714
+ return 255, 255, 255
1715
+
1716
+ @property
1717
+ def coordinates(self):
1718
+ coordinates = self.annotation_definition.to_coordinates(color=self.color)
1719
+ return coordinates
1720
+
1721
+ @property
1722
+ def x(self):
1723
+ return self.annotation_definition.x
1724
+
1725
+ @property
1726
+ def y(self):
1727
+ return self.annotation_definition.y
1728
+
1729
+ @property
1730
+ def rx(self):
1731
+ if self.annotation_definition.type == 'ellipse':
1732
+ return self.annotation_definition.rx
1733
+ else:
1734
+ return None
1735
+
1736
+ @property
1737
+ def ry(self):
1738
+ if self.annotation_definition.type == 'ellipse':
1739
+ return self.annotation_definition.ry
1740
+ else:
1741
+ return None
1742
+
1743
+ @property
1744
+ def angle(self):
1745
+ if self.annotation_definition.type in ['ellipse', 'box']:
1746
+ return self.annotation_definition.angle
1747
+ else:
1748
+ return None
1749
+
1750
+ ######################
1751
+ # annotation methods #
1752
+ ######################
1753
+ def show(self, **kwargs):
1754
+ """
1755
+ Show annotation as ndarray
1756
+ :param kwargs: see annotation definition
1757
+ :return: ndarray of the annotation
1758
+ """
1759
+ return self.annotation_definition.show(**kwargs)
1760
+
1761
+ @staticmethod
1762
+ def json_to_annotation_definition(_json):
1763
+ if 'namedAttributes' in _json:
1764
+ _json['attributes'] = _json['namedAttributes']
1765
+ else:
1766
+ if not isinstance(_json.get('attributes'), dict):
1767
+ _json['attributes'] = None
1768
+ if _json['type'] == 'segment':
1769
+ annotation = entities.Polygon.from_json(_json)
1770
+ elif _json['type'] == 'polyline':
1771
+ annotation = entities.Polyline.from_json(_json)
1772
+ elif _json['type'] == 'box':
1773
+ annotation = entities.Box.from_json(_json)
1774
+ elif _json['type'] == 'cube':
1775
+ annotation = entities.Cube.from_json(_json)
1776
+ elif _json['type'] == 'cube_3d':
1777
+ annotation = entities.Cube3d.from_json(_json)
1778
+ elif _json['type'] == 'point':
1779
+ annotation = entities.Point.from_json(_json)
1780
+ elif _json['type'] == 'binary':
1781
+ annotation = entities.Segmentation.from_json(_json)
1782
+ elif _json['type'] == 'class':
1783
+ annotation = entities.Classification.from_json(_json)
1784
+ elif _json['type'] == 'subtitle':
1785
+ annotation = entities.Subtitle.from_json(_json)
1786
+ elif _json['type'] == 'ellipse':
1787
+ annotation = entities.Ellipse.from_json(_json)
1788
+ elif _json['type'] == 'comparison':
1789
+ annotation = entities.Comparison.from_json(_json)
1790
+ elif _json['type'] == 'note':
1791
+ annotation = entities.Note.from_json(_json)
1792
+ elif _json['type'] == 'pose':
1793
+ annotation = entities.Pose.from_json(_json)
1794
+ elif _json['type'] == 'gis':
1795
+ annotation = entities.Gis.from_json(_json)
1796
+ else:
1797
+ annotation = entities.UndefinedAnnotationType.from_json(_json)
1798
+ return annotation
1799
+
1800
+ #######
1801
+ # I/O #
1802
+ #######
1803
+ @classmethod
1804
+ def new(cls, annotation, annotation_definition, frame_num, fixed, object_visible=True):
1805
+ """
1806
+ new frame state to annotation
1807
+
1808
+ :param annotation: annotation
1809
+ :param annotation_definition: annotation type object - must be same type as annotation
1810
+ :param frame_num: frame number
1811
+ :param fixed: is fixed
1812
+ :param object_visible: does the annotated object is visible
1813
+ :return: FrameAnnotation object
1814
+ """
1815
+ frame = cls(
1816
+ # annotations
1817
+ annotation=annotation,
1818
+ annotation_definition=annotation_definition,
1819
+
1820
+ # multi
1821
+ frame_num=frame_num,
1822
+ fixed=fixed,
1823
+ object_visible=object_visible,
1824
+ )
1825
+ if annotation_definition.attributes:
1826
+ frame.attributes = annotation_definition.attributes
1827
+ return frame
1828
+
1829
+ @classmethod
1830
+ def from_snapshot(cls, annotation, _json, fps):
1831
+ """
1832
+ new frame state to annotation
1833
+
1834
+ :param annotation: annotation
1835
+ :param _json: annotation type object - must be same type as annotation
1836
+ :param fps: frame number
1837
+ :return: FrameAnnotation object
1838
+ """
1839
+ # get annotation class
1840
+ _json['type'] = annotation.type
1841
+ attrs = _json.get('attributes', None)
1842
+ annotation_definition = cls.json_to_annotation_definition(_json=_json)
1843
+
1844
+ frame_num = _json.get('frame', annotation.last_frame + 1)
1845
+
1846
+ return cls(
1847
+ # annotations
1848
+ annotation=annotation,
1849
+ annotation_definition=annotation_definition,
1850
+
1851
+ # multi
1852
+ frame_num=frame_num,
1853
+ fixed=_json.get('fixed', False),
1854
+ object_visible=_json.get('objectVisible', True),
1855
+ recipe_1_attributes=attrs,
1856
+ )
1857
+
1858
+ def to_snapshot(self):
1859
+
1860
+ snapshot_dict = {
1861
+ 'frame': self.frame_num,
1862
+ 'fixed': self.fixed,
1863
+ 'label': self.label,
1864
+ 'type': self.type,
1865
+ 'objectVisible': self.object_visible,
1866
+ 'data': self.coordinates
1867
+ }
1868
+
1869
+ if self.annotation_definition.description is not None:
1870
+ snapshot_dict['description'] = self.annotation_definition.description
1871
+
1872
+ if self.attributes is not None:
1873
+ snapshot_dict['namedAttributes'] = self.attributes
1874
+
1875
+ if self._recipe_1_attributes is not None:
1876
+ snapshot_dict['attributes'] = self._recipe_1_attributes
1877
+
1878
+
1879
+ return snapshot_dict