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.
@@ -1,6 +1,7 @@
1
1
  import imghdr
2
2
  import os
3
3
  from typing import Dict, List, Optional, Set, Tuple, Union
4
+ from uuid import UUID
4
5
 
5
6
  from supervisely import (
6
7
  Api,
@@ -16,7 +17,9 @@ from supervisely.io.fs import get_file_ext, get_file_name
16
17
  from supervisely.io.json import load_json_file
17
18
  from supervisely.pointcloud.pointcloud import ALLOWED_POINTCLOUD_EXTENSIONS
18
19
  from supervisely.pointcloud.pointcloud import validate_ext as validate_pcd_ext
20
+ from supervisely.pointcloud_annotation.constants import OBJECT_KEY
19
21
  from supervisely.project.project_settings import LabelingInterface
22
+ from supervisely.video_annotation.key_id_map import KeyIdMap
20
23
 
21
24
 
22
25
  class PointcloudEpisodeConverter(BaseConverter):
@@ -47,7 +50,14 @@ class PointcloudEpisodeConverter(BaseConverter):
47
50
  def create_empty_annotation(self) -> PointcloudEpisodeAnnotation:
48
51
  return PointcloudEpisodeAnnotation()
49
52
 
50
- def set_related_images(self, related_images: dict) -> None:
53
+ def set_related_images(self, related_images: Tuple[str, str, Optional[str]]) -> None:
54
+ """Adds related image to the item.
55
+
56
+ related_images tuple:
57
+ - path to image
58
+ - path to .json with image metadata
59
+ - path to .figures.json (can be None if no figures)
60
+ """
51
61
  self._related_images.append(related_images)
52
62
 
53
63
  def __init__(
@@ -104,7 +114,10 @@ class PointcloudEpisodeConverter(BaseConverter):
104
114
  else:
105
115
  progress_cb = None
106
116
 
107
- frame_to_pointcloud_ids = {}
117
+ frame_to_pointcloud_ids: Dict[int, int] = {}
118
+ pcl_to_rimg_figures: Dict[int, Dict[str, List[Dict]]] = {}
119
+ pcl_to_hash_to_id: Dict[int, Dict[str, int]] = {}
120
+ key_id_map = KeyIdMap()
108
121
  for batch in batched(self._items, batch_size=batch_size):
109
122
  item_names = []
110
123
  item_paths = []
@@ -129,12 +142,21 @@ class PointcloudEpisodeConverter(BaseConverter):
129
142
  rimg_infos = []
130
143
  camera_names = []
131
144
  if len(item._related_images) > 0:
132
- img_paths, rimg_ann_paths = list(zip(*item._related_images))
145
+ img_paths: List[str] = []
146
+ ann_paths: List[str] = []
147
+ fig_paths: List[Optional[str]] = []
148
+
149
+ for triple in item._related_images:
150
+ img_paths.append(triple[0])
151
+ ann_paths.append(triple[1])
152
+ fig_paths.append(triple[2] if len(triple) > 2 else None)
153
+
133
154
  rimg_hashes = api.pointcloud_episode.upload_related_images(img_paths)
134
- for img_ind, (img_hash, rimg_ann_path) in enumerate(
135
- zip(rimg_hashes, rimg_ann_paths)
155
+
156
+ for img_ind, (img_hash, ann_path, fig_path) in enumerate(
157
+ zip(rimg_hashes, ann_paths, fig_paths)
136
158
  ):
137
- meta_json = load_json_file(rimg_ann_path)
159
+ meta_json = load_json_file(ann_path)
138
160
  try:
139
161
  if ApiField.META not in meta_json:
140
162
  raise ValueError("Related image meta not found in json file.")
@@ -152,19 +174,102 @@ class PointcloudEpisodeConverter(BaseConverter):
152
174
  ApiField.META: meta_json[ApiField.META],
153
175
  }
154
176
  )
155
- api.pointcloud.add_related_images(rimg_infos, camera_names)
177
+
178
+ if fig_path is not None:
179
+ try:
180
+ figs_json = load_json_file(fig_path)
181
+ pcl_to_rimg_figures.setdefault(pcd_id, {})[img_hash] = figs_json
182
+ except Exception as e:
183
+ logger.debug(
184
+ f"Failed to read figures json '{fig_path}': {repr(e)}"
185
+ )
186
+
156
187
  except Exception as e:
157
188
  logger.warning(
158
- f"Failed to upload related image or add it to pointcloud episo: {repr(e)}"
189
+ f"Failed to process related image meta '{ann_path}': {repr(e)}"
159
190
  )
160
191
  continue
161
192
 
193
+ uploaded_rimgs = api.pointcloud.add_related_images(rimg_infos, camera_names)
194
+ try:
195
+ # build mapping hash->id
196
+ for info, uploaded in zip(rimg_infos, uploaded_rimgs):
197
+ img_hash = info.get(ApiField.HASH)
198
+ img_id = (
199
+ uploaded.get(ApiField.ID)
200
+ if isinstance(uploaded, dict)
201
+ else getattr(uploaded, "id", None)
202
+ )
203
+ if img_hash is not None and img_id is not None:
204
+ pcl_to_hash_to_id.setdefault(pcd_id, {})[img_hash] = img_id
205
+ except Exception as e:
206
+ logger.debug(
207
+ f"Failed to build hash->ID mapping for related images: {repr(e)}"
208
+ )
209
+
162
210
  if log_progress:
163
211
  progress_cb(len(batch))
164
212
 
165
213
  if self.items_count > 0:
166
214
  ann = self.to_supervisely(self._items[0], meta, renamed_classes, renamed_tags)
167
- api.pointcloud_episode.annotation.append(dataset_id, ann, frame_to_pointcloud_ids)
215
+ api.pointcloud_episode.annotation.append(
216
+ dataset_id, ann, frame_to_pointcloud_ids, key_id_map
217
+ )
218
+
219
+ # ---- upload figures for processed batch ----
220
+ if len(pcl_to_rimg_figures) > 0:
221
+ if log_progress:
222
+ progress, progress_cb = self.get_progress(
223
+ self.items_count, "Uploading figures for related images..."
224
+ )
225
+ else:
226
+ progress_cb = None
227
+
228
+ try:
229
+ dataset_info = api.dataset.get_info_by_id(dataset_id)
230
+ project_id = dataset_info.project_id
231
+
232
+ figures_to_upload: List[Dict] = []
233
+ for pcl_id, hash_to_figs in pcl_to_rimg_figures.items():
234
+ hash_to_ids = pcl_to_hash_to_id.get(pcl_id, {})
235
+ if len(hash_to_ids) == 0:
236
+ continue
237
+
238
+ for img_hash, figs_json in hash_to_figs.items():
239
+ if img_hash not in hash_to_ids:
240
+ continue
241
+ img_id = hash_to_ids[img_hash]
242
+
243
+ for fig in figs_json:
244
+ try:
245
+ fig[ApiField.ENTITY_ID] = img_id
246
+ fig[ApiField.DATASET_ID] = dataset_id
247
+ fig[ApiField.PROJECT_ID] = project_id
248
+ if OBJECT_KEY in fig:
249
+ fig[ApiField.OBJECT_ID] = key_id_map.get_object_id(
250
+ UUID(fig[OBJECT_KEY])
251
+ )
252
+ except Exception as e:
253
+ logger.debug(
254
+ f"Failed to process figure json for img_hash={img_hash}: {repr(e)}"
255
+ )
256
+ continue
257
+
258
+ figures_to_upload.extend(figs_json)
259
+
260
+ if len(figures_to_upload) > 0:
261
+ try:
262
+ api.image.figure.create_bulk(
263
+ figures_json=figures_to_upload, dataset_id=dataset_id
264
+ )
265
+ except Exception as e:
266
+ logger.debug(f"Failed to upload figures for related images: {repr(e)}")
267
+
268
+ if log_progress:
269
+ progress_cb(len(pcl_to_rimg_figures))
270
+
271
+ except Exception as e:
272
+ logger.debug(f"Unexpected error during figures upload: {repr(e)}")
168
273
 
169
274
  if log_progress:
170
275
  if is_development():
@@ -177,7 +282,7 @@ class PointcloudEpisodeConverter(BaseConverter):
177
282
  pcd_dict = {}
178
283
  frames_pcd_map = None
179
284
  used_img_ext = set()
180
- rimg_dict, rimg_json_dict = {}, {}
285
+ rimg_dict, rimg_json_dict, rimg_fig_dict = {}, {}, {}
181
286
  for root, _, files in os.walk(self._input_data):
182
287
  for file in files:
183
288
  full_path = os.path.join(root, file)
@@ -187,7 +292,9 @@ class PointcloudEpisodeConverter(BaseConverter):
187
292
 
188
293
  ext = get_file_ext(full_path)
189
294
  recognized_ext = imghdr.what(full_path)
190
- if ext == ".json":
295
+ if file.endswith(".figures.json"):
296
+ rimg_fig_dict[file] = full_path
297
+ elif ext == ".json":
191
298
  rimg_json_dict[file] = full_path
192
299
  elif recognized_ext:
193
300
  if ext.lower() == ".pcd":
@@ -226,9 +333,12 @@ class PointcloudEpisodeConverter(BaseConverter):
226
333
  if rimg_name in rimg_dict:
227
334
  rimg_path = rimg_dict[rimg_name]
228
335
  rimg_ann_name = f"{rimg_name}.json"
336
+ rimg_fig_name = f"{rimg_name}.figures.json"
337
+ rimg_fig_path = rimg_fig_dict.get(rimg_fig_name)
338
+
229
339
  if rimg_ann_name in rimg_json_dict:
230
340
  rimg_ann_path = rimg_json_dict[rimg_ann_name]
231
- item.set_related_images((rimg_path, rimg_ann_path))
341
+ item.set_related_images((rimg_path, rimg_ann_path, rimg_fig_path))
232
342
  items.append(item)
233
343
  else:
234
344
  logger.warning(f"Pointcloud file {pcd_name} not found. Skipping frame.")
@@ -127,7 +127,11 @@ class SLYPointcloudEpisodesConverter(PointcloudEpisodeConverter):
127
127
  rimg_ann_name = f"{rimg_name}.json"
128
128
  if rimg_ann_name in rimg_json_dict:
129
129
  rimg_ann_path = rimg_json_dict[rimg_ann_name]
130
- item.set_related_images((rimg_path, rimg_ann_path))
130
+ rimg_fig_name = f"{rimg_name}.figures.json"
131
+ rimg_fig_path = os.path.join(os.path.dirname(rimg_ann_path), rimg_fig_name)
132
+ if not os.path.exists(rimg_fig_path):
133
+ rimg_fig_path = None
134
+ item.set_related_images((rimg_path, rimg_ann_path, rimg_fig_path))
131
135
  self._items.append(item)
132
136
  else:
133
137
  logger.warning(f"Pointcloud file {pcd_name} not found. Skipping frame.")
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
  import os
7
7
  import random
8
8
  from typing import Callable, Dict, List, NamedTuple, Optional, Tuple, Union
9
+ from uuid import UUID
9
10
 
10
11
  from tqdm import tqdm
11
12
 
@@ -17,6 +18,7 @@ from supervisely.api.pointcloud.pointcloud_api import PointcloudInfo
17
18
  from supervisely.collection.key_indexed_collection import KeyIndexedCollection
18
19
  from supervisely.io.fs import dir_exists, get_file_name, list_files, mkdir, touch
19
20
  from supervisely.io.json import dump_json_file, load_json_file
21
+ from supervisely.pointcloud_annotation.constants import OBJECT_KEY
20
22
  from supervisely.pointcloud_annotation.pointcloud_episode_annotation import (
21
23
  PointcloudEpisodeAnnotation,
22
24
  )
@@ -354,6 +356,7 @@ class PointcloudEpisodeProject(PointcloudProject):
354
356
  def _list_items_for_splits(project) -> List[EpisodeItemInfo]:
355
357
  items = []
356
358
  for dataset in project.datasets:
359
+ dataset: PointcloudEpisodeDataset
357
360
  for item_name in dataset:
358
361
  items.append(
359
362
  EpisodeItemInfo(
@@ -745,7 +748,7 @@ def download_pointcloud_episode_project(
745
748
  datasets_infos = api.dataset.get_list(project_id)
746
749
 
747
750
  for dataset in datasets_infos:
748
- dataset_fs = project_fs.create_dataset(dataset.name)
751
+ dataset_fs: PointcloudEpisodeDataset = project_fs.create_dataset(dataset.name)
749
752
  pointclouds = api.pointcloud_episode.get_list(dataset.id)
750
753
 
751
754
  # Download annotation to project_path/dataset_path/annotation.json
@@ -774,6 +777,30 @@ def download_pointcloud_episode_project(
774
777
  pc_to_frame = {v: k for k, v in frame_to_pc_map.items()}
775
778
  item_to_ann = {name: pc_to_frame[name] for name in pointcloud_names}
776
779
 
780
+ batch_rimg_figures = {}
781
+ if download_related_images:
782
+ try:
783
+ rimgs = api.pointcloud_episode.get_list_related_images_batch(
784
+ dataset.id, pointcloud_ids
785
+ )
786
+ if len(rimgs) > 0:
787
+ rimg_ids = [rimg[ApiField.ID] for rimg in rimgs]
788
+ batch_rimg_figures = api.image.figure.download(
789
+ dataset_id=dataset.id, image_ids=rimg_ids
790
+ )
791
+ else:
792
+ batch_rimg_figures = []
793
+ except Exception as e:
794
+ logger.info(
795
+ "INFO FOR DEBUGGING",
796
+ extra={
797
+ "project_id": project_id,
798
+ "dataset_id": dataset.id,
799
+ "pointcloud_ids": pointcloud_ids,
800
+ },
801
+ )
802
+ raise e
803
+
777
804
  for pointcloud_id, pointcloud_name, pointcloud_info in zip(
778
805
  pointcloud_ids, pointcloud_names, batch
779
806
  ):
@@ -830,6 +857,7 @@ def download_pointcloud_episode_project(
830
857
 
831
858
  path_img = os.path.join(related_images_path, name)
832
859
  path_json = os.path.join(related_images_path, name + ".json")
860
+ path_figures = os.path.join(related_images_path, name + ".figures.json")
833
861
 
834
862
  try:
835
863
  api.pointcloud_episode.download_related_image(rimage_id, path_img)
@@ -846,9 +874,37 @@ def download_pointcloud_episode_project(
846
874
  },
847
875
  )
848
876
  raise e
849
-
850
877
  dump_json_file(rimage_info, path_json)
851
878
 
879
+ try:
880
+ if rimage_id in batch_rimg_figures:
881
+ rimg_figures = batch_rimg_figures[rimage_id]
882
+ rimg_figures_json = []
883
+ for fig in rimg_figures:
884
+ fig_json = fig.to_json()
885
+ if ApiField.OBJECT_ID in fig_json:
886
+ fig_json[OBJECT_KEY] = str(
887
+ key_id_map.get_object_key(fig_json[ApiField.OBJECT_ID])
888
+ )
889
+ fig_json.pop(ApiField.OBJECT_ID, None)
890
+ rimg_figures_json.append(fig_json)
891
+ else:
892
+ raise RuntimeError(f"Figure {fig} has no object id")
893
+ dump_json_file(rimg_figures_json, path_figures)
894
+ except Exception as e:
895
+ logger.info(
896
+ "INFO FOR DEBUGGING",
897
+ extra={
898
+ "project_id": project_id,
899
+ "dataset_id": dataset.id,
900
+ "pointcloud_id": pointcloud_id,
901
+ "pointcloud_name": pointcloud_name,
902
+ "rimage_id": rimage_id,
903
+ "path_figures": path_figures,
904
+ },
905
+ )
906
+ raise e
907
+
852
908
  pointcloud_info = pointcloud_info._asdict() if download_pointclouds_info else None
853
909
  try:
854
910
  dataset_fs.add_item_file(
@@ -901,6 +957,7 @@ def upload_pointcloud_episode_project(
901
957
 
902
958
  key_id_map = KeyIdMap()
903
959
  for dataset_fs in project_fs.datasets:
960
+ dataset_fs: PointcloudEpisodeDataset
904
961
  ann_json_path = dataset_fs.get_ann_path()
905
962
 
906
963
  if os.path.isfile(ann_json_path):
@@ -974,8 +1031,9 @@ def upload_pointcloud_episode_project(
974
1031
 
975
1032
  # STEP 3 — upload photo context
976
1033
  img_infos = {"img_paths": [], "img_metas": []}
977
-
978
1034
  # STEP 3.1 — upload images
1035
+ pcl_to_rimg_figures: Dict[int, Dict[str, List[Dict]]] = {}
1036
+ pcl_to_hash_to_id: Dict[int, Dict[str, int]] = {}
979
1037
  for pcl_info in pcl_infos:
980
1038
  related_items = dataset_fs.get_related_images(pcl_info.name)
981
1039
  images_paths_for_frame = [img_path for img_path, _ in related_items]
@@ -1014,6 +1072,7 @@ def upload_pointcloud_episode_project(
1014
1072
  img_hash = next(images_hashes_iterator)
1015
1073
  if "deviceId" not in meta_json[ApiField.META].keys():
1016
1074
  meta_json[ApiField.META]["deviceId"] = f"CAM_{str(img_ind).zfill(2)}"
1075
+
1017
1076
  img_infos["img_metas"].append(
1018
1077
  {
1019
1078
  ApiField.ENTITY_ID: pcl_info.id,
@@ -1022,10 +1081,38 @@ def upload_pointcloud_episode_project(
1022
1081
  ApiField.META: meta_json[ApiField.META],
1023
1082
  }
1024
1083
  )
1084
+ img_name = meta_json[ApiField.NAME]
1085
+ related_images_dir = dataset_fs.get_related_images_path(pcl_info.name)
1086
+ fig_json_path = os.path.join(related_images_dir, img_name + ".figures.json")
1087
+ if os.path.isfile(fig_json_path):
1088
+ try:
1089
+ figs_json = load_json_file(fig_json_path)
1090
+ pcl_to_rimg_figures.setdefault(pcl_info.id, {})[img_hash] = figs_json
1091
+ except Exception as e:
1092
+ logger.info(
1093
+ "INFO FOR DEBUGGING",
1094
+ extra={
1095
+ "project_id": project.id,
1096
+ "dataset_id": dataset.id,
1097
+ "pointcloud_id": pcl_info.id,
1098
+ "fig_json_path": fig_json_path,
1099
+ },
1100
+ )
1101
+ raise e
1025
1102
 
1026
1103
  if len(img_infos["img_metas"]) > 0:
1027
1104
  try:
1028
- api.pointcloud_episode.add_related_images(img_infos["img_metas"])
1105
+ uploaded_rimgs = api.pointcloud_episode.add_related_images(img_infos["img_metas"])
1106
+ # build mapping hash->id
1107
+ for info, uploaded in zip(img_infos["img_metas"], uploaded_rimgs):
1108
+ img_hash = info.get(ApiField.HASH)
1109
+ img_id = (
1110
+ uploaded.get(ApiField.ID)
1111
+ if isinstance(uploaded, dict)
1112
+ else getattr(uploaded, "id", None)
1113
+ )
1114
+ if img_hash is not None and img_id is not None:
1115
+ pcl_to_hash_to_id[img_hash] = img_id
1029
1116
  except Exception as e:
1030
1117
  logger.info(
1031
1118
  "INFO FOR DEBUGGING",
@@ -1037,4 +1124,39 @@ def upload_pointcloud_episode_project(
1037
1124
  )
1038
1125
  raise e
1039
1126
 
1127
+ for pcl_info in pcl_infos:
1128
+ rimg_figures = pcl_to_rimg_figures.get(pcl_info.id)
1129
+ if not rimg_figures:
1130
+ continue
1131
+
1132
+ try:
1133
+ for img_hash, figs_json in rimg_figures.items():
1134
+ if img_hash in pcl_to_hash_to_id:
1135
+ img_id = pcl_to_hash_to_id[img_hash]
1136
+ for fig in figs_json:
1137
+ fig[ApiField.ENTITY_ID] = img_id
1138
+ fig[ApiField.DATASET_ID] = dataset.id
1139
+ fig[ApiField.PROJECT_ID] = project.id
1140
+ fig[ApiField.OBJECT_ID] = key_id_map.get_object_id(
1141
+ UUID(fig[OBJECT_KEY])
1142
+ )
1143
+
1144
+ api.image.figure.create_bulk(
1145
+ figures_json=[fig for figs in rimg_figures.values() for fig in figs],
1146
+ dataset_id=dataset.id,
1147
+ )
1148
+
1149
+ except Exception as e:
1150
+ logger.info(
1151
+ "INFO FOR DEBUGGING",
1152
+ extra={
1153
+ "project_id": project.id,
1154
+ "dataset_id": dataset.id,
1155
+ "pointcloud_id": pcl_info.id,
1156
+ "pointcloud_name": pcl_info.name,
1157
+ "rimg_figures": rimg_figures,
1158
+ },
1159
+ )
1160
+ raise e
1161
+
1040
1162
  return project.id, project.name