supervisely 6.73.377__py3-none-any.whl → 6.73.379__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.
- supervisely/__init__.py +1 -1
- supervisely/api/entity_annotation/tag_api.py +223 -20
- supervisely/api/image_api.py +81 -1
- supervisely/api/pointcloud/pointcloud_api.py +8 -0
- supervisely/convert/image/sly/sly_image_converter.py +10 -7
- supervisely/convert/pointcloud/pointcloud_converter.py +103 -12
- supervisely/convert/pointcloud/sly/sly_pointcloud_converter.py +5 -1
- supervisely/convert/pointcloud_episodes/pointcloud_episodes_converter.py +122 -12
- supervisely/convert/pointcloud_episodes/sly/sly_pointcloud_episodes_converter.py +5 -1
- supervisely/project/pointcloud_episode_project.py +126 -4
- supervisely/project/pointcloud_project.py +160 -19
- {supervisely-6.73.377.dist-info → supervisely-6.73.379.dist-info}/METADATA +1 -1
- {supervisely-6.73.377.dist-info → supervisely-6.73.379.dist-info}/RECORD +17 -17
- {supervisely-6.73.377.dist-info → supervisely-6.73.379.dist-info}/LICENSE +0 -0
- {supervisely-6.73.377.dist-info → supervisely-6.73.379.dist-info}/WHEEL +0 -0
- {supervisely-6.73.377.dist-info → supervisely-6.73.379.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.377.dist-info → supervisely-6.73.379.dist-info}/top_level.txt +0 -0
supervisely/__init__.py
CHANGED
|
@@ -314,4 +314,4 @@ except Exception as e:
|
|
|
314
314
|
# If new changes in Supervisely Python SDK require upgrade of the Supervisely instance
|
|
315
315
|
# set a new value for the environment variable MINIMUM_INSTANCE_VERSION_FOR_SDK, otherwise
|
|
316
316
|
# users can face compatibility issues, if the instance version is lower than the SDK version.
|
|
317
|
-
os.environ["MINIMUM_INSTANCE_VERSION_FOR_SDK"] = "6.
|
|
317
|
+
os.environ["MINIMUM_INSTANCE_VERSION_FOR_SDK"] = "6.13.00"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
|
|
3
|
-
from typing import List, Optional
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
4
|
|
|
5
5
|
from supervisely._utils import batched
|
|
6
6
|
from supervisely.api.module_api import ApiField, ModuleApi
|
|
@@ -210,11 +210,13 @@ class TagApi(ModuleApi):
|
|
|
210
210
|
raise RuntimeError("SDK error: len(tags_keys) != len(tags_to_add)")
|
|
211
211
|
if len(tags_keys) == 0:
|
|
212
212
|
return
|
|
213
|
-
ids = self.append_to_objects_json(entity_id, tags_to_add)
|
|
213
|
+
ids = self.append_to_objects_json(entity_id, tags_to_add, project_id)
|
|
214
214
|
KeyIdMap.add_tags_to(key_id_map, tags_keys, ids)
|
|
215
215
|
return ids
|
|
216
216
|
|
|
217
|
-
def append_to_objects_json(
|
|
217
|
+
def append_to_objects_json(
|
|
218
|
+
self, entity_id: int, tags_json: List[Dict], project_id: Optional[int] = None
|
|
219
|
+
) -> List[int]:
|
|
218
220
|
"""
|
|
219
221
|
Add Tags to Annotation Objects for specific entity (image etc.).
|
|
220
222
|
|
|
@@ -224,14 +226,50 @@ class TagApi(ModuleApi):
|
|
|
224
226
|
:type tags_json: dict
|
|
225
227
|
:return: List of tags IDs
|
|
226
228
|
:rtype: list
|
|
229
|
+
|
|
230
|
+
:Usage example:
|
|
231
|
+
|
|
232
|
+
.. code-block:: python
|
|
233
|
+
|
|
234
|
+
import supervisely as sly
|
|
235
|
+
|
|
236
|
+
api = sly.Api(server_address, token)
|
|
237
|
+
|
|
238
|
+
tags_list = [
|
|
239
|
+
{
|
|
240
|
+
"tagId": 25926,
|
|
241
|
+
"objectId": 652959,
|
|
242
|
+
"value": None
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"tagId": 25927,
|
|
246
|
+
"objectId": 652959,
|
|
247
|
+
"value": "v1"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"tagId": 25927,
|
|
251
|
+
"objectId": 652958,
|
|
252
|
+
"value": "v2"
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
response = api.video.tag.append_to_objects_json(12345, tags_list)
|
|
256
|
+
|
|
257
|
+
print(response)
|
|
258
|
+
# Output:
|
|
259
|
+
# [
|
|
260
|
+
# 80421101,
|
|
261
|
+
# 80421102,
|
|
262
|
+
# 80421103
|
|
263
|
+
# ]
|
|
227
264
|
"""
|
|
228
265
|
|
|
229
266
|
if len(tags_json) == 0:
|
|
230
267
|
return []
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
268
|
+
if project_id is not None:
|
|
269
|
+
json_data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: tags_json}
|
|
270
|
+
else:
|
|
271
|
+
json_data = {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: tags_json}
|
|
272
|
+
response = self._api.post("annotation-objects.tags.bulk.add", json_data)
|
|
235
273
|
ids = [obj[ApiField.ID] for obj in response.json()]
|
|
236
274
|
return ids
|
|
237
275
|
|
|
@@ -242,11 +280,21 @@ class TagApi(ModuleApi):
|
|
|
242
280
|
batch_size: int = 100,
|
|
243
281
|
log_progress: bool = False,
|
|
244
282
|
progress: Optional[tqdm_sly] = None,
|
|
245
|
-
) -> List[
|
|
283
|
+
) -> List[Dict[str, Union[str, int, None]]]:
|
|
246
284
|
"""
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
285
|
+
For images project:
|
|
286
|
+
Add Tags to existing Annotation Figures (labels).
|
|
287
|
+
The `tags_list` example:
|
|
288
|
+
[{"tagId": 12345, "figureId": 54321, "value": "tag_value"}, ...].
|
|
289
|
+
For video, pointcloud, volume and pointcloud episodes projects:
|
|
290
|
+
Add Tags to existing Annotation Objects.
|
|
291
|
+
The `frameRange` field is optional and is supported only for video and pointcloud episodes projects.
|
|
292
|
+
The `tags_list`` example:
|
|
293
|
+
[{"tagId": 12345, "objectId": 54321, "value": "tag_value"}, ...].
|
|
294
|
+
or with frameRange:
|
|
295
|
+
[{"tagId": 12345, "objectId": 54321, "value": "tag_value", "frameRange": [1, 10]}, ...].
|
|
296
|
+
|
|
297
|
+
All objects must belong to entities of the same project.
|
|
250
298
|
|
|
251
299
|
:param project_id: Project ID in Supervisely.
|
|
252
300
|
:type project_id: int
|
|
@@ -259,7 +307,7 @@ class TagApi(ModuleApi):
|
|
|
259
307
|
:param progress: Progress bar object to display progress.
|
|
260
308
|
:type progress: Optional[tqdm_sly]
|
|
261
309
|
:return: List of tags infos as dictionaries.
|
|
262
|
-
:rtype: List[
|
|
310
|
+
:rtype: List[Dict[str, Union[str, int, None]]]
|
|
263
311
|
|
|
264
312
|
Usage example:
|
|
265
313
|
.. code-block:: python
|
|
@@ -272,7 +320,8 @@ class TagApi(ModuleApi):
|
|
|
272
320
|
{
|
|
273
321
|
"tagId": 25926,
|
|
274
322
|
"figureId": 652959,
|
|
275
|
-
"value": None #
|
|
323
|
+
"value": None # optional for tag with type 'None'
|
|
324
|
+
"frameRange": [1, 10] # optional (supported only for video and pointcloud episodes projects)
|
|
276
325
|
},
|
|
277
326
|
{
|
|
278
327
|
"tagId": 25927,
|
|
@@ -282,7 +331,7 @@ class TagApi(ModuleApi):
|
|
|
282
331
|
{
|
|
283
332
|
"tagId": 25927,
|
|
284
333
|
"figureId": 652958,
|
|
285
|
-
"value": "v2"
|
|
334
|
+
"value": "v2",
|
|
286
335
|
}
|
|
287
336
|
]
|
|
288
337
|
response = api.image.tag.add_to_figures(12345, tag_list)
|
|
@@ -310,16 +359,14 @@ class TagApi(ModuleApi):
|
|
|
310
359
|
# }
|
|
311
360
|
# ]
|
|
312
361
|
"""
|
|
313
|
-
if type(self) is not TagApi:
|
|
314
|
-
raise NotImplementedError("This method is not available for classes except TagApi")
|
|
315
|
-
|
|
316
|
-
if len(tags_list) == 0:
|
|
317
|
-
return []
|
|
318
362
|
|
|
319
363
|
if progress is not None:
|
|
320
364
|
log_progress = False
|
|
321
365
|
|
|
322
366
|
result = []
|
|
367
|
+
|
|
368
|
+
if len(tags_list) == 0:
|
|
369
|
+
return result
|
|
323
370
|
if log_progress:
|
|
324
371
|
progress = tqdm_sly(
|
|
325
372
|
desc="Adding tags to figures",
|
|
@@ -327,8 +374,164 @@ class TagApi(ModuleApi):
|
|
|
327
374
|
)
|
|
328
375
|
for batch in batched(tags_list, batch_size):
|
|
329
376
|
data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: batch}
|
|
330
|
-
|
|
377
|
+
if type(self) is TagApi:
|
|
378
|
+
response = self._api.post("figures.tags.bulk.add", data)
|
|
379
|
+
else:
|
|
380
|
+
response = self._api.post("annotation-objects.tags.bulk.add", data)
|
|
331
381
|
result.extend(response.json())
|
|
332
382
|
if progress is not None:
|
|
333
383
|
progress.update(len(batch))
|
|
334
384
|
return result
|
|
385
|
+
|
|
386
|
+
def add_to_entities_json(
|
|
387
|
+
self,
|
|
388
|
+
project_id: int,
|
|
389
|
+
tags_list: List[Dict[str, Union[str, int, None]]],
|
|
390
|
+
batch_size: int = 100,
|
|
391
|
+
log_progress: bool = False,
|
|
392
|
+
) -> List[int]:
|
|
393
|
+
"""
|
|
394
|
+
Bulk add tags to entities (images, videos, pointclouds, volumes) in a project.
|
|
395
|
+
Not supported for pointcloud episodes projects.
|
|
396
|
+
All entities must belong to the same project.
|
|
397
|
+
The `frameRange` field in a tag object within the tags list is optional and is supported only for video projects.
|
|
398
|
+
|
|
399
|
+
The `tags_list` example:
|
|
400
|
+
[{"tagId": 12345, "entityId": 54321, "value": "tag_value"}, ...].
|
|
401
|
+
or with frameRange:
|
|
402
|
+
[{"tagId": 12345, "entityId": 54321, "value": "tag_value", "frameRange": [1, 10]}, ...].
|
|
403
|
+
|
|
404
|
+
:param project_id: Project ID in Supervisely.
|
|
405
|
+
:type project_id: int
|
|
406
|
+
:param tags_list: List of tag object infos as dictionaries
|
|
407
|
+
(e.g. {"tagId": 12345, "entityId": 54321, "value": "tag_value"}).
|
|
408
|
+
:param batch_size: Number of tags to add in one request.
|
|
409
|
+
:type batch_size: int
|
|
410
|
+
:param log_progress: If True, will display a progress bar.
|
|
411
|
+
:type log_progress: bool
|
|
412
|
+
:return: List of tags IDs.
|
|
413
|
+
:rtype: List[int]
|
|
414
|
+
|
|
415
|
+
Usage example:
|
|
416
|
+
.. code-block:: python
|
|
417
|
+
|
|
418
|
+
import supervisely as sly
|
|
419
|
+
|
|
420
|
+
api = sly.Api(server_address, token)
|
|
421
|
+
|
|
422
|
+
tag_list = [
|
|
423
|
+
{
|
|
424
|
+
"tagId": 25926,
|
|
425
|
+
"entityId": 652959,
|
|
426
|
+
"value": None # optional for tag with type 'None'
|
|
427
|
+
"frameRange": [1, 10] # optional (supported only for video projects)
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"tagId": 25927,
|
|
431
|
+
"entityId": 652959,
|
|
432
|
+
"value": "v1"
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"tagId": 25927,
|
|
436
|
+
"entityId": 652958,
|
|
437
|
+
"value": "v2"
|
|
438
|
+
}
|
|
439
|
+
]
|
|
440
|
+
api.image.tag.add_to_entities_json(project_id=12345, tag_list=tag_list)
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
result = []
|
|
444
|
+
|
|
445
|
+
if len(tags_list) == 0:
|
|
446
|
+
return result
|
|
447
|
+
|
|
448
|
+
if log_progress:
|
|
449
|
+
ds_progress = tqdm_sly(desc="Adding tags to entities", total=len(tags_list))
|
|
450
|
+
|
|
451
|
+
for batch in batched(tags_list, batch_size):
|
|
452
|
+
data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: batch}
|
|
453
|
+
response = self._api.post("tags.entities.bulk.add", data)
|
|
454
|
+
result.extend([obj[ApiField.ID] for obj in response.json()])
|
|
455
|
+
if log_progress:
|
|
456
|
+
ds_progress.update(len(batch))
|
|
457
|
+
|
|
458
|
+
return result
|
|
459
|
+
|
|
460
|
+
def add_tags_collection_to_objects(
|
|
461
|
+
self,
|
|
462
|
+
project_id: int,
|
|
463
|
+
tags_map: Dict[int, Any],
|
|
464
|
+
batch_size: int = 100,
|
|
465
|
+
log_progress: bool = False,
|
|
466
|
+
) -> List[Dict[str, Union[str, int, None]]]:
|
|
467
|
+
"""
|
|
468
|
+
For images project:
|
|
469
|
+
Add Tags to existing Annotation Figures (labels).
|
|
470
|
+
The `tags_map` example: {figure_id_1: TagCollection, ...}.
|
|
471
|
+
For video, pointcloud, volume and pointcloud episodes projects:
|
|
472
|
+
Add Tags to existing Annotation Objects.
|
|
473
|
+
The `frameRange` field is optional and is supported only for video and pointcloud episodes projects.
|
|
474
|
+
The `tags_map` example: {object_id_1: TagCollection, ...}.
|
|
475
|
+
|
|
476
|
+
All objects must belong to entities of the same project.
|
|
477
|
+
|
|
478
|
+
:param project_id: Project ID in Supervisely.
|
|
479
|
+
:type project_id: int
|
|
480
|
+
:param tags_map: Dictionary with mapping figure/object ID to tags collection.
|
|
481
|
+
:type tags_map: Dict[int, Any]
|
|
482
|
+
:param batch_size: Number of tags to add in one request.
|
|
483
|
+
:type batch_size: int
|
|
484
|
+
:param log_progress: If True, will display a progress bar.
|
|
485
|
+
:type log_progress: bool
|
|
486
|
+
:return: List of tags infos as dictionaries.
|
|
487
|
+
:rtype: List[Dit[str, Union[str, int, None]]]
|
|
488
|
+
|
|
489
|
+
Usage example:
|
|
490
|
+
.. code-block:: python
|
|
491
|
+
|
|
492
|
+
import supervisely as sly
|
|
493
|
+
|
|
494
|
+
api = sly.Api(server_address, token)
|
|
495
|
+
|
|
496
|
+
project_id = 12345
|
|
497
|
+
|
|
498
|
+
tag_meta = sly.TagMeta("tag_name", sly.TagValueType.ANY_STRING)
|
|
499
|
+
meta = sly.ProjectMeta(tag_metas=[tag_meta])
|
|
500
|
+
meta = sly.ProjectMeta.from_json(api.project.update_meta(project_id, meta))
|
|
501
|
+
tag_meta = meta.get_tag_meta("tag_name")
|
|
502
|
+
|
|
503
|
+
# for images project:
|
|
504
|
+
tag_map = {
|
|
505
|
+
652959: sly.TagCollection([sly.Tag(tag_meta, value="v1"), sly.Tag(tag_meta, value="v2"), ...]),
|
|
506
|
+
652958: sly.TagCollection([sly.Tag(tag_meta, value="v3"), sly.Tag(tag_meta, value="v4"), ...]),
|
|
507
|
+
...
|
|
508
|
+
}
|
|
509
|
+
api.image.tag.add_tags_to_objects(project_id, tag_map)
|
|
510
|
+
|
|
511
|
+
# for videos projects (frameRange is optional):
|
|
512
|
+
tag_map = {
|
|
513
|
+
652959: sly.VideoTagCollection([sly.VideoTag(tag_meta, value="v1", frameRange=[1, 10]), ...]),
|
|
514
|
+
652958: sly.VideoTagCollection([sly.VideoTag(tag_meta, value="v2", frameRange=[4, 12]), ...]),
|
|
515
|
+
...
|
|
516
|
+
}
|
|
517
|
+
api.video.tag.add_to_objects_json_batch(project_id, tag_map)
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
OBJ_ID_FIELD = ApiField.FIGURE_ID if type(self) is TagApi else ApiField.OBJECT_ID
|
|
521
|
+
|
|
522
|
+
data = []
|
|
523
|
+
for obj_id, tags in tags_map.items():
|
|
524
|
+
for tag in tags:
|
|
525
|
+
|
|
526
|
+
if tag.meta.sly_id is None:
|
|
527
|
+
raise ValueError(f"Tag {tag.name} meta has no sly_id")
|
|
528
|
+
|
|
529
|
+
data.append(
|
|
530
|
+
{
|
|
531
|
+
ApiField.TAG_ID: tag.meta.sly_id,
|
|
532
|
+
OBJ_ID_FIELD: obj_id,
|
|
533
|
+
**tag.to_json()
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
return self.add_to_objects(project_id, data, batch_size, log_progress)
|
supervisely/api/image_api.py
CHANGED
|
@@ -3601,7 +3601,87 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
3601
3601
|
if progress_cb is not None:
|
|
3602
3602
|
progress_cb(len(batch_ids))
|
|
3603
3603
|
|
|
3604
|
-
def
|
|
3604
|
+
def add_tags_batch(
|
|
3605
|
+
self,
|
|
3606
|
+
image_ids: List[int],
|
|
3607
|
+
tag_ids: Union[int, List[int]],
|
|
3608
|
+
values: Optional[Union[str, int, List[Union[str, int, None]]]] = None,
|
|
3609
|
+
log_progress: bool = False,
|
|
3610
|
+
batch_size: Optional[int] = 100,
|
|
3611
|
+
tag_metas: Optional[Union[TagMeta, List[TagMeta]]] = None,
|
|
3612
|
+
) -> List[int]:
|
|
3613
|
+
"""
|
|
3614
|
+
Add tag with given ID to Images by IDs with different values.
|
|
3615
|
+
|
|
3616
|
+
:param image_ids: List of Images IDs in Supervisely.
|
|
3617
|
+
:type image_ids: List[int]
|
|
3618
|
+
:param tag_ids: Tag IDs in Supervisely.
|
|
3619
|
+
:type tag_ids: int or List[int]
|
|
3620
|
+
:param values: List of tag values for each image or single value for all images.
|
|
3621
|
+
:type values: List[str] or List[int] or str or int, optional
|
|
3622
|
+
:param log_progress: If True, will log progress.
|
|
3623
|
+
:type log_progress: bool, optional
|
|
3624
|
+
:param batch_size: Batch size
|
|
3625
|
+
:type batch_size: int, optional
|
|
3626
|
+
:param tag_metas: Tag Metas. Needed for values validation, omit to skip validation
|
|
3627
|
+
:type tag_metas: TagMeta or List[TagMeta], optional
|
|
3628
|
+
:return: List of tags IDs.
|
|
3629
|
+
:rtype: List[int]
|
|
3630
|
+
:Usage example:
|
|
3631
|
+
|
|
3632
|
+
.. code-block:: python
|
|
3633
|
+
|
|
3634
|
+
import supervisely as sly
|
|
3635
|
+
|
|
3636
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
3637
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
3638
|
+
api = sly.Api.from_env()
|
|
3639
|
+
image_ids = [2389126, 2389127]
|
|
3640
|
+
tag_ids = 277083
|
|
3641
|
+
values = ['value1', 'value2']
|
|
3642
|
+
api.image.add_tags_batch(image_ids, tag_ids, values)
|
|
3643
|
+
"""
|
|
3644
|
+
if len(image_ids) == 0:
|
|
3645
|
+
return []
|
|
3646
|
+
|
|
3647
|
+
if isinstance(tag_ids, int):
|
|
3648
|
+
tag_ids = [tag_ids] * len(image_ids)
|
|
3649
|
+
|
|
3650
|
+
if isinstance(tag_metas, TagMeta):
|
|
3651
|
+
tag_metas = [tag_metas] * len(image_ids)
|
|
3652
|
+
|
|
3653
|
+
if values is None:
|
|
3654
|
+
values = [None] * len(image_ids)
|
|
3655
|
+
elif isinstance(values, (str, int)):
|
|
3656
|
+
values = [values] * len(image_ids)
|
|
3657
|
+
|
|
3658
|
+
if len(values) != len(image_ids):
|
|
3659
|
+
raise ValueError("Length of image_ids and values should be the same")
|
|
3660
|
+
|
|
3661
|
+
if len(tag_ids) != len(image_ids):
|
|
3662
|
+
raise ValueError("Length of image_ids and tag_ids should be the same")
|
|
3663
|
+
|
|
3664
|
+
if tag_metas and len(tag_metas) != len(image_ids):
|
|
3665
|
+
raise ValueError("Length of image_ids and tag_metas should be the same")
|
|
3666
|
+
|
|
3667
|
+
if tag_metas:
|
|
3668
|
+
for tag_meta, tag_id, value in zip(tag_metas, tag_ids, values):
|
|
3669
|
+
if not (tag_meta.sly_id == tag_id):
|
|
3670
|
+
raise ValueError(f"{tag_meta.name = } and {tag_id = } should be same")
|
|
3671
|
+
if not tag_meta.is_valid_value(value):
|
|
3672
|
+
raise ValueError(f"{tag_meta.name = } can not have value {value = }")
|
|
3673
|
+
|
|
3674
|
+
project_id = self.get_project_id(image_ids[0])
|
|
3675
|
+
data = [
|
|
3676
|
+
{ApiField.ENTITY_ID: image_id, ApiField.TAG_ID: tag_id, ApiField.VALUE: value}
|
|
3677
|
+
for image_id, tag_id, value in zip(image_ids, tag_ids, values)
|
|
3678
|
+
]
|
|
3679
|
+
|
|
3680
|
+
return self.tag.add_to_entities_json(project_id, data, batch_size, log_progress)
|
|
3681
|
+
|
|
3682
|
+
def update_tag_value(
|
|
3683
|
+
self, tag_id: int, value: Union[str, float]
|
|
3684
|
+
) -> Dict:
|
|
3605
3685
|
"""
|
|
3606
3686
|
Update tag value with given ID.
|
|
3607
3687
|
|
|
@@ -421,6 +421,14 @@ class PointcloudApi(RemoveableBulkModuleApi):
|
|
|
421
421
|
convert_json_info_cb=lambda x: x,
|
|
422
422
|
)
|
|
423
423
|
|
|
424
|
+
def get_list_related_images_batch(self, dataset_id: int, ids: List[int]) -> List:
|
|
425
|
+
filters = [{"field": ApiField.ENTITY_ID, "operator": "in", "value": ids}]
|
|
426
|
+
return self.get_list_all_pages(
|
|
427
|
+
"point-clouds.images.list",
|
|
428
|
+
{ApiField.DATASET_ID: dataset_id, ApiField.FILTER: filters},
|
|
429
|
+
convert_json_info_cb=lambda x: x,
|
|
430
|
+
)
|
|
431
|
+
|
|
424
432
|
def download_related_image(self, id: int, path: str = None) -> Response:
|
|
425
433
|
"""
|
|
426
434
|
Download a related context image from Supervisely to local directory by image id.
|
|
@@ -17,7 +17,7 @@ from supervisely.api.api import Api
|
|
|
17
17
|
from supervisely.convert.base_converter import AvailableImageConverters
|
|
18
18
|
from supervisely.convert.image.image_converter import ImageConverter
|
|
19
19
|
from supervisely.convert.image.image_helper import validate_image_bounds
|
|
20
|
-
from supervisely.io.fs import dirs_filter, file_exists, get_file_ext
|
|
20
|
+
from supervisely.io.fs import dirs_filter, file_exists, get_file_ext, get_file_name
|
|
21
21
|
from supervisely.io.json import load_json_file
|
|
22
22
|
from supervisely.project.project import find_project_dirs
|
|
23
23
|
from supervisely.project.project import upload_project as upload_project_fs
|
|
@@ -122,17 +122,20 @@ class SLYImageConverter(ImageConverter):
|
|
|
122
122
|
self._items = []
|
|
123
123
|
for image_path in images_list:
|
|
124
124
|
item = self.Item(image_path)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
json_name, json_name_noext = f"{item.name}.json", f"{get_file_name(item.name)}.json"
|
|
126
|
+
ann_path = ann_dict.get(json_name, ann_dict.get(json_name_noext))
|
|
127
|
+
if ann_path:
|
|
128
128
|
if self._meta is None:
|
|
129
129
|
meta = self.generate_meta_from_annotation(ann_path, meta)
|
|
130
130
|
is_valid = self.validate_ann_file(ann_path, meta)
|
|
131
131
|
if is_valid:
|
|
132
132
|
item.ann_data = ann_path
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
|
|
134
|
+
meta_path = img_meta_dict.get(json_name, img_meta_dict.get(json_name_noext))
|
|
135
|
+
if meta_path:
|
|
136
|
+
item.set_meta_data(meta_path)
|
|
137
|
+
if item.ann_data is not None or item.meta is not None:
|
|
138
|
+
detected_ann_cnt += 1
|
|
136
139
|
self._items.append(item)
|
|
137
140
|
self._meta = meta
|
|
138
141
|
return detected_ann_cnt > 0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import imghdr
|
|
2
2
|
import os
|
|
3
|
-
from typing import List, Optional, Set, Tuple
|
|
3
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
4
|
+
from uuid import UUID
|
|
4
5
|
|
|
5
6
|
import supervisely.convert.pointcloud.sly.sly_pointcloud_helper as helpers
|
|
6
7
|
from supervisely import (
|
|
@@ -17,6 +18,8 @@ from supervisely.io.fs import get_file_ext, get_file_name
|
|
|
17
18
|
from supervisely.io.json import load_json_file
|
|
18
19
|
from supervisely.pointcloud.pointcloud import ALLOWED_POINTCLOUD_EXTENSIONS
|
|
19
20
|
from supervisely.pointcloud.pointcloud import validate_ext as validate_pcd_ext
|
|
21
|
+
from supervisely.pointcloud_annotation.constants import OBJECT_KEY
|
|
22
|
+
from supervisely.video_annotation.key_id_map import KeyIdMap
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class PointcloudConverter(BaseConverter):
|
|
@@ -41,7 +44,14 @@ class PointcloudConverter(BaseConverter):
|
|
|
41
44
|
def create_empty_annotation(self) -> PointcloudAnnotation:
|
|
42
45
|
return PointcloudAnnotation()
|
|
43
46
|
|
|
44
|
-
def set_related_images(self, related_images: Tuple[str, str]) -> None:
|
|
47
|
+
def set_related_images(self, related_images: Tuple[str, str, Optional[str]]) -> None:
|
|
48
|
+
"""Adds related image to the item.
|
|
49
|
+
|
|
50
|
+
related_images tuple:
|
|
51
|
+
- path to image
|
|
52
|
+
- path to .json with image metadata
|
|
53
|
+
- path to .figures.json (can be None if no figures)
|
|
54
|
+
"""
|
|
45
55
|
self._related_images.append(related_images)
|
|
46
56
|
|
|
47
57
|
@property
|
|
@@ -98,21 +108,26 @@ class PointcloudConverter(BaseConverter):
|
|
|
98
108
|
item_paths,
|
|
99
109
|
)
|
|
100
110
|
pcd_ids = [pcd_info.id for pcd_info in pcd_infos]
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
pcl_to_rimg_figures: Dict[int, Dict[str, List[Dict]]] = {}
|
|
112
|
+
pcl_to_hash_to_id: Dict[int, Dict[str, int]] = {}
|
|
113
|
+
key_id_map = KeyIdMap()
|
|
114
|
+
for pcd_id, ann, item in zip(pcd_ids, anns, batch):
|
|
103
115
|
if ann is not None:
|
|
104
|
-
api.pointcloud.annotation.append(pcd_id, ann)
|
|
116
|
+
api.pointcloud.annotation.append(pcd_id, ann, key_id_map)
|
|
105
117
|
|
|
106
118
|
rimg_infos = []
|
|
107
119
|
camera_names = []
|
|
108
|
-
for img_ind,
|
|
120
|
+
for img_ind, rel_tuple in enumerate(item._related_images):
|
|
121
|
+
img_path = rel_tuple[0]
|
|
122
|
+
rimg_ann_path = rel_tuple[1]
|
|
123
|
+
fig_path = rel_tuple[2] if len(rel_tuple) > 2 else None
|
|
109
124
|
meta_json = load_json_file(rimg_ann_path)
|
|
110
125
|
try:
|
|
111
126
|
if ApiField.META not in meta_json:
|
|
112
127
|
raise ValueError("Related image meta not found in json file.")
|
|
113
128
|
if ApiField.NAME not in meta_json:
|
|
114
129
|
raise ValueError("Related image name not found in json file.")
|
|
115
|
-
|
|
130
|
+
img_hash = api.pointcloud.upload_related_image(img_path)
|
|
116
131
|
if "deviceId" not in meta_json[ApiField.META].keys():
|
|
117
132
|
camera_names.append(f"CAM_{str(img_ind).zfill(2)}")
|
|
118
133
|
else:
|
|
@@ -121,17 +136,87 @@ class PointcloudConverter(BaseConverter):
|
|
|
121
136
|
{
|
|
122
137
|
ApiField.ENTITY_ID: pcd_id,
|
|
123
138
|
ApiField.NAME: meta_json[ApiField.NAME],
|
|
124
|
-
ApiField.HASH:
|
|
139
|
+
ApiField.HASH: img_hash,
|
|
125
140
|
ApiField.META: meta_json[ApiField.META],
|
|
126
141
|
}
|
|
127
142
|
)
|
|
128
|
-
|
|
143
|
+
|
|
144
|
+
if fig_path is not None and os.path.isfile(fig_path):
|
|
145
|
+
try:
|
|
146
|
+
figs_json = load_json_file(fig_path)
|
|
147
|
+
pcl_to_rimg_figures.setdefault(pcd_id, {})[img_hash] = figs_json
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.debug(f"Failed to read figures json '{fig_path}': {repr(e)}")
|
|
150
|
+
|
|
129
151
|
except Exception as e:
|
|
130
152
|
logger.warn(
|
|
131
153
|
f"Failed to upload related image or add it to pointcloud: {repr(e)}"
|
|
132
154
|
)
|
|
133
155
|
continue
|
|
134
156
|
|
|
157
|
+
# add images for this point cloud
|
|
158
|
+
if len(rimg_infos) > 0:
|
|
159
|
+
try:
|
|
160
|
+
uploaded_rimgs = api.pointcloud.add_related_images(rimg_infos, camera_names)
|
|
161
|
+
# build mapping hash->id
|
|
162
|
+
for info, uploaded in zip(rimg_infos, uploaded_rimgs):
|
|
163
|
+
img_hash = info.get(ApiField.HASH)
|
|
164
|
+
img_id = (
|
|
165
|
+
uploaded.get(ApiField.ID)
|
|
166
|
+
if isinstance(uploaded, dict)
|
|
167
|
+
else getattr(uploaded, "id", None)
|
|
168
|
+
)
|
|
169
|
+
if img_hash is not None and img_id is not None:
|
|
170
|
+
pcl_to_hash_to_id.setdefault(pcd_id, {})[img_hash] = img_id
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.debug(f"Failed to add related images to pointcloud: {repr(e)}")
|
|
173
|
+
|
|
174
|
+
# ---- upload figures for processed batch ----
|
|
175
|
+
if len(pcl_to_rimg_figures) > 0:
|
|
176
|
+
try:
|
|
177
|
+
dataset_info = api.dataset.get_info_by_id(dataset_id)
|
|
178
|
+
project_id = dataset_info.project_id
|
|
179
|
+
|
|
180
|
+
figures_payload: List[Dict] = []
|
|
181
|
+
|
|
182
|
+
for pcl_id, hash_to_figs in pcl_to_rimg_figures.items():
|
|
183
|
+
hash_to_ids = pcl_to_hash_to_id.get(pcl_id, {})
|
|
184
|
+
if len(hash_to_ids) == 0:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
for img_hash, figs_json in hash_to_figs.items():
|
|
188
|
+
if img_hash not in hash_to_ids:
|
|
189
|
+
continue
|
|
190
|
+
rimg_id = hash_to_ids[img_hash]
|
|
191
|
+
for fig in figs_json:
|
|
192
|
+
try:
|
|
193
|
+
fig[ApiField.ENTITY_ID] = rimg_id
|
|
194
|
+
fig[ApiField.DATASET_ID] = dataset_id
|
|
195
|
+
fig[ApiField.PROJECT_ID] = project_id
|
|
196
|
+
if OBJECT_KEY in fig:
|
|
197
|
+
fig[ApiField.OBJECT_ID] = key_id_map.get_object_id(
|
|
198
|
+
UUID(fig[OBJECT_KEY])
|
|
199
|
+
)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.debug(
|
|
202
|
+
f"Failed to process figure json for img_hash={img_hash}: {repr(e)}"
|
|
203
|
+
)
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
figures_payload.extend(figs_json)
|
|
207
|
+
|
|
208
|
+
if len(figures_payload) > 0:
|
|
209
|
+
try:
|
|
210
|
+
api.image.figure.create_bulk(
|
|
211
|
+
figures_json=figures_payload, dataset_id=dataset_id
|
|
212
|
+
)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.debug(
|
|
215
|
+
f"Failed to upload figures for related images: {repr(e)}"
|
|
216
|
+
)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.debug(f"Unexpected error during related image figures upload: {repr(e)}")
|
|
219
|
+
|
|
135
220
|
if log_progress:
|
|
136
221
|
progress_cb(len(batch))
|
|
137
222
|
|
|
@@ -143,7 +228,7 @@ class PointcloudConverter(BaseConverter):
|
|
|
143
228
|
def _collect_items_if_format_not_detected(self) -> Tuple[List[Item], bool, Set[str]]:
|
|
144
229
|
only_modality_items = True
|
|
145
230
|
unsupported_exts = set()
|
|
146
|
-
pcd_list, rimg_dict, rimg_ann_dict = [], {}, {}
|
|
231
|
+
pcd_list, rimg_dict, rimg_ann_dict, rimg_fig_dict = [], {}, {}, {}
|
|
147
232
|
used_img_ext = set()
|
|
148
233
|
for root, _, files in os.walk(self._input_data):
|
|
149
234
|
for file in files:
|
|
@@ -170,6 +255,8 @@ class PointcloudConverter(BaseConverter):
|
|
|
170
255
|
pcd_list.append(full_path)
|
|
171
256
|
except:
|
|
172
257
|
pass
|
|
258
|
+
elif file.endswith(".figures.json"):
|
|
259
|
+
rimg_fig_dict[file] = full_path
|
|
173
260
|
else:
|
|
174
261
|
only_modality_items = False
|
|
175
262
|
unsupported_exts.add(ext)
|
|
@@ -179,9 +266,13 @@ class PointcloudConverter(BaseConverter):
|
|
|
179
266
|
for pcd_path in pcd_list:
|
|
180
267
|
item = self.Item(pcd_path)
|
|
181
268
|
rimg, rimg_ann = helpers.find_related_items(
|
|
182
|
-
item.name, used_img_ext, rimg_dict, rimg_ann_dict
|
|
269
|
+
item.name, list(used_img_ext), rimg_dict, rimg_ann_dict
|
|
183
270
|
)
|
|
184
271
|
if rimg is not None and rimg_ann is not None:
|
|
185
|
-
|
|
272
|
+
rimg_ext = get_file_ext(rimg)
|
|
273
|
+
rimg_fig_path = rimg_fig_dict.get(f"{get_file_name(rimg)}{rimg_ext}.figures.json")
|
|
274
|
+
if rimg_fig_path is None:
|
|
275
|
+
rimg_fig_path = rimg_fig_dict.get(f"{get_file_name(rimg)}.figures.json")
|
|
276
|
+
item.set_related_images((rimg, rimg_ann, rimg_fig_path))
|
|
186
277
|
items.append(item)
|
|
187
278
|
return items, only_modality_items, unsupported_exts
|
|
@@ -101,7 +101,11 @@ class SLYPointcloudConverter(PointcloudConverter):
|
|
|
101
101
|
item.name, used_img_ext, rimg_dict, rimg_ann_dict
|
|
102
102
|
)
|
|
103
103
|
if rimg is not None and rimg_ann is not None:
|
|
104
|
-
|
|
104
|
+
rimg_ext = get_file_ext(rimg)
|
|
105
|
+
rimg_fig_path = rimg_ann.replace(f"{rimg_ext}.json", f"{rimg_ext}.figures.json")
|
|
106
|
+
if not os.path.exists(rimg_fig_path):
|
|
107
|
+
rimg_fig_path = None
|
|
108
|
+
item.set_related_images((rimg, rimg_ann, rimg_fig_path))
|
|
105
109
|
self._items.append(item)
|
|
106
110
|
return sly_ann_detected
|
|
107
111
|
|