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