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 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.12.44"
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(self, entity_id: int, tags_json: dict) -> list:
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
- response = self._api.post(
232
- "annotation-objects.tags.bulk.add",
233
- {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: tags_json},
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[dict]:
283
+ ) -> List[Dict[str, Union[str, int, None]]]:
246
284
  """
247
- Add Tags to existing Annotation Figures.
248
- All figures must belong to entities of the same project.
249
- Applies only to images project.
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[dict]
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 # value is optional for tag with type '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
- response = self._api.post("figures.tags.bulk.add", data)
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)
@@ -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 update_tag_value(self, tag_id: int, value: Union[str, float]) -> Dict:
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
- ann_name = f"{item.name}.json"
126
- if ann_name in ann_dict:
127
- ann_path = ann_dict[ann_name]
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
- detected_ann_cnt += 1
134
- if ann_name in img_meta_dict:
135
- item.set_meta_data(img_meta_dict[ann_name])
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
- for pcd_id, ann in zip(pcd_ids, anns):
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, (img_path, rimg_ann_path) in enumerate(item._related_images):
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
- img = api.pointcloud.upload_related_image(img_path)
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: img,
139
+ ApiField.HASH: img_hash,
125
140
  ApiField.META: meta_json[ApiField.META],
126
141
  }
127
142
  )
128
- api.pointcloud.add_related_images(rimg_infos, camera_names)
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
- item.set_related_images((rimg, rimg_ann))
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
- item.set_related_images((rimg, rimg_ann))
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